KB-7B04

P3D — B3-ELD Entity Living DB Placeholder Design

28 min read Revision 1
p3dbirth-systemb3-eldentity-living-dbdesignplaceholder2026-05-14

P3D — B3-ELD Entity Living DB Placeholder Design

Date: 2026-05-14 Author: Opus (Claude Code) — DESIGN ONLY Mode: NO PG mutation, NO DDL, NO DML, NO function patch, NO trigger patch. Read-only verification + design doc. Predecessor: p3d-birth-mainline-resume-completeness-verification-report.md GPT priority: gpt-review-b3-mainline-resume-completeness-verification-priority-adjusted-2026-05-14.md Secrets redacted: true


1. Purpose & scope

GPT review accepted B3-MAINLINE-RESUME evidence and adjusted priority so the Entity Living DB (ELD) placeholder layer is the PRIMARY birth-complete blocker. Of the 6 design-named ELD fields, only governance_role lives on birth_registry. The four absent placeholders — canonical_address, owner, jsonb_profile, identity_profile — must be designed before any DDL/DML is authorized.

This document decides per-field placement, birth responsibility, backfill, function impact, contract impact, PG-native pattern, relation-hook alignment, enrichment alignment, options comparison, and a single recommendation. It authorizes nothing. Phase 5C2 remains gated. Wrapper / system_health_checks insert / scheduler repair remain separate workstreams.

Out of scope: trigger function rewrites, vector pipeline, Pivot population, UI cutover, dot-dot-health repair.


2. Live PG evidence (2026-05-14, READ-ONLY)

All SELECTs executed via docker exec postgres psql -U directus -d directus -At. No mutation. Secrets redacted.

2.1 Mandatory existing-pattern detection

Existing columns named like the 5 ELD fields anywhere in public:

column_name tables data_type notes
canonical_address event_outbox, event_pending, information_unit, iu_notification_event, tac_logical_unit, unit_edit_draft (6 tables) text Already a live convention. Sample values look like slug paths: pilot.iu0.test-001.
identity_profile information_unit, tac_logical_unit (2 tables) jsonb Both NOT NULL DEFAULT '{}'::jsonb. Sample: {"title": "...", "owner_lookup_ref": "agent:p3c4", "primary_section_type_ref": "section"}.
owner agents, collection_registry, dot_tools, help_articles, modules, organizations, os_deals, os_projects, tac_change_set, tac_logical_unit, tac_publication, tac_section_type_vocab, ui_pages (13 tables) mostly varchar, some text/character Inconsistent shape. Many tables also use distinct owner_ref text (e.g., information_unit).
jsonb_profile NONE Never used as a literal column name. Variants exist: identity_profile, content_profile, publication_profile. jsonb_profile is a category, not a column.
governance_role birth_registry, collection_registry, v_entity_full_classification varchar/text Already on birth_registry (verified prior).

2.2 information_unit schema (canonical ELD reference shape)

Verified columns (first 15):

id                       uuid       NOT NULL  DEFAULT gen_random_uuid()
canonical_address        text       NOT NULL
unit_kind                text       NOT NULL
lifecycle_status         text       NOT NULL  DEFAULT 'draft'
content_anchor_ref       text       NULL
version_anchor_ref       uuid       NULL
owner_ref                text       NOT NULL                       -- NOTE: owner_ref, not owner
parent_or_container_ref  uuid       NULL
conformance_status       text       NOT NULL  DEFAULT 'open'
identity_profile         jsonb      NOT NULL  DEFAULT '{}'::jsonb
created_at               timestamptz NOT NULL DEFAULT now()
...

Key signals: canonical_address and identity_profile already coexist on the same row in the entity layer, with identity_profile as a JSONB default-{} placeholder. owner is exposed as owner_ref (text reference) — not the bare owner column variant.

2.3 birth_registry schema (verified prior, re-confirmed)

19 columns. Of the 6 ELD targets, only governance_role present:

id, sort, user_created, date_created, user_updated, date_updated,
entity_code, collection_name, species_code, composition_level, dot_origin, born_at,
governance_role, inspect_pen, inspect_stamp, inspect_gate, certified, certified_at, status

2.4 fn_birth_registry_auto body (verified)

Inserts exactly 8 columns: entity_code, collection_name, species_code, composition_level, dot_origin, born_at, governance_role, certified.

