APPROVE Stronger-Gate Fix (fn_iu_verify_mark + fn_cut_verify_mark)
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 c9c0553f → c740ff7c
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) |