07 Next Pack — State-Machine Rollback Policy after mark_verified
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)
- Add legal transition edges in
fn_cut_request_transition:mark_verified → mark_rejected— requiresp_metadata.rollback_reasonANDp_metadata.rollback_doc_id(operator audit trail).mark_rejected → copied— already exists indirectly viamark_rejected → mark_in_progress → marked, but a directmark_rejected → copiedmay simplify the recovery flow (decision deferred to design phase).
- 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_verifiedANDcut_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 toapproved → rejectedfor aniu_staging_record. - Inserts a
dot_iu_command_runrow withcommand_name='dot_cut_rollback_after_approval'.
- Validates current status is
- Forbidden in this pack:
- No data deletion (preserve audit trail).
- No DROP of any function.
- No alias contract change.
- No vocab change.
- Dry-run probe on Điều 39: synthesize the rollback in BEGIN/ROLLBACK, prove that on COMMIT it would leave cut_request status=
mark_rejectedand manifest lifecycle=rejectedwith 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_progressbecause IU rows may already exist (would orphan them). Hard-blockcut_in_progress → mark_rejected; require manual SQL with explicit forensic doc. - OBSERVATION:
iu_staging_recordhasconsumed_by_run_id/consumed_at/approval_doc_idfields — preserve them in metadata for audit even when lifecycle moves torejected.
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:
- Re-MARK with corrected
unit_kind=law_unit× 16. - VERIFY_MARK with new preflight gate → expect
cut_readiness_ok=true. - CUT dry-run (
fn_iu_cut_from_manifest(p_apply=false)). - CUT apply.
- VERIFY_CUT.
- COMPLETE.
- 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).