The trigger function has NO knowledge of any other column on birth_registry. Any nullable column added to birth_registry defaults to NULL on insert; the function does not need to know it exists. The function is "dumb/fast" by design and stays that way.

ON CONFLICT (entity_code) DO NOTHING on insert.

2.5 Sample birth_registry rows

292599|knowledge_documents::3174|knowledge_documents|ai_support|atom|PG:trg_birth_knowledge_documents|observed|born

Row is a registry stub. It correctly already references the entity by (collection_name, entity_code) — i.e., the universal birth address (B2 §4). It does NOT carry instance-level enrichment yet.

2.6 Profile-like columns across schema

tac_unit_version        content_profile        jsonb
unit_version            content_profile        jsonb
information_unit        identity_profile       jsonb
tac_logical_unit        identity_profile       jsonb
tac_publication         publication_profile    jsonb

The pattern is kind-specific JSONB columns, not a single generic jsonb_profile.

2.7 Address tables

Only organization_addresses exists. No general entity_address / address_registry exists. canonical_address is treated as a slug/path string in information_unit, not a normalized FK to an address table.


3. Per-field placement decisions

For each field, choices: A=birth_registry, B=collection_registry, C=per-entity physical table, D=new ELD table, E=JSONB profile store, F=derived-only, G=hybrid.

3.1 canonical_address

  • Decision: G (HYBRID) — primary placement A: birth_registry.canonical_address text as a placeholder; physical-table copies on entities that already have one (information_unit + 5 others) remain authoritative.
  • Rationale:
    • Existing pattern (§2.1) puts canonical_address on the entity row, not a central registry. Pattern is text slug-path.
    • But B2 §4 already declares universal birth address = (collection_name, entity_code). For 99% of collections there is no native slug; the placeholder must live in one universal place: birth_registry.
    • For collections that already carry canonical_address on the physical table (6 tables), the birth-registry copy is mirrored on enrichment (DERIVED_LATER from physical), not authoritative.
  • Evidence tags: LIVE_PG §2.1, §2.2; KB B2 §4.
  • Type: text NULL on birth_registry. No default. Backfill via enrichment job — see §5.

