KB-68F8

dot-iu-cutter v0.5 — Canonical Path Survey · fn_iu_create Contract Survey (S2 PASS · rollback-only test confirms zero persistence) (doc 2 of 7)

11 min read Revision 1
dot-iu-cutterv0.5fn-iu-create-canonical-path-survey-and-redesignfn-iu-create-contract-surveysecurity-definerrollback-only-test-passedwrites-iu-uv-anchorwriter-digest-equivalencelifecycle-status-draft-gapdieu442026-05-20

dot-iu-cutter v0.5 — Canonical Path Survey · fn_iu_create Contract Survey

doc 2 of 7 · 2026-05-20 · read-only survey + ROLLBACK-only test

phase             : S2 — survey canonical fn_iu_create + companions
outcome           : PASS — contract fully mapped + live ROLLBACK-only test
production_mutation : NONE (test ran inside BEGIN/ROLLBACK; counts verified
                      byte-identical post-rollback)

1. Signature

public.fn_iu_create(
  p_canonical_address text,
  p_title             text,
  p_body              text,
  p_actor             text,
  p_unit_kind         text DEFAULT NULL,   -- auto-resolve from dot_config
  p_section_type      text DEFAULT NULL,   -- auto-resolve from dot_config
  p_owner_ref         text DEFAULT NULL,   -- default = p_actor
  p_publication_type  text DEFAULT NULL,   -- optional, explicit only
  p_parent_ref        uuid DEFAULT NULL    -- optional parent/container
) RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, public
owner               : directus
security_definer    : YES (runs as directus inside the function body)
volatility          : VOLATILE
md5(pg_get_functiondef): dcade99af1ef096892748c9f14082e11
body_length         : 5871 chars

2. Internal behaviour (read full body)

In order :

0. validate inputs (canonical_address/title/body/actor required, non-empty)
1. fn_iu_create_preflight()  → check schema columns, FK deferrability, all
                                three IU triggers (gateway, L1, L2) present
2. unique-canonical_address guard discovery (exactly one such constraint)
3. fn_iu_resolve_default     → unit_kind, section_type (auto from
                                iu_create.default_unit_kind etc.); publication_type
                                (explicit only)
4. owner := COALESCE(p_owner_ref, p_actor)
5. pg_advisory_xact_lock(hashtext(canonical_address))     -- race-safety
6. fn_iu_classify_existing(canonical_address)
     - 'not_found'         → proceed
     - 'exists_complete'   → idempotent return (NO new row)
     - 'exists_missing_*' / 'exists_anchor_invalid' / 'exists_duplicate_*' /
       'exists_unknown_state' → health/remediation (NOT success ; caller
       must NOT treat as success)
7. IF FK not initially deferred → SET CONSTRAINTS <fk> DEFERRED
8. PERFORM set_config('app.canonical_writer', 'fn_iu_create', true)
     -- txn-local marker; gateway trigger lets the writes through
9. v_iu_id := gen_random_uuid() ; v_uv_id := gen_random_uuid()
   v_hash  := fn_content_hash(p_body)                -- sha256 hex of body
10. INSERT INTO public.information_unit
      (id, canonical_address, unit_kind, owner_ref, created_by, updated_by,
       identity_profile, parent_or_container_ref)
   VALUES (v_iu_id, btrim(p_canonical_address), v_unit_kind, v_owner,
           btrim(p_actor), btrim(p_actor),
           {"title": btrim(p_title),
            "owner_lookup_ref": v_owner,
            "primary_section_type_ref": v_section_type[,
            "publication_type_ref": btrim(p_publication_type)]},
           p_parent_ref)
   -- unique_violation → re-classify_existing → idempotent return
11. 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))
12. UPDATE public.information_unit
      SET version_anchor_ref = v_uv_id,
          content_anchor_ref = v_uv_id::text
   WHERE id = v_iu_id
13. fn_iu_verify_invariants(canonical_address) → assert all_pass=true
14. RETURN jsonb_build_object(
      'status','created', 'iu_id', v_iu_id, 'uv_id', v_uv_id,
      'canonical_address', btrim(p_canonical_address),
      'content_hash', v_hash,
      'birth_verified', true, 'invariants_verified', true,
      'unit_kind', v_unit_kind, 'section_type', v_section_type,
      'version_seq', 1)

3. Q3 RESOLVED — fn_iu_create writes IU + UV + anchor itself

The function is "complete-or-nothing" : IU + UV(version_seq=1) + the anchor UPDATE all happen inside the function body. There is NO separate canonical UV writer ; UV(v1) is birthed atomically with the IU.

For the constitution CUT this means one call per row is sufficient — no separate UV INSERT loop, no separate anchor UPDATE loop.

4. fn_iu_create_plan — dry-run companion

signature       : identical to fn_iu_create
security_definer: YES
volatility      : STABLE
md5             : 9840036c9747fc213234f111314144bc
returns         : jsonb with {mode='plan', status, would_create, existing,
                  resolved_unit_kind, resolved_section_type, resolved_owner_ref,
                  body_length, content_hash_preview (first 16 hex),
                  preflight, fk_initially_deferred, issues}
status values   : plan_ok | invalid_input | invalid_publication_type |
                  unresolved_vocab | preflight_failed | <existing_status>

The redesign does NOT strictly require fn_iu_create_plan (the canonical fn_iu_create runs its own preflight + classify_existing internally). Optional as a defense-in-depth pre-CUT check.

5. fn_iu_verify_invariants — invariants checked at COMMIT step 13

