KB-5724

Next CUT Step + Carry-forward — state-machine rollback policy needed (2026-05-27)

7 min read Revision 1
iu-cutcarry-forwardstate-machine-rollbacknext-macrocf-1-high2026-05-27

07 — Next CUT Step / Carry-forward

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.

Name (proposed): IU_CUT_STATE_MACHINE_ROLLBACK_AFTER_APPROVAL_POLICY

Scope:

  1. Policy decision (operator-driven): Should mark_verified -> mark_rejected be a legal transition? Options:

    • (a) Yes — single-cut_request reuse. Append 'mark_rejected' to the WHEN 'mark_verified' legal list in fn_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_archived as a terminal endpoint for "approved but voided pre-CUT" manifests. Cleaner forensics. More state-machine surface area.
  2. 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).

  3. Apply to Điều 39 specifically:

    • Reject manifest 9fa4685e-... with audit metadata (root cause: pre-unit_kind-gate manifest).
    • Transition 146f1520-... from mark_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=true for separate CUT macro.

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_mark analog)
  • 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.mark signal enqueued via fn_cut_request_signal (gate-respecting; will skip if queue.job_substrate.enabled=false)
  • event_outbox count 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, established cut_manifest_piece_schema_v1 as 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
Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-iu-cut-dieu39-unit-kind-root-cause-and-contract-fix/07-next-cut-step.md