3.2 owner

  • Decision: A (birth_registry) — placeholder with explicit name owner text NULL for the placeholder slot; enrichment populates via owner-resolution chain (B2 §3).
  • Rationale:
    • Existing owner columns (§2.1, 13 tables) are inconsistent (varchar/text/character) and named ambiguously vs owner_ref (information_unit's pattern).
    • We choose owner text NULL on birth_registry to match B2 §3's owner-resolution result (final resolved string, e.g. agent:p3c4, user:abc, team:legal, UNKNOWN_OWNER).
    • We do NOT add an owner_ref second column on birth_registry — birth_registry stores the resolved owner, not the source pointer. The source pointer remains on the physical table (where applicable).
  • Evidence tags: LIVE_PG §2.1, §2.2 (owner_ref on information_unit); KB B2 §3 (owner ABSENT per contract).
  • Type: text NULL. No default. Backfill: ENRICHED_LATER (UNKNOWN_OWNER sentinel allowed; never NULL after enrichment passes).

3.3 jsonb_profile

  • Decision: A (birth_registry) — jsonb_profile jsonb NOT NULL DEFAULT '{}'::jsonb as the generic JSONB store for cross-cutting birth-time hints (description_policy snapshot, classification snapshot, hook references, owner-resolution trace).
  • Rationale:
    • Existing physical-table pattern uses kind-specific JSONB columns (identity_profile, content_profile, publication_profile) — those stay on physical tables and are NOT collapsed into jsonb_profile.
    • birth_registry is the universal layer; it needs a catch-all JSONB slot so future enrichment (B6) can add keys without DDL. This is the self-expanding pattern.
    • Default '{}'::jsonb lets the soft gate / full-scan check jsonb_profile <> '{}'::jsonb for enrichment readiness without NULL-handling.
  • Evidence tags: LIVE_PG §2.6 (kind-specific profiles exist, generic profile does NOT exist).
  • Type: jsonb NOT NULL DEFAULT '{}'::jsonb. Backfill: existing 292,599 rows get default {} — safe.

3.4 identity_profile

  • Decision: G (HYBRID), but primary placement is C (per-entity physical table) for entities that need it; placeholder column on birth_registry is OPTIONAL and recommended SKIPPED.
  • Rationale:
    • identity_profile already exists on information_unit and tac_logical_unit as jsonb NOT NULL DEFAULT '{}'. The intended semantic is kind-specific identity metadata (title, primary refs, lookup keys for the entity as that entity-kind).
    • For ~140 collections that have no kind-specific identity profile, putting a per-row identity_profile on birth_registry would duplicate jsonb_profile semantics (decision 3.3) and create two empty JSONB slots competing for the same data.
    • Recommendation: DO NOT add identity_profile to birth_registry. Treat it as DERIVED_LATER from physical tables when present; otherwise leave absent. jsonb_profile.identity JSONB key on birth_registry can carry a snapshot for cross-collection summaries if needed.
  • Evidence tags: LIVE_PG §2.1, §2.2, §2.6.
  • Type: not added centrally.

3.5 Summary table

Field Primary placement birth_registry column? Type Default Rationale ref
canonical_address HYBRID (registry placeholder + physical-table mirror) YES text NULL §3.1
owner birth_registry YES text NULL §3.2
jsonb_profile birth_registry (new universal slot) YES jsonb '{}'::jsonb NOT NULL §3.3
identity_profile per-entity physical (existing); not on birth_registry NO §3.4

Net DDL surface (when authorized): 3 nullable/defaulted ADD COLUMN on birth_registry, no new tables, no FK, no UNIQUE.


4. Birth responsibility matrix

Field Classification Birth trigger writes it? Enriched by When
canonical_address PLACEHOLDER_AT_BIRTH (NULL slot) NO — trigger does not write enrichment job derived from physical table; otherwise stays NULL post-birth, idempotent
owner PLACEHOLDER_AT_BIRTH (NULL slot) NO B2 §3 owner-resolution chain → agent:*, user:*, team:*, UNKNOWN_OWNER post-birth, idempotent
jsonb_profile REQUIRED_AT_BIRTH (default '{}', NOT NULL) NO write needed — column default covers it enrichment job patches JSONB keys post-birth, idempotent
identity_profile NOT_BIRTH_RESPONSIBILITY (per-entity) NO already populated where it exists (default '{}') on physical tables n/a
governance_role REQUIRED_AT_BIRTH (already wired) YES — trigger writes from collection_registry birth-time
description_policy NOT_BIRTH_RESPONSIBILITY (collection-level) n/a — lives on collection_registry, fully populated n/a

Birth trigger function changes required: NONE — see §6.


5. Backfill plan

Pre-conditions: 292,599 rows in birth_registry (most recent IDs probed).

Field Default Existing rows backfill Safe? Required before Phase 5C2?
canonical_address NULL None at DDL time; enrichment job fills later YES — adding a nullable column is metadata-only NO (placeholder OK at 5C2 entry; enrichment can be parallel)
owner NULL None at DDL time; enrichment job fills later YES NO — placeholder acceptable; enrichment closes gap
jsonb_profile '{}'::jsonb (DEFAULT, NOT NULL) All existing rows auto-fill '{}' at ADD COLUMN (PG ≥11 fast-path with constant default) YES — fast-path; no full rewrite YES — column must exist before 5C2 if soft gate references it
identity_profile n/a n/a n/a n/a

No backfill DDL/DML authorized here. This plan is for the future B3-ELD-EXEC pack.

Phase 5C2 readiness: the only strictly required slot before 5C2 is jsonb_profile (because soft gate/full-scan may reference it). canonical_address and owner columns must exist (DDL) but their values may be NULL at 5C2 entry, with enrichment closing the gap in B6.


6. Function impact

Object Modification required? Reason
fn_birth_registry_auto NO Function inserts 8 named columns (§2.4). Adding nullable/defaulted columns is transparent.
fn_birth_registry_auto_id NO Same reasoning — variants inherit same insert shape.
fn_collection_onboarding_soft_gate OPTIONAL Currently checks species mapping + classification. Adding a check for jsonb_profile column existence is OPTIONAL hardening for B3-F1d follow-up.
fn_birth_onboarding_full_scan OPTIONAL Could add a NULL-rate report for canonical_address/owner to surface enrichment gaps. Non-blocking.
fn_b3f1_log_collection_onboarding_gap NO No signature change.

Net effect: birth_function_patch_required=false. Trigger layer stays dumb/fast. Enrichment is a separate pass.


7. Contract impact (B2 / B3-F)

7.1 B2 contract (p3d-birth-system-b2-contract-design.md rev2)

B2 rev3 REQUIRED. Required revisions:

  • §2 (Birth metadata contract): adjust from "9/10 supported, owner ABSENT" to "9/10 supported; owner present as PLACEHOLDER_AT_BIRTH; canonical_address, jsonb_profile added as PLACEHOLDER/REQUIRED slots; identity_profile remains per-entity-kind (not centralized)".
  • §3 (Owner resolution): explicitly mark birth_registry.owner as the resolved result; physical-table owner / owner_ref columns are sources.
  • §4 (Universal birth address): canonical address is (collection_name, entity_code) pair AND optionally canonical_address text for entities with native slug paths. Birth registry carries a NULL-able mirror.
  • §5 (8 relation hooks): unchanged — see §9 mapping.
  • §6 (Coverage classification): add ELD column-population coverage as a sub-status (e.g., ELD_PLACEHOLDER_PRESENT, ELD_ENRICHED, ELD_PENDING).

7.2 B3-F gate updates

Gate Update required? What
Soft gate (fn_collection_onboarding_soft_gate) NO for placeholder DDL; OPTIONAL to add ELD column presence check non-blocking
Full-scan (fn_birth_onboarding_full_scan) NO for DDL; YES (recommended) to add NULL-rate audit columns post-DDL enrichment visibility
Onboarding (new collection) requires ELD fields? NO — placeholder columns are universal on birth_registry; nothing per-collection is required at onboarding self-expanding by design

7.3 Roadmap impact

  • B2 → B2 rev3 (this design's contract revision).
  • B3 → unchanged, but a new sub-step B3-ELD-EXEC added between B3 and B4.
  • B4 (backfill) → only canonical_address and owner enrichment; jsonb_profile is auto-defaulted by DDL.

8. PG-native self-expanding pattern

Design must NOT hardcode collection lists or species lists.

8.1 Anti-hardcode invariants

  • ELD placeholder columns live on a universal table (birth_registry), not per-collection. ✓
  • DDL adds 3 columns once; no per-collection migration. ✓
  • No list of "ELD-enabled collections" anywhere in code or config. ✓
  • New collections inherit birth_registry columns automatically as soon as fn_birth_registry_auto fires. ✓

8.2 Self-expansion mechanics

When a new collection is onboarded:

  1. Row appears in collection_registry with governance_role (B2 contract guaranteed).
  2. Optional species mapping appears in species_collection_map.
  3. First INSERT on the new table fires fn_birth_registry_auto, which writes the 8 known columns. The 3 new ELD placeholder columns default automatically:
    • canonical_address → NULL
    • owner → NULL
    • jsonb_profile'{}'::jsonb
  4. Enrichment passes pick up the row via WHERE owner IS NULL / WHERE jsonb_profile = '{}' / WHERE canonical_address IS NULL queries — no allowlist required.

8.3 Discovery-first contract

Enrichment jobs MUST derive their target set from:

  • collection_registry (which collections are governed),
  • species_collection_map (per-collection species),
  • birth_registry (which rows exist), and
  • the new ELD placeholder NULL/empty predicate.

No IN ('table_a', 'table_b', ...) literals allowed.


9. Relation hook alignment matrix (8 hooks × 4 ELD fields)

Cells show what role each field plays for each Điều 21 §III hook.

Hook canonical_address owner jsonb_profile identity_profile
IDENTITY (T̀ôi là ai) anchor (universal birth address mirror) secondary snapshot key per-kind identity
BELONGS_TO (T̀ôi thuộc về ai) direct support (owner=container) snapshot key belongs_to_ref
CONTAINS (T̀ôi chứa gì) snapshot key contains_summary
DEPENDS_ON snapshot key depends_on_refs
USED_BY snapshot key used_by_refs
TRANSITIVE indirect (via address graph) snapshot key transitive_seed
PEERS snapshot key peers_classification per-kind identity (feeds peer query)
SIMILAR snapshot key similar_seed_text per-kind identity (feeds vector input)

Key insight: jsonb_profile is the universal hook-snapshot slot, populated lazily, namespaced by hook name. This eliminates 8 separate columns and gives B6 enrichment freedom to add keys without DDL.


10. Enrichment alignment matrix

How each placeholder supports each enrichment workstream.

Enrichment canonical_address owner jsonb_profile identity_profile
Vector / Qdrant jsonb_profile.similar_seed_text feeds embedding input per-kind identity feeds embedding (existing pipeline)
Pivot / facet / classification jsonb_profile.peers_classification stores pivot snapshot
peers snapshot from pivot result per-kind classification basis
similar seed text + vector hash per-kind text source
transitive seed for graph BFS edge enumeration cache
description_policy jsonb_profile.description_policy_snapshot (from collection_registry.description_policy)
canonical address resolution primary fallback cache
owner resolution primary (resolved result) trace in jsonb_profile.owner_resolution_trace

11. Options A/B/C/D comparison

Add canonical_address text NULL, owner text NULL, jsonb_profile jsonb NOT NULL DEFAULT '{}'::jsonb. Skip identity_profile (per §3.4).

Aspect Detail
Pros Smallest DDL surface; zero trigger-function change; PG ≥11 fast-path ADD COLUMN with constant default; aligns with universal (collection_name, entity_code) address; backwards compatible; self-expanding; rollback = DROP COLUMN
Cons Columns are universal — may force NULLs on collections that semantically have no canonical_address; small storage overhead for 292k+ rows
Hardcode risk LOW — no per-collection logic
Scale risk LOW — birth_registry is one table; columns scale with row count, not collection count
Migration complexity LOW — 3 ADD COLUMNs; fast-path defaults
Function impact NONE
Rollback complexity LOW — DROP COLUMN works as long as no enrichment writes are pending
Compatibility with Phase 5C2 HIGH — jsonb_profile present, soft gate can reference it

Option B — New entity_living_db table with 1:1 to birth_registry

Create entity_living_db (birth_id FK, canonical_address, owner, jsonb_profile, identity_profile, ...).

Aspect Detail
Pros Clean separation of concerns; ELD evolves without touching birth_registry; allows richer ELD schema later (FK to address tables, audit trail)
Cons Requires NEW FK on every birth; trigger function MUST be patched to also insert into ELD table; backfill required for 292k rows; JOIN every read; rollback complex (DROP TABLE + un-patch trigger)
Hardcode risk LOW
Scale risk MEDIUM — extra table doubles birth-time write cost; JOIN on every enrichment read
Migration complexity HIGH — CREATE TABLE + FK + trigger patch + backfill 292k rows
Function impact YES — fn_birth_registry_auto MUST be patched (breaks "dumb/fast" principle)
Rollback complexity HIGH
Compatibility with Phase 5C2 MEDIUM — requires successful trigger patch first

Option C — Hybrid: minimal columns on birth_registry + entity_living_db_profiles for kind-specific JSONB

Add canonical_address, owner to birth_registry; create a entity_living_db_profiles (birth_id, profile_kind, payload jsonb) table for identity_profile, content_profile, etc.

Aspect Detail
Pros Theoretically tidy; kind-specific profiles centralized
Cons Existing physical tables already own kind-specific profiles (information_unit, tac_*); duplicates them; loop-back risk; requires sync; collides with the existing successful pattern
Hardcode risk MEDIUM — profile_kind enum tends to become hardcoded list
Scale risk MEDIUM
Migration complexity MEDIUM-HIGH
Function impact MEDIUM — additional INSERT path
Rollback complexity MEDIUM
Compatibility with Phase 5C2 MEDIUM

Option D — Derived-only / no storage

Do NOT add any column. Define canonical_address, owner, jsonb_profile, identity_profile as views over birth_registry JOIN collection_registry JOIN per-entity tables.

Aspect Detail
Pros Zero DDL; zero migration; pure read-path
Cons Cannot persist owner-resolution result; cannot snapshot description_policy at birth time; soft gate cannot reference a column; performance penalty on every read (multi-table join, sometimes 100+ collections); fragile when underlying tables drop columns
Hardcode risk HIGH — view must enumerate all collections with canonical_address
Scale risk HIGH — JOINs across hundreds of physical tables
Migration complexity LOW (just views)
Function impact NONE
Rollback complexity LOW
Compatibility with Phase 5C2 LOW — soft gate cannot enforce

12. Recommendation

Option A — Add 3 nullable/defaulted columns to birth_registry.

Justification:

  1. Smallest blast radius. 3 ADD COLUMNs, all with PG ≥11 fast-path metadata-only default. No trigger function patch. Rollback is 3 DROP COLUMNs.
  2. Honors "birth is dumb/fast" (B2/architecture v3 principle): the trigger function is untouched.
  3. Reuses the existing convention detected in §2.1 (canonical_address text, jsonb for profile-style data).
  4. PG-native self-expanding (§8): no allowlist, no per-collection migration. New collections inherit placeholders the moment their first row triggers birth.
  5. Skips identity_profile because it already lives correctly on physical tables (information_unit, tac_logical_unit) — adding it to birth_registry would duplicate jsonb_profile semantics.
  6. owner column on birth_registry is the resolved owner string — the resolution chain (B2 §3) writes this; sources stay on physical tables (information_unit's owner_ref, collection_registry's owner, etc.).
  7. jsonb_profile is a generic JSONB catch-all that lets B6 enrichment add keys without DDL — the strongest anti-DDL-creep design.

DDL execution NOT authorized here. Subsequent pack B3-ELD-EXEC will:

  • Execute ALTER TABLE birth_registry ADD COLUMN ... × 3 under standard governance review.
  • Update B2 contract to rev3 (§7.1).
  • Optionally update full-scan to surface NULL rates (§6).

13. Open questions / deferred to execution phase

  1. Should birth_registry.canonical_address be text or varchar(255)? Recommend text to match information_unit. Decide at exec time.
  2. Should jsonb_profile have a GIN index? Recommend YES if enrichment writes are frequent; defer to exec when query patterns known.
  3. Should owner accept NULL or always default to 'UNKNOWN_OWNER'? Recommend NULL pre-enrichment, sentinel 'UNKNOWN_OWNER' post-enrichment. Decide at exec time.
  4. Should the existing 6 physical-table canonical_address columns mirror to birth_registry? Recommend YES via enrichment pass; not a backfill requirement.
  5. Does B3-F1c-g scheduler repair land before or after B3-ELD-EXEC? Per GPT review, parallel; this design imposes no ordering.
  6. description_policy snapshot on birth? Recommend NO — collection_registry.description_policy is authoritative; reading from there at enrichment time is fine. jsonb_profile.description_policy_snapshot is optional, not required.
  7. Should we add a eld_enrichment_status column to track enrichment progress? Possible future addition; design intentionally omitted to keep DDL minimal. Status can be derived from NULL predicates on the 3 columns.
  8. Rollback strategy if Phase 5C2 fails post-DDL? DROP COLUMN works only if no enrichment writes happened. Recommend taking a logical snapshot before B3-ELD-EXEC executes.
  9. Are there _history shadow tables on birth_registry? Not verified in this design pass. If yes, DDL must propagate.

14. Governance status

b3_eld_design_status=PASS
recommended_option=ADD_BIRTH_REGISTRY_COLUMNS
canonical_address_decision=placeholder_at_birth on birth_registry.text NULL; derived later from physical-table mirror or enrichment
owner_decision=placeholder_at_birth on birth_registry.text NULL; enriched_later via B2 §3 owner-resolution chain
jsonb_profile_decision=required_at_birth on birth_registry.jsonb NOT NULL DEFAULT '{}'; catch-all for hook snapshots and policy mirrors
identity_profile_decision=not_birth_responsibility; remain per-entity-kind on physical tables (information_unit, tac_logical_unit); not added centrally
birth_function_patch_required=false
b2_contract_rev3_required=true
b3f_gate_update_required=false (optional hardening only)
backfill_required=partial (jsonb_profile auto-defaults; owner & canonical_address enriched later in B6, not required before 5C2)
ddl_execution_allowed=false
phase5c2_resume_allowed=false
next_recommended_action=GPT_REVIEW_B3_ELD_DESIGN
secrets_redacted=true

B3-ELD Entity Living DB Placeholder Design | DESIGN ONLY | Opus | 2026-05-14

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/design/p3d-birth-entity-living-db-placeholder-design.md