i1  IU row exists at canonical_address
i2  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_registry has an 'information_unit::<iu_id>' entry
i5  uv_birth_ok (per collection_registry birth_code_strategy)
all_pass = (i1 ∧ i2 ∧ i3 ∧ i4 ∧ i5)

NOTE: invariants do NOT cover lifecycle_status, doc_code, section_code, or section_type. The function does NOT enforce 'enacted'.

6. fn_content_hash — hash equivalence with cutwrite

CREATE FUNCTION public.fn_content_hash(p_body text) RETURNS text
  IMMUTABLE LANGUAGE plpgsql
AS $$ BEGIN RETURN encode(digest(p_body,'sha256'),'hex'); END; $$;

Equivalent to cutwrite's Python hashlib.sha256(s.encode("utf-8")).hexdigest() when p_body is the same text in UTF-8. So if the same body string is passed in, the resulting content_hash is byte-identical to cutwrite's uv["content_hash"].

7. Rollback-only live test — fn_iu_create()

Executed 2026-05-20 03:44 UTC inside the postgres container via Unix socket trust auth as directus (the only role with EXECUTE today).

BASELINE_iu_uv_br=98|105|526396
BEGIN
WARNING:  Birth gate L1 PILOT-ONLY: P-pub1 missing — production sẽ BLOCK
WARNING:  Birth gate L1 PILOT-ONLY: P-pub2 missing — production sẽ BLOCK
{ "iu_id": "0f1f4eb9-…", "uv_id": "99806163-…",
  "status": "created", "unit_kind": "law_unit", "version_seq": 1,
  "content_hash": "81e10831…30c",
  "section_type": "principle",
  "birth_verified": true,
  "canonical_address": "ICX-SURVEY-ROLLBACK-ONLY/20260520T034408Z/test-1",
  "invariants_verified": true }
1                                            -- in-txn matching row (visible
                                                 only inside the txn)
ROLLBACK
POST_iu_uv_br_surveyMatch=98|105|526396|0    -- back to baseline ; 0 survey rows
ROLLBACK_TEST=PASS counts unchanged

Findings

  1. The canonical path WORKS today — returns status='created' with birth_verified=true, invariants_verified=true.
  2. The L1 trigger is in PILOT-ONLY mode and emits warnings about P-pub1/P-pub2 "production sẽ BLOCK". When L1 transitions to strict, fn_iu_create calls without p_publication_type may start failing. Not a blocker today; relevant to future re-rerun planning.
  3. ROLLBACK leaves zero persistent footprint: pre- and post-counts are byte-identical (98|105|526396), no ICX-SURVEY-ROLLBACK-ONLY% rows remain.

8. Persisted row shape — direct vs canonical

Column direct (cutwrite shape) canonical (fn_iu_create) in writer_digest?
id deterministic per cutwrite gen_random_uuid() NO
canonical_address per cutwrite passed through YES
unit_kind "law_unit" passed through YES
lifecycle_status 'enacted' (OD-W8) 'draft' (column DEFAULT) NO
content_anchor_ref / version_anchor_ref per cutwrite (then anchored) set by fn_iu_create NO
owner_ref per cutwrite (OWNER) passed as p_owner_ref NO
parent_or_container_ref NULL passed as p_parent_ref NO
conformance_status per cutwrite column DEFAULT 'open' NO
identity_profile rich (multiple keys) {title, owner_lookup_ref, primary_section_type_ref[, publication_type_ref]} NO
created_at / updated_at SQL_NOW sentinel column DEFAULT now() NO
created_by / updated_by "cutter_exec/DOT-991/constitution-cut" btrim(p_actor) NO
doc_code 'ICX-CONST' NULL NO
section_type (column) per cutwrite NULL (value lives in identity_profile.primary_section_type_ref) YES (via cutwrite row, mapped to p_section_type)
section_code per cutwrite NULL NO
uv.id deterministic per cutwrite gen_random_uuid() NO
uv.body per cutwrite passed as p_body NO (but its sha256 is)
uv.content_hash sha256(body) fn_content_hash(p_body) = sha256 hex of body YES

writer_digest input tuple = [canonical_address, unit_kind, section_type, uv.content_hash, idempotency_key].

All five components are PRESERVED end-to-end when the canonical adapter maps cutwrite's rows to fn_iu_create's args. Writer digest equivalence holds.

Semantic gaps to surface for sovereign ruling (NOT in writer_digest, but ARE in persisted row):

  • lifecycle_status='draft' instead of 'enacted'
  • doc_code column NULL (value 'ICX-CONST' lost ; ICX-CONST prefix still visible via canonical_address LIKE 'ICX-CONST%')
  • section_code column NULL (e.g. 'NT-1' lost ; still present in the trailing path segment of canonical_address)
  • section_type column NULL (value still in identity_profile.primary_section_type_ref)
  • identity_profile JSON minimal vs rich
  • id / uv.id server-generated (not deterministic)

9. Disposition

S2                          : PASS — full contract + live ROLLBACK test
no_production_mutation      : confirmed (BEGIN…ROLLBACK; counts unchanged)
next                        : S3 — existing code/docs review
remaining_sovereign_decisions :
  - whether lifecycle_status='draft' is acceptable for constitution IUs
  - whether the missing column data (doc_code/section_code/section_type/
    rich identity_profile) is acceptable
  - whether fn_iu_create needs to be extended (out of cutter_agent scope)
    OR whether the constitution CUT proceeds with the current contract

doc 2 of 7.

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.5-fn-iu-create-canonical-path-survey-and-redesign/dot-iu-cutter-v0.5-02-fn-iu-create-contract-survey-2026-05-20.md