98 — Phase 1 SB-1 F-83-1 Birth-Trigger Fix Build Requirement (no build, 2026-06-01)
98 — Phase 1 SB-1 F-83-1 Birth-Trigger Fix Build Requirement
Mission §9 (Branch F). Tier: the dedicated, load-bearing engineering requirement for the SB-1 build step (STEP 6). Mutation footprint: ZERO (specification; the fix was validated live in rehearsal under BEGIN..ROLLBACK, doc 83, and reversed). Without this fix the SB-1 INSERTs fail outright — it is the single most important Phase-1 build correction. Sources: doc 83 §83.1–83.3 (live discovery + in-txn validation), doc 84 (trigger landscape), doc 86 §F-83-1/F-83-2 + §86.2 L-APR-NO-BIRTH. Live re-confirmed 2026-06-01:
apr_action_types=6,birth_registryrows forapr_action_types=0.
98.1 Exact root cause
apr_action_typeshas anAFTER INSERTtriggertrg_birth_apr_action_typeswired tofn_birth_registry_auto()with no code-field argument.fn_birth_registry_auto()with no arg computes the synthetic Birth key from anidcolumn:entity_code := 'apr_action_types::' || (NEW.row->>'id').apr_action_typeshas noidcolumn (its PK isaction_code).- Therefore
entity_codeevaluates to'apr_action_types::' || NULL= NULL. - The function inserts into
birth_registry, whoseentity_codeis NOT NULL + UNIQUE. The NULL →null value in column "entity_code" … violates not-null constraint(observed verbatim, doc 83 §83.2).ON CONFLICT (entity_code) DO NOTHINGdoes not catch a NOT-NULL violation (only unique conflicts), so the INSERT aborts. - Why the 6 existing rows survive: they predate the birth trigger →
birth_registryhas 0 rows forcollection_name='apr_action_types'(L-APR-NO-BIRTH). The trigger has never successfully fired for this table.
Contrast that proves the fix: the sibling approval_requests birth trigger is wired fn_birth_registry_auto('code') with a code-field arg — which is exactly why approval_requests inserts succeed. apr_action_types's code field is action_code.
98.2 Why SB-1 build fails without the fix
SB-1 Phase A = INSERT 4 governance action-type rows into apr_action_types. Every one of those INSERTs fires the broken trigger and hits the NULL entity_code violation. No row can be registered. The build of SB-1 is impossible as-is; the four action-types (assign_governance_owner, grant_governance_exception, delegate_authority, assign_axis_owner) cannot exist as vocabulary. This is a hard blocker, not a warning.
98.3 Exact required build adjustment
Before the 4 INSERTs, in the same SB-1 transaction, re-wire the trigger to pass the code field:
DROP TRIGGER trg_birth_apr_action_types ON apr_action_types;
CREATE TRIGGER trg_birth_apr_action_types
AFTER INSERT ON apr_action_types
FOR EACH ROW
EXECUTE FUNCTION fn_birth_registry_auto('action_code');
This mirrors the working approval_requests wiring (fn_birth_registry_auto('code')), substituting this table's code field action_code. With the arg present, fn_birth_registry_auto uses NEW.action_code for the synthetic entity_code → a real, non-null, unique Birth key per row.
- It is a corrective DDL on a
directus-owned table → needs owner/superuser (the build channelpsql -U workflow_adminhas it; the read-onlyquery_pgrole does not — which is why this was rehearsed via the write channel, doc 83). - It is transactional: in rehearsal, ROLLBACK fully restored the original no-arg def (proven, doc 83 §83.3).
- At build it is permanent (kept on COMMIT) — see §98.5.
98.4 How to verify the trigger uses action_code
Before the fix (preflight, doc 96 §D):
SELECT pg_get_triggerdef(oid) FROM pg_trigger
WHERE tgname='trg_birth_apr_action_types' AND NOT tgisinternal;
-- expect: ... EXECUTE FUNCTION fn_birth_registry_auto() (no arg — the broken def, confirms fix needed)
After the fix (in-rehearsal and post-commit):
SELECT pg_get_triggerdef(oid) FROM pg_trigger
WHERE tgname='trg_birth_apr_action_types' AND NOT tgisinternal;
-- expect: ... EXECUTE FUNCTION fn_birth_registry_auto('action_code')
-- functional proof: after inserting the 4 rows, birth rows appear with entity_code = action_code:
SELECT entity_code FROM birth_registry WHERE collection_name='apr_action_types' ORDER BY entity_code;
-- expect: assign_axis_owner | assign_governance_owner | delegate_authority | grant_governance_exception
98.5 How to restore the original if rollback
- In-flight (rehearsal or pre-COMMIT abort):
ROLLBACK;— the DDL reverts to the original no-arg def automatically (proven doc 83 §83.3). Nothing further required. - Post-COMMIT: KEEP the fix.
fn_birth_registry_auto('action_code')is the correct wiring; the no-arg def is the defect. Reverting it would re-break every futureapr_action_typesinsert.- If the SB-1 rows must be undone post-commit, retire them (doc 97 §97.3 SB-1:
status='retired'), leaving the corrected trigger in place. - Revert the trigger DDL itself only under an emergency that the fix caused (it did not in rehearsal). If ever needed: first capture the exact original def, then re-apply that captured text, and document that all
apr_action_typesinserts are now blocked again.
- If the SB-1 rows must be undone post-commit, retire them (doc 97 §97.3 SB-1:
98.6 How to avoid birth_registry NULL entity_code
The fix is the avoidance: with 'action_code' passed, the synthetic entity_code is NEW.action_code (non-null, unique per action-type). Guardrails:
- Each of the 4 rows has a distinct, non-null
action_code(the four governance codes) → 4 distinct Birth keys, no collision with the 6 existing codes. - Do not insert a row with a NULL/blank
action_code(PK forbids it anyway). - Do not attempt the INSERTs before the trigger re-wire (ordering is load-bearing within the transaction).
98.7 How to verify no orphan / phantom birth rows
After the SB-1 commit, confirm exactly 4 governance Birth rows and nothing stray:
SELECT count(*) FROM birth_registry WHERE collection_name='apr_action_types'; -- expect 4 (was 0)
SELECT entity_code, entity_code IS NULL AS is_null
FROM birth_registry WHERE collection_name='apr_action_types'; -- 4 rows, none NULL
-- no birth row whose entity_code lacks a matching action-type (no phantom):
SELECT b.entity_code FROM birth_registry b
LEFT JOIN apr_action_types a ON a.action_code = b.entity_code
WHERE b.collection_name='apr_action_types' AND a.action_code IS NULL; -- expect 0 rows
In rehearsal (doc 83 §83.3) the 4 birth rows appeared with entity_code=action_code, governance_role='governed', certified=f; on ROLLBACK they were gone (birth_rows_after=0). No phantom/orphan rows were created.
98.8 Why the fix is allowed only inside the SB-1 build step
- It is engineering, not policy — doc 86 classifies F-83-1 as an engineering fold-in needing no separate council/sovereign decision beyond the SB-1 step's own M-1 + C-2.
- It is scoped to enabling the 4 INSERTs — re-wiring the trigger has no effect until a row is inserted; doing it outside the SB-1 step would be a free-floating DDL on a governed table with no authorizing context.
- Bundling it in the SB-1 transaction keeps the change reviewable, rollback-coupled to the rows, and covered by the SB-1 authorization (M-1 + C-2). It must not be pre-applied in an earlier step, nor deferred to a later one.
- It must not be generalized into "fix all birth triggers" — only
apr_action_types's trigger is in scope for SB-1.
98.9 Stop conditions (F-83-1 specific)
- The preflight trigger def is already
fn_birth_registry_auto('action_code')(someone applied it out-of-band) → STOP, reconcile authorization before proceeding; do not double-apply. - The
[TRIGGER-GUARD]DDL-audit event trigger fires at ERROR (not WARNING) level on the DROP/CREATE → STOP; route the re-wire through the guard's allow-path or obtain the guard exception; do not force past a hardened guard (F-83-2). - After the fix, the rehearsal INSERT still raises a
birth_registry.entity_codeNULL → STOP; the fix is not effective (wrong code field / wrong function signature) — investigate, do not work around. - C-2 (SB-1 council build record) or M-1 (SB-1 sovereign row) absent → STOP; rehearsal-only.
- Any temptation to drop/disable the birth trigger entirely (instead of re-wiring it) → STOP; that would silently strip Birth coverage from
apr_action_types, a no-island/no-hardcode violation.
Branch F verdict: F-83-1 is fully specified — root cause (no-arg birth trigger + no id column → NULL entity_code), build impact (SB-1 INSERTs fail), exact fix (re-wire to fn_birth_registry_auto('action_code') before the 4 INSERTs, in the SB-1 txn), verification (trigger def + 4 non-null Birth rows + no phantom), restore stance (in-flight ROLLBACK auto-restores; post-commit KEEP the fix, retire rows instead), and step-scoping rationale. Validated live under ROLLBACK; zero committed mutation; build gated on M-1 + C-2.