Phase 3B — fn_iu_create Vocab Gap Fix
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).