Next CUT Step + Carry-forward — state-machine rollback policy needed (2026-05-27)
07 — Next CUT Step / Carry-forward
Why CUT is NOT recommended now
This macro returned PARTIAL_WITH_EXACT_GAP. The Điều 39 cut_request 146f1520-... is at mark_verified with an approved manifest whose 16/16 pieces declare unit_kind=law_section — a value that fn_iu_create will refuse. Running CUT now would fail the same way it did before (status rolls back to mark_verified, cut_run_id=NULL, 0 IUs created).
The only way to proceed cleanly is to re-MARK with corrected unit_kind=law_unit, but that requires reaching mark_rejected first, which the current state machine does not allow from mark_verified.
Recommended next macro
Name (proposed): IU_CUT_STATE_MACHINE_ROLLBACK_AFTER_APPROVAL_POLICY
Scope:
-
Policy decision (operator-driven): Should
mark_verified -> mark_rejectedbe a legal transition? Options:- (a) Yes — single-cut_request reuse. Append
'mark_rejected'to theWHEN 'mark_verified'legal list infn_cut_request_transition. Add metadata audit fields (rollback_reason,rollback_actor,prior_manifest_digest). - (b) No — open a new cut_request for re-MARK. Mark old one as
cut_failed(terminal) with audit. Requires either adding a transition path or a one-off terminal flag. - (c) Hybrid — introduce a new status
mark_archivedas a terminal endpoint for "approved but voided pre-CUT" manifests. Cleaner forensics. More state-machine surface area.
- (a) Yes — single-cut_request reuse. Append
-
Implement chosen policy as a single-TX CREATE OR REPLACE of
fn_cut_request_transition(matches Phase 3B governance pattern: changing a state-machine fn is one of the few legitimate fn body edits outside aliases). -
Apply to Điều 39 specifically:
- Reject manifest
9fa4685e-...with audit metadata (root cause: pre-unit_kind-gate manifest). - Transition
146f1520-...frommark_verified->mark_rejected(or chosen terminal). - If (a) or (c): re-MARK with corrected pieces (unit_kind=law_unit), VERIFY_MARK dry-run + apply.
- STOP at
ready_for_cut=truefor separate CUT macro.
- Reject manifest
Then-and-only-then CUT
A separate macro IU_CUT_DIEU39_CUT_DRY_RUN_FIRST can run CUT on the corrected manifest with:
fn_iu_op_cut(staging_record_id, p_apply:=false, actor:=..., open_composer:=false)for dry-run preview- inspect
inner_result.refusal_code,inner_result.pieces_count, etc. - only if dry-run says clean, run
fn_iu_op_cut(..., p_apply:=true, open_composer:=true) - run VERIFY_CUT (
fn_cut_verify_markanalog) - run COMPLETE
- run cleanup_scheduled
That CUT macro should also verify (CF below).
Carry-forward gaps (in priority order)
CF-1 (HIGH) — State machine has no rollback path from mark_verified
fn_cut_request_transition:
WHEN 'mark_verified' THEN ARRAY['cut_in_progress'] -- no rollback
A cut_request that reaches mark_verified and discovers a CUT-rejected contract gap has no clean way back to re-MARK on the same cut_request_id. This blocks Điều 39 from being fixed without macro-level scope expansion.
Action: see "Recommended next macro" above.
CF-2 (HIGH) — VERIFY_MARK Axis D is reactive, not preventive at MARK store time
The pre-fix manifest 9fa4685e-... reached lifecycle_status='approved' with bad unit_kind because Axis D did not exist. With this fix, future manifests cannot reach approved with bad unit_kind. But the already-approved manifest 9fa4685e-... is grandfathered: the new gates only fire on new MARK/VERIFY_MARK calls. The data-side cleanup must happen via section 05 spec.
Action: part of CF-1 follow-on macro.
CF-3 (MEDIUM) — information_unit.unit_kind has no FK to tac_unit_kind_vocab
Unlike section_type (which has both dot_config vocab.section_type.* AND tac_section_type_vocab FK table), unit_kind has only dot_config. A direct INSERT into information_unit bypassing fn_iu_create could write any string. The contract is enforced ONLY by fn-body checks.
Action: consider creating tac_unit_kind_vocab (FK target) in a separate governance-vocab macro, syncing with dot_config vocab.unit_kind.* (analog to fn_iu_section_type_vocab_sync_check).
CF-4 (MEDIUM) — fn_cut_request_signal and event_outbox not exercised in this macro
This macro only modified validation logic; it did not exercise the signal/outbox path. The next macro that re-MARKs should confirm:
cut.marksignal enqueued viafn_cut_request_signal(gate-respecting; will skip ifqueue.job_substrate.enabled=false)event_outboxcount delta matches expected
Action: part of CF-1 follow-on.
CF-5 (LOW) — Semantic-label ergonomics deferred
If future MARK callers (Codex/agents/operators) naturally emit semantic labels like law_section, policy_clause, runbook_step and want to keep them in the manifest forensically, a dot_config mapping.unit_kind.<sem> namespace plus fn_cut_resolve_unit_kind(text) helper is the recommended design. Explicitly deferred for now (strict-refusal chosen).
Action: evaluate after observing real-world caller patterns post-fix.
CF-6 (LOW) — iu_create.default_unit_kind interaction with Axis D
fn_iu_resolve_default has a fallback mode (status default) when input is NULL. The new Axis D treats unit_kind NULL as a hard problem. This is slightly stricter than fn_iu_create itself (which allows defaults). If any pipeline relies on NULL-to-default fallback, Axis D will now block it. No known caller does this for cut manifests (every observed manifest has explicit unit_kind on every piece), but worth noting.
Action: monitor; may need to relax Axis D if a legitimate NULL-default flow is discovered.
Heartbeat note
queue.heartbeat.enabled=true at exit. The cut_pipeline_operator heartbeat (per prior macros) is wired to fn_cut_heartbeat_ping — this macro did not touch heartbeats. The next macro that re-runs the pipeline should tick the heartbeat from the external operator process before exit.
Cross-references
- Parent code change (most recent):
IU_CUT_DIEU39_VERIFY_MARK_ROOT_CAUSE_AND_CONTRACT_FIX_PASS_2026-05-27(mig 054) — same source_ref Điều 39, establishedcut_manifest_piece_schema_v1as 6-field. - This macro: widened that schema to 7 fields + added VERIFY_MARK Axis D.
- Next macro (proposed):
IU_CUT_STATE_MACHINE_ROLLBACK_AFTER_APPROVAL_POLICY— close CF-1, then re-MARK Điều 39. - Then:
IU_CUT_DIEU39_CUT_DRY_RUN_FIRST— finally run CUT on corrected manifest.
Return code
IU_CUT_DIEU39_UNIT_KIND_ROOT_CAUSE_AND_CONTRACT_FIX_PARTIAL_WITH_EXACT_GAP
gap: state_machine_lacks_mark_verified_to_mark_rejected_rollback_path
gap_blocks: dieu39_rerun_mark_verify_approval
fix_committed_code_side: true
regression_proven: true
forbiddens_honored: 15/15