Điều 39 unit_kind Root Cause + Contract Fix — Summary (PARTIAL_WITH_EXACT_GAP, 2026-05-27)
00 — Summary: IU_CUT_DIEU39_UNIT_KIND_ROOT_CAUSE_AND_CONTRACT_FIX
Result: PARTIAL_WITH_EXACT_GAP (state-machine rollback policy deferred)
Date: 2026-05-27
Channel: SSH→docker exec postgres→psql workflow_admin@directus
Migration mode: single TX, COMMITTED
What got fixed (code-side, COMMITTED)
-
fn_cut_mark_staged_filemd567721b42a55315858e3c377654c2a5b1→e85065acb9996623e0ef1f654d991df6cut_manifest_piece_schema_v1is now 7 required fields (was 6).- New per-piece check:
piece.unit_kindis required AND must exist indot_configkeyvocab.unit_kind.<value>. - Indexed error pattern:
piece[N].unit_kind <V> not in dot_config vocab.unit_kind.* (cut_manifest_piece_schema_v1). - Missing-field pattern:
piece[N].unit_kind is required (cut_manifest_piece_schema_v1).
-
fn_iu_verify_markmd504e5191c430a142712bfe63089863d44→1db15847b1c48e3b86568b712a15cfd6- New Axis D = unit_kind compatibility check.
- Returns structured verdict
axis_d_ok(no hard error). - Fail message:
Axis D: unit_kind compatibility failed (total=N, missing=M, in_vocab=K) — every piece.unit_kind must exist in dot_config vocab.unit_kind.*.
-
Five MARK/CUT operator-alias bodies UNCHANGED (md5 pinned):
fn_iu_cut_from_manifest=c5d556bc22cc2d255c0484b5a969ebc5fn_iu_op_cut=66b813e50205448eb01170aebec614dffn_iu_op_mark_file=ffaa47fff7a906d93060141661080cd4fn_iu_op_verify_mark=bf20bd1929998073865808d17b1dd648fn_iu_op_verify_cut=ac61dade6519694310cbfd75d8b549fb
-
dot_configNOT modified:vocab.unit_kind.*remains 2 entries (design_doc_section,law_unit) — vocab NOT widened.- No
mapping.unit_kind.law_sectionrow added — semantic mapping NOT introduced. - 0 rows with key matching
%law_section%.
Regression refusal proofs (BEGIN/ROLLBACK, all PASS)
| # | Test | Expected | Actual |
|---|---|---|---|
| T1 | piece.unit_kind = law_section |
refuse at MARK boundary with indexed error | piece[0].unit_kind law_section not in dot_config vocab.unit_kind.* (cut_manifest_piece_schema_v1) ✓ |
| T2 | piece.unit_kind omitted | refuse with required-field message | piece[0].unit_kind is required (cut_manifest_piece_schema_v1) ✓ |
| T3 | piece.unit_kind = law_unit (valid) |
schema gate PASS, downstream status guard fires | cannot mark from status mark_verified — must be 'copied' or 'mark_rejected' ✓ |
What did NOT happen (forbiddens honored)
- No re-MARK on Điều 39 (blocked by state-machine gap — see §07)
- No CUT executed
- No VERIFY_CUT
- No COMPLETE
- No new cut_request opened (avoids polluting source_ref with orphan)
- No state-machine widening (
mark_verified → mark_rejectednot added) - No
vocab.unit_kind.law_sectionadded (vocab NOT widened) - No silent semantic mapping
law_section → law_unit - No
jsonb_setpatch on payload pieces - No MARK/CUT alias body changed
- No
production_documentstouched (table absent: 0) - No Qdrant touched (vsp = 152 unchanged)
- No
pg_croninstall (absent: 0) - No worker started, no broad gate flip
Live state at exit
| Field | Value |
|---|---|
| cut_request_id | 146f1520-aaa2-4bda-af2c-06a8f76cd36a |
| status | mark_verified (unchanged) |
| manifest_staging_record_id | 9fa4685e-d35a-45d4-aee7-aa2836785ca5 |
| manifest_digest | aded6af91fb9643fb2ea99ff024a1ede |
| manifest lifecycle_status | approved (unchanged — not mutated) |
| cut_run_id | NULL (no CUT) |
| dieu39 IU count | 0 |
| Total IU | 200 (unchanged) |
| VSP | 152 (unchanged) |
| event_outbox | 139894 (unchanged) |
| Gates | job_substrate=false, composer=false, heartbeat=true, dlq_replay=false |
Carry-forward gap (REQUIRED before re-MARK of Điều 39)
State machine in fn_cut_request_transition has NO legal path mark_verified → mark_rejected. Legal targets from mark_verified:
mark_verified→cut_in_progress(only)
Consequence: cut_request 146f1520 cannot transition back to mark_rejected (which is the only status that allows re-MARK per fn_cut_mark_staged_file guard status NOT IN ('copied','mark_rejected')).
This gap is out of scope for the unit_kind contract fix. It requires a separate macro to design the rollback/re-mark policy for the case "manifest reached approved state, then a contract gap was discovered before CUT." See report 07-next-cut-step.md for the recommended next-macro spec.
Why PARTIAL not PASS
Per user directive (Option C): the unit_kind contract gap is fixed at the code boundary and regression-proven, but Điều 39 is NOT re-MARKed because that would require either (a) opening a parallel cut_request (orphans the source_ref) or (b) widening the state machine (out of scope). Both rejected by operator. Returns PARTIAL with exact gap so the next macro can address rollback policy cleanly.
Backup
pg_dump taken pre-fix to /tmp/pre_dieu39_unit_kind_fix_20260527.sql (921 MB, md5=2d21965b13a46ae32e29ccc6f48308fc).
Rollback path: CREATE OR REPLACE prior bodies of fn_cut_mark_staged_file (md5 67721b42...) and fn_iu_verify_mark (md5 04e5191c...) in single TX. Loses the unit_kind contract gate.
Cross-references
- Parent (most recent contract pass):
IU_CUT_DIEU39_VERIFY_MARK_ROOT_CAUSE_AND_CONTRACT_FIX_PASS_2026-05-27(mig 054) — same source_ref Điều 39, fixed VERIFY_MARK structured-verdict + cut_manifest_piece_schema_v1 (6-field). - Generalizes:
feedback-fn-cut-mark-staged-file-must-validate-piece-schema-upstream— schema gate now 6→7 fields. - Parent operational pipeline:
IU_CUT_OPERATIONAL_PIPELINE_COPY_MARK_VERIFY_CUT_PASS_2026-05-26(mig 052) — the 13-status state machine introduced here.
Next-macro recommendation
IU_CUT_STATE_MACHINE_ROLLBACK_AFTER_APPROVAL_POLICY — design and implement legal rollback path for mark_verified → mark_rejected (or analog), then re-MARK Điều 39 with corrected unit_kind=law_unit pieces.