KB-16F6

Điều 39 unit_kind Root Cause + Contract Fix — Summary (PARTIAL_WITH_EXACT_GAP, 2026-05-27)

6 min read Revision 1
iu-cutdieu39unit-kindcut-manifest-piece-schema-v1verify-mark-axis-dpartial-with-exact-gap2026-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)

  1. fn_cut_mark_staged_file md5 67721b42a55315858e3c377654c2a5b1e85065acb9996623e0ef1f654d991df6

    • cut_manifest_piece_schema_v1 is now 7 required fields (was 6).
    • New per-piece check: piece.unit_kind is required AND must exist in dot_config key vocab.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).
  2. fn_iu_verify_mark md5 04e5191c430a142712bfe63089863d441db15847b1c48e3b86568b712a15cfd6

    • 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.*.
  3. Five MARK/CUT operator-alias bodies UNCHANGED (md5 pinned):

    • fn_iu_cut_from_manifest = c5d556bc22cc2d255c0484b5a969ebc5
    • fn_iu_op_cut = 66b813e50205448eb01170aebec614df
    • fn_iu_op_mark_file = ffaa47fff7a906d93060141661080cd4
    • fn_iu_op_verify_mark = bf20bd1929998073865808d17b1dd648
    • fn_iu_op_verify_cut = ac61dade6519694310cbfd75d8b549fb
  4. dot_config NOT modified:

    • vocab.unit_kind.* remains 2 entries (design_doc_section, law_unit) — vocab NOT widened.
    • No mapping.unit_kind.law_section row 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_rejected not added)
  • No vocab.unit_kind.law_section added (vocab NOT widened)
  • No silent semantic mapping law_section → law_unit
  • No jsonb_set patch on payload pieces
  • No MARK/CUT alias body changed
  • No production_documents touched (table absent: 0)
  • No Qdrant touched (vsp = 152 unchanged)
  • No pg_cron install (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_verifiedcut_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.

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-iu-cut-dieu39-unit-kind-root-cause-and-contract-fix/00-summary.md