KB-187B

98 — Phase 1 SB-1 F-83-1 Birth-Trigger Fix Build Requirement (no build, 2026-06-01)

10 min read Revision 1
one-roof-governanceimplementation-indexphase1sb-1F-83-1birth-triggerfn_birth_registry_autobuild-requirementno-commit2026-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_registry rows for apr_action_types=0.


98.1 Exact root cause

  1. apr_action_types has an AFTER INSERT trigger trg_birth_apr_action_types wired to fn_birth_registry_auto() with no code-field argument.
  2. fn_birth_registry_auto() with no arg computes the synthetic Birth key from an id column: entity_code := 'apr_action_types::' || (NEW.row->>'id'). apr_action_types has no id column (its PK is action_code).
  3. Therefore entity_code evaluates to 'apr_action_types::' || NULL = NULL.
  4. The function inserts into birth_registry, whose entity_code is 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 NOTHING does not catch a NOT-NULL violation (only unique conflicts), so the INSERT aborts.
  5. Why the 6 existing rows survive: they predate the birth trigger → birth_registry has 0 rows for collection_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 channel psql -U workflow_admin has it; the read-only query_pg role 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 future apr_action_types insert.
    • 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_types inserts are now blocked again.

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_code NULL → 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.

Back to Knowledge Hub knowledge/dev/reports/architecture/one-roof-governance-technical-addendum-and-implementation-index-2026-06-01/98-phase1-sb1-f83-trigger-fix-build-requirement.md