KB-4A3D

IU Core 8000x — Compensation Primitives Design (fn_iu_supersede + fn_iu_retire)

7 min read Revision 1
iu-corev0.68000xcompensation-primitivesfn-iu-supersedefn-iu-retiremigration-026fsmreversibilitysandbox-250

IU Core 8000x — Compensation Primitives Design Note

Why this matters

Constitution rule: reversible by default. Every irreversible action must be paired with a documented compensation primitive that can put the system back to a known-good state without pg_dump restore.

public.fn_iu_enact (M3a-2026-05-20) advances draft → enacted and writes to three tables in one transaction: information_unit, unit_version, iu_lifecycle_log. The FSM defined inside fn_iu_enact recognises three additional transitions but its body returns 'transition_not_yet_implemented':

enacted    → superseded   (transition_type='supersede')
enacted    → retired      (transition_type='retire_from_enacted')
draft      → retired      (transition_type='retire_from_draft')
superseded → retired      (transition_type='retire_from_superseded')

Without these, every fn_iu_enact call is a one-way door: the only way back is a pg_dump restore, which loses everything else committed since.

What the 8000x primitives provide

Primitive Signature What it does Why safe
fn_iu_supersede(canonical, actor, rd_id, cs_id?, reason?, tool_rev?, superseded_by_canonical?, dry_run) (text,text,uuid,uuid,text,text,text,boolean) → jsonb enacted → superseded for one IU; logs transition_type='supersede' with optional successor pointer Same SECURITY DEFINER pattern as fn_iu_enact, refuses NULL rd_id, probes cutter_governance.review_decision FK, sets app.canonical_writer='fn_iu_supersede' so fn_iu_enacted_immut trigger permits the body update, post-write read-back assertion
fn_iu_retire(canonical, actor, rd_id, cs_id?, reason?, tool_rev?, dry_run) (text,text,uuid,uuid,text,text,boolean) → jsonb `{draft enacted

Safety pattern (mirrored from fn_iu_enact)

  1. Input validation → returns {status: invalid_input} on missing canonical_address / actor / review_decision_id.
  2. SELECT … FROM public.information_unit … FOR UPDATE (row-level lock).
  3. FSM legality check vs iu_lifecycle_vocab (implicit via transition_type CASE).
  4. cutter_governance.review_decision FK probe — returns {status: review_decision_not_found} if missing.
  5. Optional cutter_governance.cut_change_set FK probe.
  6. Dry-run early return with {status: plan_ok, would_write_rows:{…}}.
  7. pg_advisory_xact_lock(hashtext('iu_supersede:'||iu_id)) / 'iu_retire:'||iu_id.
  8. set_config('app.canonical_writer', '<fn_name>', true) so the fn_iu_enacted_immut trigger permits the body update.
  9. Dual-table UPDATE (information_unit + unit_version).
  10. INSERT into iu_lifecycle_log with transition_type + metadata {warnings, app_canonical_writer, compensation_function_version}.
  11. Post-write read-back assertion (defence-in-depth — RAISE on mismatch).
  12. Return {status: superseded|retired, iu_id, log_id, …}.

What it does NOT do

  • No automatic FSM "unretire" path — retirement is itself one-way. The compensation primitives close the original gap (no compensation) but do NOT introduce infinite reversibility. If both fn_iu_enact and fn_iu_retire have been applied wrongly, the only rollback is full pg_dump restore.
  • No bulk variant — one IU per call so that any single failure rolls only that IU; the package files iterate inside their own DO block so the wrapping transaction fails atomically on the first error.
  • No gate gating — the primitives are inert by construction (refuse without sovereign-authored review_decision_id). No new dot_config row is needed.

Test coverage

  • tests/test_iu_core_8000x_compensation_primitives.py (25 TestCases, all PASS):
    • migration file shape: ON_ERROR_STOP, BEGIN/COMMIT
    • both functions SECURITY DEFINER, pinned search_path
    • both refuse NULL review_decision_id
    • both probe cutter_governance.review_decision FK
    • supersede only allows from_enacted
    • retire allows draft / enacted / superseded with correct transition_type
    • both set the app.canonical_writer marker
    • both INSERT iu_lifecycle_log
    • both have post-write read-back assertions
    • dry-run path exists for both
    • no DML outside function bodies
    • both functions documented via COMMENT ON FUNCTION
    • rollback drops both with matching signatures
    • sandbox/250 covers every refusal branch
    • DOT scan registers both new function names + bumps function count to 54

Sandbox coverage

sql/iu-core/sandbox/250_compensation_primitives_probe.sql:

Step Probe
250.1 fn_iu_supersede('ICX-CONST/x', 'sandbox/250', NULL)status='invalid_input'
250.2 fn_iu_supersede('no/such/canonical', actor, rd_uuid)status='iu_not_found'
250.3 fn_iu_supersede(<draft canonical>, actor, rd_uuid)status='fsm_denied'
250.4 fn_iu_supersede(<enacted canonical>, actor, fake_rd_uuid, dry_run=true)status='review_decision_not_found'
250.5 fn_iu_retire('ICX-CONST/x', 'sandbox/250', NULL)status='invalid_input'
250.6 fn_iu_retire('no/such/canonical', actor, rd_uuid)status='iu_not_found'
250.7 fn_iu_retire(<retired canonical>, actor, fake_rd_uuid)status='already_retired' (skipped if no retired corpus)
250.8 SELECT count(*) … pg_proc WHERE proname IN ('fn_iu_supersede','fn_iu_retire') = 2

Whole script wraps BEGIN; … ROLLBACK;. Nothing committed.

Application order

  1. psql -d directus -f sql/iu-core/026_compensation_primitives.sql (migration)
  2. psql -d directus -f sql/iu-core/sandbox/250_compensation_primitives_probe.sql (sandbox proof)
  3. psql -d directus -f sql/iu-core/runtime/110_iu_core_dot_conformance_scan.sql (DOT conformance — D9 row for function must report 54/54, ok=true)
  4. Then proceed to ops/governance-promotion-package-8000x/ for the actual DIEU promotion.

Rollback

psql -d directus -f sql/iu-core/rollback/026_compensation_primitives.rollback.sql

Safe at any time:

  • Both functions are NOT called by triggers or generated columns.
  • iu_lifecycle_log rows written by them remain in place (metadata-only retention).
  • DOT scan after rollback will report function count = 52 (back to 144 total) — re-running the macro re-applies migration 026 without loss.
Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-iu-core-8000x-dependency-closure-promotion-qdrant-open-goal/04_compensation_primitives_design.md