00 Summary — VERIFY_MARK CUT-Readiness Gate Design + Fix (PASS, 2026-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:
-
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_reviewconstraint), 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.
-
UPDATED
public.fn_iu_verify_mark- Delegates to
fn_iu_cut_preflight_validate. approve=trueis refused unlesscut_readiness_ok=true.- On reject +
p_apply=true, lifecycle movespending_review → rejectedwithverify_mark_preflightaudit pinned in metadata. - md5(funcdef):
1db15847b1c48e3b86568b712a15cfd6→c9c0553f3184bfaa3f2ee5488b5ff46c.
- Delegates to
-
UNCHANGED — alias contract preserved (governance pin):
fn_iu_op_verify_markmd5bf20bd1929998073865808d17b1dd648✓ (pinned)fn_iu_op_mark_filemd5ffaa47fff7a906d93060141661080cd4✓fn_iu_op_cutmd566b813e50205448eb01170aebec614df✓fn_iu_op_verify_cutmd5ac61dade6519694310cbfd75d8b549fb✓fn_iu_cut_from_manifestmd5c5d556bc22cc2d255c0484b5a969ebc5✓fn_cut_mark_staged_filemd5e85065acb9996623e0ef1f654d991df6✓fn_iu_createmd5dcade99af1ef096892748c9f14082e11✓fn_cut_request_transitionmd5b6845f31889e701cea2d009b12cd823f✓
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
- No
pg_cron. 2. No worker. 3. Noevent_outboxmutation. 4. Noproduction_documents. 5. No Qdrant. 6. Noruntime.phaseflip. 7. No new cut_request. 8. No CUT executed. 9. No VERIFY_CUT. 10. No COMPLETE. 11. No manual manifest patch on9fa4685e. 12. No silentlaw_section → law_unitmapping. 13. No vocab widening. 14. No alias contract change (6 alias md5s pinned). 15. Noruntime.phasechange.
Carry-forward (next packs)
- CF-1 HIGH —
fn_cut_request_transitionhas NO legalmark_verified → mark_rejectedrollback edge. Recovery for Điều 39 stuck-at-mark_verifiedrequires next packIU_CUT_STATE_MACHINE_ROLLBACK_AFTER_APPROVAL_POLICY(see 07). - CF-2 MEDIUM —
fn_iu_cut_preflight_validateis read-only and unwired fromcut_request_signalevent flow; consider running it duringcut.marksignal as a defense-in-depth health check. - CF-3 LOW — Axis E1 overlaps
fn_cut_mark_staged_fileper-piece schema check; redundancy is intentional (preflight is the verdict authority). - CF-4 LOW —
vocab.piece_role.*still empty; not enforced today. When defined, add E8 piece_role check. - CF-5 LOW — Preflight could expose a
--json-onlyREST endpoint for operator visibility.
Cross-links
- 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]].