KB-1C97

00 Summary — VERIFY_MARK CUT-Readiness Gate Design + Fix (PASS, 2026-05-27)

8 min read Revision 1
dieu44iu-cutverify-markpreflightgovernancepass2026-05-27

00 — Summary: IU_CUT_VERIFY_MARK_CUT_READINESS_GATE_FIX_PASS

Date: 2026-05-27 Mission: IU_CUT_VERIFY_MARK_CUT_READINESS_GATE_DESIGN_AND_FIX Result: IU_CUT_VERIFY_MARK_CUT_READINESS_GATE_FIX_PASS Channel: local Claude Code CLI → ssh contabo → docker exec postgres → psql workflow_admin@directus (LIVE APPLY, not query_pg read-only).

Root problem fixed

VERIFY_MARK was an incomplete gate: it approved manifests that later failed CUT. Concretely, manifest 9fa4685e-d35a-45d4-aee7-aa2836785ca5 (Điều 39) was approved by an older fn_iu_verify_mark and then CUT refused all 16 pieces because unit_kind=law_section is not in dot_config vocab.unit_kind.* (live keys: design_doc_section, law_unit).

After mig 054 added per-piece Axis D at MARK + verify_mark, the runtime contract converged for new manifests, but verify_mark still did not enforce the full set of CUT-time refusals that fn_iu_cut_from_manifest + fn_iu_create apply (e.g. section_type vocab existence, canonical_address collision with existing IU, derivable title).

Mig 055 — what changed

Single-TX, LIVE APPLY via SSH→docker→psql:

  1. NEW public.fn_iu_cut_preflight_validate(p_staging_record_id, p_source_hash, p_actor)

    • LANGUAGE plpgsql STABLE — read-only deterministic verdict.
    • Works on any lifecycle_status (no pending_review constraint), no IU creation, no Qdrant, no mutation.
    • Returns structured {ok, verdict, axis_a_ok..d_ok, cut_readiness_ok, problems[], counts{}, gates{}, lifecycle_status_at_check, checked_at, checked_by}.
    • Checks: Axes A/B/C/D (unchanged from prior verify_mark) + new E1 section_type ∈ vocab + E2 canonical_address collision + E3 publication_type vocab + E4 title derivable + E5 local_piece_id unique + E6 manifest_digest 32-hex + E7 coverage_proof.covered_bytes = manifest.source_bytes + optional E0 source_hash match.
    • md5(funcdef) = 914e26d61de0de914408af5cdc679c07.
  2. UPDATED public.fn_iu_verify_mark

    • Delegates to fn_iu_cut_preflight_validate.
    • approve=true is refused unless cut_readiness_ok=true.
    • On reject + p_apply=true, lifecycle moves pending_review → rejected with verify_mark_preflight audit pinned in metadata.
    • md5(funcdef): 1db15847b1c48e3b86568b712a15cfd6c9c0553f3184bfaa3f2ee5488b5ff46c.
  3. UNCHANGED — alias contract preserved (governance pin):

    • fn_iu_op_verify_mark md5 bf20bd1929998073865808d17b1dd648 ✓ (pinned)
    • fn_iu_op_mark_file md5 ffaa47fff7a906d93060141661080cd4
    • fn_iu_op_cut md5 66b813e50205448eb01170aebec614df
    • fn_iu_op_verify_cut md5 ac61dade6519694310cbfd75d8b549fb
    • fn_iu_cut_from_manifest md5 c5d556bc22cc2d255c0484b5a969ebc5
    • fn_cut_mark_staged_file md5 e85065acb9996623e0ef1f654d991df6
    • fn_iu_create md5 dcade99af1ef096892748c9f14082e11
    • fn_cut_request_transition md5 b6845f31889e701cea2d009b12cd823f

No vocab widening. No silent mapping (law_section is NOT mapped to law_unit). No runtime.phase flip. No pg_cron. No worker. No production_documents. No Qdrant.

PHASE E — Điều 39 invalid manifest caught at preflight (no mutation)

Calling preflight on 9fa4685e returned:

ok=false  verdict=rejected  cut_readiness_ok=false
axis_a_ok=true  axis_b_ok=true  axis_c_ok=true  axis_d_ok=false
gates: e1..e7 ALL true
counts.unit_kind_in_vocab=0 (of 16), unit_kind_missing=0
problems=[
  "Axis D: unit_kind compatibility failed (total=16, missing=0, in_vocab=0) -- every piece.unit_kind must exist in dot_config vocab.unit_kind.*"
]
lifecycle_status_at_check=approved   ← read-only inspection, manifest NOT mutated

