KB-16F0

07 Next Pack — State-Machine Rollback Policy after mark_verified

5 min read Revision 1
dieu44rollbackstate-machinenext-packdieu392026-05-27

07 — Next Pack: State-Machine Rollback Policy after mark_verified

Why this pack

After mig 055, the forward gate is sound: future MARKs with invalid unit_kind (or any of E1..E7 violations) cannot reach mark_verified / approved. But the stuck Điều 39 request — cut_request 146f1520 at mark_verified with manifest 9fa4685e at lifecycle=approved — has no legal rollback edge in fn_cut_request_transition:

mark_verified → cut_in_progress         (only legal next move)
mark_verified → mark_rejected          NOT ALLOWED
mark_verified → mark_in_progress       NOT ALLOWED
approved manifest → rejected manifest   NOT ALLOWED via verify_mark (lifecycle precondition)

Even worse, iu_staging_record_approved_consistency_chk enforces:

(lifecycle_status = 'approved') = (approved_at IS NOT NULL AND approved_by IS NOT NULL)
OR lifecycle_status IN ('consumed','rejected','expired','cleaned')

So moving manifest 9fa4685e approved → rejected is constraint-compatible, but fn_iu_verify_mark rejects the call due to the pending_review precondition.

Proposed pack: IU_CUT_STATE_MACHINE_ROLLBACK_AFTER_APPROVAL_POLICY

Scope (DRY-RUN FIRST)

  1. Add legal transition edges in fn_cut_request_transition:
    • mark_verified → mark_rejected — requires p_metadata.rollback_reason AND p_metadata.rollback_doc_id (operator audit trail).
    • mark_rejected → copied — already exists indirectly via mark_rejected → mark_in_progress → marked, but a direct mark_rejected → copied may simplify the recovery flow (decision deferred to design phase).
  2. Add SECURITY DEFINER wrapper fn_cut_request_rollback_after_approval(p_cut_request_id, p_actor, p_rollback_reason, p_rollback_doc_id):
    • Validates current status is mark_verified AND cut_run_id IS NULL (no CUT has run).
    • Calls fn_cut_request_transition(..., 'mark_rejected', ..., {rollback_reason, rollback_doc_id, rolled_back_at}).
    • Calls fn_iu_staging_record_rollback(manifest_staging_record_id, 'rejected', actor) (NEW helper) which is the only path to approved → rejected for an iu_staging_record.
    • Inserts a dot_iu_command_run row with command_name='dot_cut_rollback_after_approval'.
  3. Forbidden in this pack:
    • No data deletion (preserve audit trail).
    • No DROP of any function.
    • No alias contract change.
    • No vocab change.
  4. Dry-run probe on Điều 39: synthesize the rollback in BEGIN/ROLLBACK, prove that on COMMIT it would leave cut_request status=mark_rejected and manifest lifecycle=rejected with all audit fields pinned.

Risk classification

  • HIGH RISK: bypassing approval requires extra guards (operator approval doc, dry-run preview by default).
  • CONCERN: rollback should never be possible after cut_in_progress because IU rows may already exist (would orphan them). Hard-block cut_in_progress → mark_rejected; require manual SQL with explicit forensic doc.
  • OBSERVATION: iu_staging_record has consumed_by_run_id/consumed_at/approval_doc_id fields — preserve them in metadata for audit even when lifecycle moves to rejected.

Sibling next pack: IU_CUT_DIEU39_CUT_DRY_RUN_FIRST

Once Điều 39 is unblocked (either via rollback policy above or by accepting the request and rebuilding from scratch), the next operational step is:

  1. Re-MARK with corrected unit_kind=law_unit × 16.
  2. VERIFY_MARK with new preflight gate → expect cut_readiness_ok=true.
  3. CUT dry-run (fn_iu_cut_from_manifest(p_apply=false)).
  4. CUT apply.
  5. VERIFY_CUT.
  6. COMPLETE.
  7. CLEANUP scheduled.

Composer gate must be flipped on for the CUT step and flipped back off after VERIFY_CUT.

Why this pack is deliberately separated from mig 055

mig 055 is a forward-only contract tightening — safe and reversible (DROP fn_iu_cut_preflight_validate + revert fn_iu_verify_mark body). Adding state-machine rollback edges is a policy change with operator-process implications (who can call rollback, with what evidence). Keeping it as its own pack respects the "small, surgical, single-responsibility migration" pattern.

Rollback policy (for mig 055 itself, not for cut_requests)

If we need to revert mig 055:

BEGIN;
DROP FUNCTION public.fn_iu_cut_preflight_validate(uuid, text, text);
CREATE OR REPLACE FUNCTION public.fn_iu_verify_mark(...) ... -- restore mig 054 body
COMMIT;

The pre-mig fn_iu_verify_mark body is captured at /tmp/fn_iu_verify_mark.sql on the dev workstation and in pg_dump /tmp/pg_dump_pre_mig055_20260527T043033Z.dump (md5 1ddadb0b6b0916b5d7784b9eb737c1fd).

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-iu-cut-verify-mark-cut-readiness-gate/07-next-state-machine-rollback-policy.md