KB-2FF9

dot-iu-cutter v0.5 — Lifecycle Enactment Design · Live Lifecycle Survey (G1 PASS) (doc 1 of 6)

24 min read Revision 1
dot-iu-cutterv0.5lifecycle-enactment-designlive-lifecycle-surveyinformation-unitunit-versiongateway-config-driventac-uv-lifecycle-vocabenacted-at-column-already-existsfn-tac-enacted-immut-templatefn-iu-apply-edit-draft-global-couplingdieu442026-05-20

dot-iu-cutter v0.5 — Lifecycle Enactment Design · Live Lifecycle Survey

doc 1 of 6 · 2026-05-20 · READ-ONLY SURVEY — NO MUTATION

phase             : G1 — live lifecycle survey
outcome           : PASS — production lifecycle surface fully mapped
production_mutation : NONE (read-only queries via context_pack_readonly)

0. Survey scope

This document maps the LIVE production lifecycle surface for public.information_unit + public.unit_version as of 2026-05-20 06:40Z, in preparation for designing a canonical draft → enacted transition path for the 60 ICX-CONST IUs born in v0.5 leg-A CUT.

The survey is exhaustively read-only. All queries were executed under context_pack_readonly role with statement_timeout 5s and hard LIMIT 500. No SET, no DDL, no transactions written. The survey is reproducible from the SQL listed below.

1. Authoritative inventory — 60 ICX-CONST IUs uniformly draft

SELECT COUNT(*) AS n_icx_const,
       COUNT(*) FILTER (WHERE lifecycle_status='draft') AS n_draft,
       array_agg(DISTINCT lifecycle_status) AS statuses
FROM public.information_unit
WHERE canonical_address LIKE 'ICX-CONST%';
n_icx_const     : 60
n_draft         : 60
statuses        : ['draft']
non_draft_count : 0
SELECT lifecycle_status, COUNT(*) AS n,
       MIN(created_at) AS first_seen,
       MAX(updated_at) AS last_touched
FROM public.information_unit
GROUP BY lifecycle_status ORDER BY n DESC;
public.information_unit:
  lifecycle_status_uniform : 'draft'  (158 rows, 60 ICX-CONST + 98 pre-CUT pilots)
  first_seen               : 2026-05-05T07:07:26.107923Z
  last_touched             : 2026-05-20T04:18:21.854512Z

public.unit_version:
  lifecycle_status_uniform : 'draft'  (165 rows, 60 ICX-CONST v1 + 105 pre-CUT pilots)
  review_state             : NULL on all 165 rows (column exists, never populated)

No prior enactment has happened in this production database. Every row in both tables is in the column-default 'draft' state. The 60 ICX-CONST IUs are part of, but not all of, this draft population.

2. information_unit lifecycle surface

2.1 Column definitions (19 cols total)

lifecycle_status   : text  NOT NULL  DEFAULT 'draft'::text
conformance_status : text  NOT NULL  DEFAULT 'open'::text

No other lifecycle-named column exists on information_unit. No enacted_at, no effective_at, no published_at on this table.

2.2 Constraints (4 total, none on lifecycle_status)

information_unit_pkey                  : PRIMARY KEY (id)
information_unit_canonical_address_key : UNIQUE (canonical_address)
fk_iu_version_anchor                   : FOREIGN KEY (version_anchor_ref)
                                         REFERENCES unit_version(id)
                                         DEFERRABLE INITIALLY DEFERRED
trg_iu_birth_gate_layer2               : CONSTRAINT TRIGGER (deferred AFTER)

There is no CHECK constraint on information_unit.lifecycle_status. Any text value is currently DDL-legal; validation is policy-enforced inside functions only.

2.3 Triggers on information_unit (5 user triggers)

order trigger timing/event function sec.def
1 trg_aa_iu_gateway_write_guard BEFORE INSERT OR UPDATE public.fn_iu_gateway_write_guard() YES
2 trg_iu_birth_gate_layer1 BEFORE INSERT public.fn_iu_birth_gate_layer1() no
3 trg_iu_updated_at BEFORE UPDATE public.fn_iu_updated_at() no
4 trg_birth_information_unit AFTER INSERT public.fn_birth_registry_auto('__birth_synthetic_id__') no
5 trg_iu_birth_gate_layer2 AFTER INSERT OR UPDATE (DEFERRABLE) public.fn_iu_birth_gate_layer2() no

