KB-3E4B

APPROVE Stronger-Gate Fix (fn_iu_verify_mark + fn_cut_verify_mark)

4 min read Revision 1
iu-cutverify_markapprovestronger-gatecut-request-bindingmig056

Phase D — APPROVE is stronger than VERIFY

Core principle (operational)

  • VERIFY_MARK = technical checker / department head. May be read-only (p_apply=false). Detects manifest correctness AND CUT-readiness preview.
  • APPROVE_MARK (p_apply=true) = final decision gate / boss. Must be stronger than VERIFY: re-runs preflight at approval time AND adds cut_request cross-binding.

Two functions touched

fn_iu_verify_mark — md5 c9c0553fc740ff7c

Inside p_apply=true (after preflight passes), added cross-binding to cut_request:

SELECT * INTO v_cr FROM public.cut_request
  WHERE manifest_staging_record_id = p_staging_record_id
  ORDER BY requested_at DESC
  LIMIT 1
  FOR UPDATE;
IF FOUND THEN
  IF v_cr.status <> 'marked' THEN
    RETURN jsonb_build_object('ok',false,'verdict','refused_approve',
      'reason','cut_request.status must be marked at approval time', ...);
  END IF;
  IF v_cr.cut_run_id IS NOT NULL OR v_cr.cut_done_at IS NOT NULL
     OR v_cr.cut_verified_at IS NOT NULL OR v_cr.completed_at IS NOT NULL THEN
    RETURN jsonb_build_object('ok',false,'verdict','refused_approve',
      'reason','cut_request shows downstream activity; APPROVE refused', ...);
  END IF;
END IF;

Required checks at APPROVE:

check mechanism
manifest correctness preflight axes A-D + E1..E7
runtime readiness (composer gate) preflight E8
cut_readiness_ok=true preflight aggregator
manifest_staging_record_id exists iu_staging_record FOR UPDATE
manifest_digest 32-hex preflight E6
source_hash freshness (optional p_source_hash) preflight E0
cut_request.status = marked NEW cross-binding
cut_request.cut_run_id IS NULL NEW cross-binding
cut_request.cut_done_at IS NULL NEW cross-binding
cut_request.cut_verified_at IS NULL NEW cross-binding
cut_request.completed_at IS NULL NEW cross-binding
p_approval_doc_id non-empty hard refusal
p_approver non-empty hard refusal

Approval is refused without mutating lifecycle if any cross-binding fails. Legacy/standalone manifests with no cut_request row still work (cut_request_bound=false in result), but the recommended path is operator → fn_cut_verify_mark(cut_request_id, true, ...).

fn_cut_verify_mark (wrapper) — md5 2b77a680

Pre-fix wrapper transitioned marked→mark_verified purely on p_approve=true, ignoring inner refusal. Post-fix it honours the inner verdict:

v_inner      := v_alias_out->'inner_result';
v_inner_ok   := COALESCE((v_inner->>'ok')::boolean, false);
v_inner_verd := COALESCE(v_inner->>'verdict', '');

IF p_approve THEN
  IF v_inner_ok AND v_inner_verd = 'approved' THEN
    v_to_status := 'mark_verified'; v_verdict := 'approved';
  ELSE
    v_to_status := 'mark_rejected'; v_verdict := 'rejected';
  END IF;
ELSE
  v_to_status := 'mark_rejected'; v_verdict := 'rejected';
END IF;

The wrapper now produces a consistent cross-binding: cut_request.status mirrors the inner verdict. If the inner refused due to E8 composer gate, the cut_request goes to mark_rejected and the manifest goes to rejected — both audit-pinned by transition metadata {inner_verdict, inner_ok, reason}.

Live verification (T3, inside BEGIN/ROLLBACK)

Setup: revert manifest to pending_review + NULL approved_at/by/doc_id (constraint requires both ends consistent). Revert cut_request to marked. Call fn_cut_verify_mark(p_approve=true, ...) with composer=false.

output value
verdict rejected
status mark_rejected
inner_verdict rejected
inner_ok false
post cut_request.status mark_rejected
post manifest lifecycle rejected

The wrapper successfully refused approval AND kept all states consistent.

Pinned md5 of unchanged alias

function md5
fn_iu_op_verify_mark bf20bd1929998073865808d17b1dd648 (UNCHANGED)
Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-iu-cut-verify-approve-cut-gate-consistency-fix/04-approve-stronger-gate-fix.md