This proves the isolated root cause: only unit_kind is wrong. Everything else is well-formed. The manifest is still lifecycle_status=approved and cut_request_id=146f1520 is still mark_verified with cut_run_id=NULL. No CUT was run. Rollback policy is handled in 07-next-state-machine-rollback-policy.md.

PHASE F — Regression matrix PASS (BEGIN/ROLLBACK)

# fixture expected result
F.1 valid 2-piece law_unit manifest ok=true verdict=approved cut_readiness_ok=true PASS
F.2 invalid unit_kind=law_section rejected on Axis D PASS
F.3 empty content_text AND empty canonical_address rejected on E4 PASS
F.4 canonical_address = existing IU D38-DIEU35-S8-P1 rejected on E2 (collisions=1) PASS
F.5 section_type=not_a_real_section rejected on E1 PASS
F.6 fn_iu_verify_mark(apply=true) with bad unit_kind ok=false verdict=rejected AND lifecycle_after=rejected (NOT approved) PASS — gate works
F.7 fn_iu_verify_mark(apply=false) dry-run with bad unit_kind rejected verdict, lifecycle_after=pending_review (no mutation) PASS
F.8 p_source_hash mismatch E0 mismatch problem PASS

All 8 fixtures were BEGIN/ROLLBACK'd. Nothing persisted.

Baseline → exit deltas

metric baseline exit
information_unit count 200 200
iu_vector_sync_point count 152 152
iu_staging_record count 10 10
dieu39_iu_count (CUT'd IUs for Điều 39) 0 0
cut_request 146f1520 status mark_verified mark_verified
manifest 9fa4685e lifecycle approved approved
cut_request 146f1520 cut_run_id NULL NULL
gate queue.job_substrate.enabled false false
gate iu_core.composer_enabled false false
gate queue.heartbeat.enabled true true
gate queue.dlq.replay_enabled false false
production_documents table absent absent
pg_cron extension absent absent
pg_dump bytes 84,298,333 84,306,773 (+8,440 B)

15/15 forbiddens honored

  1. No pg_cron. 2. No worker. 3. No event_outbox mutation. 4. No production_documents. 5. No Qdrant. 6. No runtime.phase flip. 7. No new cut_request. 8. No CUT executed. 9. No VERIFY_CUT. 10. No COMPLETE. 11. No manual manifest patch on 9fa4685e. 12. No silent law_section → law_unit mapping. 13. No vocab widening. 14. No alias contract change (6 alias md5s pinned). 15. No runtime.phase change.

Carry-forward (next packs)

  • CF-1 HIGHfn_cut_request_transition has NO legal mark_verified → mark_rejected rollback edge. Recovery for Điều 39 stuck-at-mark_verified requires next pack IU_CUT_STATE_MACHINE_ROLLBACK_AFTER_APPROVAL_POLICY (see 07).
  • CF-2 MEDIUMfn_iu_cut_preflight_validate is read-only and unwired from cut_request_signal event flow; consider running it during cut.mark signal as a defense-in-depth health check.
  • CF-3 LOW — Axis E1 overlaps fn_cut_mark_staged_file per-piece schema check; redundancy is intentional (preflight is the verdict authority).
  • CF-4 LOWvocab.piece_role.* still empty; not enforced today. When defined, add E8 piece_role check.
  • CF-5 LOW — Preflight could expose a --json-only REST endpoint for operator visibility.
  • Parent root-cause: [[project-iu-cut-dieu39-unit-kind-root-cause-and-contract-fix-partial-2026-05-27]]
  • Parent contract gate: [[feedback-cut-manifest-piece-schema-v1-now-7-field-unit-kind-required-in-dot-config-vocab]]
  • Sibling: [[project-iu-cut-dieu39-verify-mark-root-cause-and-contract-fix-pass-2026-05-27]]
  • Grandparent runtime hardening: [[project-iu-cut-operational-pipeline-runtime-hardening-pass-2026-05-27]]

New lesson learned

fn_iu_verify_mark must delegate to a dedicated read-only CUT-readiness preflight rather than re-implement piecewise checks. Any new CUT-time refusal reason should be added to fn_iu_cut_preflight_validate only, and fn_iu_verify_mark will inherit it automatically. See [[feedback-verify-mark-must-delegate-to-fn-iu-cut-preflight-validate]].

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-iu-cut-verify-mark-cut-readiness-gate/00-summary.md