KB-4907

Phase 3B — fn_iu_create Vocab Gap Fix

8 min read Revision 1
dieu45phase-3bfn-iu-createvocab-syncsection-typetac-vocab2026-05-26

Phase 3B — fn_iu_create Vocab Gap Fix

Diagnosis

Phase 3 Phase 3 pilot incident root cause: fn_iu_create validates p_section_type via fn_iu_resolve_default(_,_,'vocab.section_type.'), which checks for dot_config WHERE key = 'vocab.section_type.' || trimmed_value. dot_config carried 13 keys; tac_section_type_vocab (the governed authority — FK target for tac_logical_unit.section_type) carried 17 active codes. The 5-row delta caused the mid-flight CUT failure at piece 11 ([[feedback-fn-iu-create-section-type-vocab-13-item-internal-narrower-than-tac-vocab-17]]).

information_unit itself has no FK to tac_section_type_vocab (only fk_iu_version_anchor → unit_version), so the validation is purely soft via the dot_config vocab gate.

Fix approach

Chosen: Data-side sync — add 5 missing vocab.section_type.* keys to dot_config so fn_iu_create accepts all governed values. No change to fn_iu_create body or to fn_iu_resolve_default. Plus governance assets to detect future drift.

Rejected alternatives:

Alternative Why rejected
Refactor fn_iu_resolve_default to read tac_section_type_vocab for the vocab.section_type. prefix Generic resolver used by many vocab namespaces (unit_kind, publication_type, …); special-casing one prefix breaks abstraction; needs broader design review.
Add fn_iu_section_type_validate(text) and modify fn_iu_create body to call it Changes fn_iu_create body md5; out of mission's "no unrelated behavior changed" envelope; adds a parallel validation path while the dot_config path persists.
Remove the orphan section from dot_config Phase 3 pilot durably stored identity_profile.primary_section_type_ref='section' on 2 IUs (#debt, #law-links). Removing the key changes future fn_iu_create acceptance for section and breaks the existing remap-to-section workaround used by pilot. Documented but not removed.

Apply migration (single committed TX)

BEGIN;
INSERT INTO dot_config(key,value,description) VALUES
 ('vocab.section_type.invariant_list','invariant_list','Phase 3B sync with tac_section_type_vocab (governed). 2026-05-26 dieu45_phase3b.'),
 ('vocab.section_type.matrix','matrix','Phase 3B sync ...'),
 ('vocab.section_type.open_decision_list','open_decision_list','Phase 3B sync ...'),
 ('vocab.section_type.rationale','rationale','Phase 3B sync ...'),
 ('vocab.section_type.reference_mapping','reference_mapping','Phase 3B sync ...');

CREATE OR REPLACE VIEW v_iu_section_type_vocab_sync AS
WITH tac AS (SELECT code FROM tac_section_type_vocab WHERE lifecycle_status='active'),
     dot AS (SELECT substring(key from length('vocab.section_type.')+1) AS code
             FROM dot_config WHERE left(key,length('vocab.section_type.'))='vocab.section_type.')
SELECT COALESCE(tac.code,dot.code) AS code,
       (tac.code IS NOT NULL) AS in_tac_governed,
       (dot.code IS NOT NULL) AS in_dot_config_vocab,
       CASE WHEN tac.code IS NOT NULL AND dot.code IS NOT NULL THEN 'in_sync'
            WHEN tac.code IS NOT NULL AND dot.code IS NULL THEN 'governed_missing_from_dot_config'
            WHEN tac.code IS NULL AND dot.code IS NOT NULL THEN 'dot_config_orphan_not_governed' END AS sync_status
FROM tac FULL OUTER JOIN dot ON tac.code=dot.code;

CREATE OR REPLACE FUNCTION fn_iu_section_type_vocab_sync_check()
RETURNS jsonb LANGUAGE sql STABLE SET search_path TO 'pg_catalog','public' AS $$
  SELECT jsonb_build_object(
    'total',                                    (SELECT count(*) FROM v_iu_section_type_vocab_sync),
    'in_sync',                                  (SELECT count(*) FROM v_iu_section_type_vocab_sync WHERE sync_status='in_sync'),
    'governed_missing_from_dot_config_count',   (SELECT count(*) FROM v_iu_section_type_vocab_sync WHERE sync_status='governed_missing_from_dot_config'),
    'governed_missing_from_dot_config',         COALESCE((SELECT jsonb_agg(code ORDER BY code) FROM v_iu_section_type_vocab_sync WHERE sync_status='governed_missing_from_dot_config'),'[]'::jsonb),
    'dot_config_orphan_not_governed_count',     (SELECT count(*) FROM v_iu_section_type_vocab_sync WHERE sync_status='dot_config_orphan_not_governed'),
    'dot_config_orphan_not_governed',           COALESCE((SELECT jsonb_agg(code ORDER BY code) FROM v_iu_section_type_vocab_sync WHERE sync_status='dot_config_orphan_not_governed'),'[]'::jsonb),
    'status',                                   CASE WHEN (SELECT count(*) FROM v_iu_section_type_vocab_sync WHERE sync_status='governed_missing_from_dot_config')=0 THEN 'PASS' ELSE 'FAIL_GOVERNED_GAP' END,
    'checked_at',                               now()
  )
$$;
COMMIT;

Result: COMMIT clean. dot_config vocab.section_type.* count: 13 → 18.

Sync view after apply

code in_tac_governed in_dot_config_vocab sync_status
appendix..technical_spec (17 governed) t t in_sync
section f t dot_config_orphan_not_governed

fn_iu_section_type_vocab_sync_check() returns:

{
  "total": 18, "status": "PASS", "in_sync": 17, "checked_at": "...",
  "governed_missing_from_dot_config": [], "governed_missing_from_dot_config_count": 0,
  "dot_config_orphan_not_governed": ["section"], "dot_config_orphan_not_governed_count": 1
}

Behavior proof — fix-side (fn_iu_resolve_default)

For all 17 active tac_section_type_vocab codes, post-fix fn_iu_resolve_default(code,'iu_create.default_section_type','vocab.section_type.')->>'status' returns explicit (17/17).

Invalid value definitely_not_a_section_type_xyz returns:

{
  "value": "definitely_not_a_section_type_xyz",
  "status": "invalid",
  "message": "Not in vocab. Available: appendix, article, changelog, checklist, definition, governance_process, heading, instruction_block, invariant_list, matrix, open_decision_list, paragraph, principle, process, rationale, reference_mapping, section, technical_spec"
}

Empty/null still returns unresolved (default-chain unchanged — multiple values, no iu_create.default_section_type key present).

Behavior proof — end-to-end (fn_iu_create BEGIN/ROLLBACK)

For each of the 17 governed codes, BEGIN; call fn_iu_create(p_unit_kind:='law_unit', p_section_type:=v_code, …) with a synthetic canonical_address; assert status='created' AND section_type=v_code; ROLLBACK.

Result: total=17 pass=17 fail=0 (P-pub1/P-pub2 birth-gate warnings observed but non-blocking — known §0-G pilot warnings).

Negative test: p_section_type:='definitely_not_valid_zzz' raised section_type: Not in vocab. Available: appendix, article, ..., section, technical_spec (18-value list).

Orphan probe: p_section_type:='section'status=created section_type=section (preserved, no behavior change).

Post-rollback information_unit count: 192 → 192 (zero durable pollution).

fn_iu_create body invariance

md5(pg_get_functiondef('fn_iu_create')) post-apply: dcade99af1ef096892748c9f14082e11 — identical to pre-apply. The fix is pure data-side; the function code never changed.

Lesson saved

Existing memory [[feedback-fn-iu-create-section-type-vocab-13-item-internal-narrower-than-tac-vocab-17]] is now resolved (gap closed; sync view + check fn protect against regression).

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-dieu45-phase-3b-queue-cutter-hardening/02-fn-iu-create-vocab-gap-fix.md