Critical implications for an UPDATE on information_unit:

  • The gateway guard (#1) fires on UPDATE just as it does on INSERT. Any UPDATE not carrying an allowed app.canonical_writer marker is REJECTED.
  • The birth-gate layer-1 (#2) is BEFORE INSERT only — does NOT re-run on UPDATE. So PILOT-ONLY warnings about P-pub1/P-pub2 (publication_authority_ref / publication_type_ref) do NOT block an UPDATE today.
  • trg_iu_updated_at rewrites updated_at = now() on every UPDATE — automatic.
  • trg_iu_birth_gate_layer2 fires AFTER UPDATE (deferred) and re-checks version_anchor_ref/content_anchor_ref consistency. As long as the UPDATE leaves these untouched, layer-2 passes.

2.4 Gateway policy mechanism (re-confirmed)

iu_create.gateway.mode                  : enforced
iu_create.gateway.marker_key            : app.canonical_writer
iu_create.gateway.marker_value          : fn_iu_create
iu_create.gateway.allowed_marker_values : fn_iu_create,fn_iu_apply_edit_draft
iu_create.gateway.direct_insert_policy  : block_after_guard
iu_create.gateway.exempt_policy         : none_active
iu_create.gateway.canonical_function    : public.fn_iu_create(text,text,text,text,text,text,text,text,uuid)
iu_create.gateway.plan_function         : public.fn_iu_create_plan(text,text,text,text,text,text,text,text,uuid)
iu_create.gateway.policy_doc_path       : knowledge/dev/laws/dieu44-trien-khai/design/22-p3-iu-creation-gateway-scope.md
iu_create.gateway.readme_path           : knowledge/dev/laws/dieu44-trien-khai/readme/iu-create-gateway-readme.md

The guard function body (md5 6907fa4e…26d7, 1364 chars) reads app.canonical_writer via current_setting(key, true) and accepts the write iff the value is one of the comma-separated entries in iu_create.gateway.allowed_marker_values. Adding a new canonical writer is a SINGLE-ROW dot_config update + a new SECDEF function that sets the marker — no code change to the guard.

2.5 Table-level ACL (per pg_class.relacl)

public.information_unit:
  directus              : arwdDxt   (owner; INS/SEL/UPD/DEL/TRUNC/REF/TRIGGER)
  context_pack_readonly : r         (SELECT only — survey role)
  cutter_exec           : r         (SELECT only — write must go through SECDEF fn)
  cutter_verify         : r         (SELECT only)

public.unit_version:
  directus              : arwdDxt
  context_pack_readonly : r
  cutter_exec           : r
  cutter_verify         : r

public.birth_registry:
  directus              : arwdDxt
  incomex               : r
  context_pack_readonly : r

cutter_exec (the cutter_agent role) has NO direct INSERT or UPDATE on either table. All writes already route through SECDEF functions. This is the same posture used by fn_iu_create (executed under directus inside the SECDEF body, called by cutter_exec from outside).

3. unit_version lifecycle surface

3.1 Column definitions (16 cols total)

lifecycle_status : text                    NOT NULL  DEFAULT 'draft'::text
enacted_at       : timestamp with time zone NULL     -- KEY: column EXISTS, never set
review_state     : text                    NULL      -- never populated (NULL on all 165 rows)
provenance       : text                    NULL
editor           : text                    NULL
updated_at       : timestamp with time zone NULL
content_profile  : jsonb                   NOT NULL  DEFAULT '{}'::jsonb

unit_version.enacted_at already exists as a nullable timestamp. This is strong design intent: when a version is enacted, its enacted_at must be set. Our fn_iu_enact design will write enacted_at = now() on the current version-anchor's UV row.

3.2 Triggers on unit_version (2 user triggers + 1 sandbox)

order trigger timing/event function sec.def
1 trg_aa_uv_gateway_write_guard BEFORE INSERT OR UPDATE public.fn_iu_gateway_write_guard() (SAME function as IU) YES
2 trg_aa_iu_notif_version AFTER INSERT public.fn_iu_notif_version() YES

sandbox_tac.unit_version has its own pre-insert trigger that is irrelevant to public.

The gateway also guards unit_version UPDATE. So a draft→enacted transition on UV must also be performed by a function that sets app.canonical_writer to an allowed value.

4. Existing IU-domain functions (24 total) — gap on lifecycle transitions

Filtered to public.fn_iu_*:

exist (canonical write path):
  fn_iu_create              SECDEF  (p_canonical_address, p_title, p_body, p_actor, p_unit_kind, p_section_type, p_owner_ref, p_publication_type, p_parent_ref)
  fn_iu_apply_edit_draft    SECDEF  (p_draft_id uuid, p_actor text, p_review_note text)
  fn_iu_create_plan         SECDEF  (...)
  fn_iu_create_edit_draft   SECDEF  (...)
  fn_iu_save                SECDEF  (...)
  fn_iu_edit / fn_iu_edit_plan
  fn_iu_comment / fn_iu_comment_edit_draft
  fn_iu_mark_read / fn_iu_unread / fn_iu_notification_board / fn_iu_notif_*

exist (helpers, no write):
  fn_iu_classify_existing   (p_addr text)
  fn_iu_create_preflight    ()
  fn_iu_resolve_default     (p_explicit, p_config_key, p_vocab_prefix)
  fn_iu_verify_invariants   (p_addr text) → jsonb

NOT FOUND  (full pg_proc scan):
  fn_iu_enact               — does NOT exist
  fn_iu_publish             — does NOT exist
  fn_iu_activate            — does NOT exist
  fn_iu_promote             — does NOT exist
  fn_iu_lifecycle           — does NOT exist
  fn_iu_lifecycle_transition — does NOT exist
  fn_iu_finalize            — does NOT exist

The lifecycle gap is unambiguous: there is NO canonical function that can transition an IU from draft to enacted today. Any direct UPDATE attempt is fail-closed by the gateway guard (because no allowed app.canonical_writer value covers an enactment transition).

5. Sibling lifecycle infrastructure in the database

The database already has FOUR parallel lifecycle subsystems. Each gives us a template to follow or a coupling to avoid.

5.1 LAW / NORMATIVE subsystem — {draft, enacted, retired} with content-immutability

Triggers exist:

table trigger function sec.def
approval_requests trg_apr_lifecycle fn_enforce_apr_lifecycle no
normative_registry nrm_enacted_must_have_approval fn_nrm_enacted_must_have_approval no
normative_registry nrm_enacted_must_have_enforcement fn_nrm_enacted_must_have_enforcement no
normative_registry nrm_enacted_immutable fn_nrm_enacted_immutable no
(a law table) (trg) fn_law_enacted_immutable no
(a law table) (trg) fn_law_enacted_must_have_enforcement no

fn_law_enacted_immutable body (verbatim) — TEMPLATE for our trg_iu_enacted_immut:

IF OLD.status = 'enacted' THEN
  IF NEW.status = 'retired' THEN RETURN NEW; END IF;
  IF (NEW.article_number, NEW.name, NEW.version, NEW.status,
      NEW.category, NEW.scope_summary, NEW.kb_path,
      NEW.enacted_session, NEW.council_score,
      NEW.composition_level, NEW.species_code)
     IS DISTINCT FROM
     (OLD.article_number, OLD.name, OLD.version, OLD.status, ...)
  THEN
    RAISE EXCEPTION 'Enacted law BẤT BIẾN. Tạo Amendment thay vì UPDATE. Hoặc retire trước.';
  END IF;
END IF;
RETURN NEW;

fn_nrm_enacted_immutable (verbatim, slightly stricter):

IF OLD.status = 'enacted' THEN
  IF NEW.status = 'retired' THEN RETURN NEW; END IF;
  IF NEW.status != OLD.status THEN
    RAISE EXCEPTION 'Enacted normative document is immutable. Only retire allowed.';
  END IF;
  IF NEW.name IS DISTINCT FROM OLD.name OR NEW.sections IS DISTINCT FROM OLD.sections
     OR NEW.version IS DISTINCT FROM OLD.version OR NEW.doc_type IS DISTINCT FROM OLD.doc_type THEN
    RAISE EXCEPTION 'Enacted document content is immutable. Create amendment instead.';
  END IF;
END IF;

5.2 TAC subsystem — {draft, enacted, superseded, retired} (4-state, SECDEF)

public.tac_uv_lifecycle_vocab rows (verbatim):

code name sort_order
draft Bản nháp 10
enacted Đã ban hành 20
superseded Bị thay 30
retired Đã rút 40

public.tac_lu_lifecycle_vocab rows:

code name sort_order
active Hoạt động 10
draft_only Chỉ bản nháp 20
retired Đã rút 30

Trigger fired on public.tac_unit_version:

trigger : trg_tac_enacted_immut  (sec.def YES)
function: public.fn_tac_enacted_immut

fn_tac_enacted_immut body (verbatim) — the BEST TEMPLATE we have, because it operates on UV-shape rows (body/title/description/content_profile):

IF OLD.lifecycle_status = 'enacted' AND TG_OP = 'UPDATE' THEN
    IF (NEW.body IS DISTINCT FROM OLD.body)
    OR (NEW.title IS DISTINCT FROM OLD.title)
    OR (NEW.description IS DISTINCT FROM OLD.description)
    OR (NEW.content_profile IS DISTINCT FROM OLD.content_profile)
    THEN
        RAISE EXCEPTION 'INV-ENACTED-IMMUT: cannot modify body/title/description/content_profile of enacted unit_version %', OLD.id
            USING ERRCODE = 'check_violation';
    END IF;
END IF;
IF TG_OP = 'DELETE' AND OLD.lifecycle_status = 'enacted' THEN
    RAISE EXCEPTION 'INV-ENACTED-IMMUT: cannot delete enacted unit_version %', OLD.id
        USING ERRCODE = 'check_violation';
END IF;
RETURN COALESCE(NEW, OLD);

5.3 Generic transition subsystem — fn_transition_lifecycle(p_collection, p_entity_id, ...)

Exists in public. Signature:

public.fn_transition_lifecycle(
  p_collection   text,
  p_entity_ids   integer[],
  p_new_status   text,
  p_reason       text,
  p_performed_by text
) RETURNS jsonb

Hard-coded vocab: {draft, active, deprecated, retired} (NOT our 4-state vocab; uses active not enacted).

Writes to public.lifecycle_log which has integer entity_id — INCOMPATIBLE with our UUID-keyed information_unit. Cannot be reused.

5.4 public.lifecycle_log schema (16 cols)

id                        : integer   NOT NULL  serial
entity_collection         : varchar   NOT NULL
entity_id                 : integer   NOT NULL  -- ⚠ INTEGER, breaks for UUID-keyed IU
entity_code               : varchar   NULL
from_status               : varchar   NOT NULL
to_status                 : varchar   NOT NULL
terminal_reason           : varchar   NULL   DEFAULT 'none'
transition_type           : varchar   NULL
reason                    : text      NULL
performed_by              : varchar   NOT NULL
performed_at              : timestamptz NULL DEFAULT now()
related_entity_collection : varchar   NULL
related_entity_id         : integer   NULL    -- ⚠ INTEGER
related_entity_code       : varchar   NULL
approval_ref              : text      NULL
metadata                  : jsonb     NULL    DEFAULT '{}'::jsonb

Conclusion: a new UUID-keyed lifecycle log table is required for IU. Reusing lifecycle_log would either require an ALTER (high blast radius) or an ID-mapping hack (technical debt).

6. Function bodies relevant to the design — verbatim selections

6.1 fn_iu_apply_edit_draft — global-DB lifecycle coupling (CRITICAL)

This function contains the following sensitive block (verbatim):

SELECT count(DISTINCT lifecycle_status), min(lifecycle_status)
  INTO v_lc_count, v_uv_lifecycle
  FROM public.unit_version;
IF v_lc_count != 1 OR v_uv_lifecycle IS NULL OR btrim(v_uv_lifecycle)='' THEN
  RETURN jsonb_build_object('status','lifecycle_ambiguous',
    'guidance','Not uniquely determined.',
    'next_action','stop_for_gpt_review');
END IF;

This is a GLOBAL count across ALL rows in public.unit_version. The function uses it to decide which lifecycle_status to stamp on a newly-applied edit-draft version. The current behavior: every UV in the database has lifecycle_status='draft', so v_lc_count = 1, the check passes, and new versions inherit 'draft'.

AFTER enacting the 60 ICX-CONST UVs, v_lc_count will become 2 ('draft' for 105 rows + 'enacted' for 60 rows). Every future call to fn_iu_apply_edit_draft will fail with 'lifecycle_ambiguous', irrespective of which IU is being edited. This is a HARD BREAKAGE of all in-place edit flows.

This coupling MUST be addressed before enactment proceeds. It is surfaced as OQ-1 in [[dot-iu-cutter-v0-5-03-design-options-analysis-2026-05-20]] and folded into the recommended design in [[dot-iu-cutter-v0-5-04-recommended-lifecycle-enactment-contract-2026-05-20]].

6.2 fn_iu_create — canonical writer marker pattern (verbatim relevant block)

PERFORM set_config('app.canonical_writer', 'fn_iu_create', true);
v_iu_id := gen_random_uuid();
v_uv_id := gen_random_uuid();
v_hash  := public.fn_content_hash(p_body);
INSERT INTO public.information_unit(...) VALUES(...);
INSERT INTO public.unit_version(id, unit_id, body, content_hash, version_seq, created_by)
  VALUES (v_uv_id, v_iu_id, p_body, v_hash, 1, btrim(p_actor));
UPDATE public.information_unit
   SET version_anchor_ref = v_uv_id,
       content_anchor_ref = v_uv_id::text
 WHERE id = v_iu_id;

set_config(key, value, true) — the third arg true makes the setting transaction-local and auto-cleared at COMMIT/ROLLBACK. Our fn_iu_enact will use the same pattern with set_config('app.canonical_writer','fn_iu_enact',true).

6.3 fn_iu_verify_invariants — re-usable precondition probe

Returns a jsonb with five invariants:

i1_iu_exists       : IU row exists at canonical_address
i2_uv_linked       : unit_version row is linked (version_anchor_ref resolves)
i3_anchors_exact   : version_anchor_ref::text == content_anchor_ref
                     AND unit_version.unit_id == iu.id
i4_birth_exists    : birth_registry has 'information_unit::<iu_id>' entry
i5_uv_birth_ok     : per collection_registry.birth_code_strategy
all_pass = (i1 ∧ i2 ∧ i3 ∧ i4 ∧ i5)

Used inside fn_iu_create (post-write) and fn_iu_apply_edit_draft. Reusable as a precondition probe inside fn_iu_enact — invariants are a necessary condition for enactment (if any of i1..i5 fail, the IU is structurally broken and enactment must NOT proceed).

7. Governance / audit linkage state (cutter_governance schema)

The cutter_governance schema exists with 24 tables (confirmed via pg_class join pg_namespace):

cutter_governance schema (24 tables):
  cut_change_set                         -- leg-B governed recording
  cut_change_set_affected_row
  review_decision                        -- the GPT/User ruling rows
  manifest_envelope
  manifest_unit_block
  dot_pair_signature                     -- DOT-991 / DOT-992 signing
  verify_result                          -- M2 write-VERIFY
  decision_backlog_entry / _history / _dependency / _sweep_log
  source_document_registry / source_document_version_registry / source_family_registry
  address_template_registry / authority_override / canonical_address_alias
  entity_kind_registry / entity_reference_registry
  grammar_profile / grammar_profile_level / grammar_profile_status_marker
  matcher_config_registry / metadata_key_registry

context_pack_readonly does NOT have SELECT on these tables (relacl probe returned can_select=false for cut_change_set, dot_pair_signature, manifest_envelope, review_decision, verify_result). The design must take their existence and shape as documented (from prior memory + KB), not as live-queried, for this scope.

Linked governance rows currently live for the 60-IU CUT (from MEMORY closeouts):

linked_change_set_id        : 456c6830-a747-4b53-ac2f-665e25e12cd0
linked_manifest_envelope_id : 638cf363-f45a-4bb3-b9bb-928c5e24c15b
linked_review_decision_id   : 29c88a7b-60f7-41bd-af45-43cc9b9f41c0
linked_executor_signature_id: 3a249063-e33a-406a-9302-2e9e646a0938
verify_result_id            : 18278460-438c-4fb4-bf9c-997c82447f92
verifier_signature_id       : f5c3ee34-7f9f-4af3-879d-1bdcf5508a8f

These IDs are inputs the enactment design must accept (as foreign references) but does not own.

8. Survey summary — design constraints distilled

hard constraints (must hold in any design):
  C-1: writes to public.information_unit and public.unit_version go through
       fn_iu_gateway_write_guard; only canonical writers in
       iu_create.gateway.allowed_marker_values are permitted.
  C-2: cutter_exec role has SELECT-only on both tables.
  C-3: information_unit.lifecycle_status has NO CHECK constraint;
       semantic vocab is policy-enforced inside functions only.
  C-4: unit_version.enacted_at already exists (nullable timestamptz)
       — design must populate it on enactment.
  C-5: fn_iu_apply_edit_draft contains a GLOBAL "uniform lifecycle_status
       across all UVs" check that breaks the instant any UV transitions
       out of 'draft'. Either patch this function or sequence enactment
       after that path is hardened (see OQ-1).
  C-6: trg_iu_birth_gate_layer1 is BEFORE INSERT only — UPDATE-only
       enactment will not re-trigger layer-1 P-pub1/P-pub2 warnings.
  C-7: trg_iu_birth_gate_layer2 is AFTER INSERT OR UPDATE (DEFERRED);
       enactment UPDATE must leave version_anchor_ref/content_anchor_ref
       untouched to avoid re-checks.
  C-8: existing public.lifecycle_log is INTEGER-keyed, incompatible
       with UUID-keyed IU; a new iu_lifecycle_log table is required
       OR audit must be carried inside cutter_governance.decision_backlog_*.

soft constraints (design should respect):
  S-1: Adopt the tac_uv_lifecycle_vocab 4-state vocab
       {draft, enacted, superseded, retired} — already canonical in TAC.
  S-2: Mirror fn_tac_enacted_immut pattern for enacted-IU immutability.
  S-3: Keep audit linkage to cutter_governance (review_decision_id,
       cut_change_set_id) as soft FKs in the new log table — full FK
       is impossible cross-schema while context_pack_readonly cannot
       SELECT cutter_governance.
  S-4: Detector / L3 monitoring is deferred per Pack 22 closure;
       speed-bump gateway is the production posture.

forbidden in any design:
  F-1: No DROP TRIGGER trg_aa_iu_gateway_write_guard (would re-open
       direct-INSERT/UPDATE).
  F-2: No widening of iu_create.gateway.allowed_marker_values beyond
       the strict minimum (must be specific function name).
  F-3: No exempt_policy = 'constitution_first_enact' OR similar
       one-shot UPDATE (explicitly rejected by Pack 22-P3 doctrine
       and by the prior assessment as OPT-E3 NOT_RECOMMENDED).
  F-4: No direct UPDATE on production rows from any role.

9. G1 disposition

G1_live_lifecycle_survey : PASS
production_mutation       : NONE
next                      : G2 — existing docs/code discovery review
                            (see [[dot-iu-cutter-v0-5-02-existing-lifecycle-docs-code-review-2026-05-20]])

Related KB documents:

  • [[dot-iu-cutter-v0-5-02-existing-lifecycle-docs-code-review-2026-05-20]] — discover phase
  • [[dot-iu-cutter-v0-5-03-design-options-analysis-2026-05-20]] — OPT-E1/E2/E3 comparison
  • [[dot-iu-cutter-v0-5-04-recommended-lifecycle-enactment-contract-2026-05-20]] — function contract
  • [[dot-iu-cutter-v0-5-05-grant-verification-rollback-plan-2026-05-20]] — operations
  • [[dot-iu-cutter-v0-5-06-final-lifecycle-design-report-2026-05-20]] — final
  • Prior [[dot-iu-cutter-v0-5-04-lifecycle-enactment-assessment-2026-05-20]] (in v0.5-post-cut-verify-governed-recording-release-readiness/)
Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.5-lifecycle-enactment-design/dot-iu-cutter-v0.5-01-live-lifecycle-survey-2026-05-20.md