KB-2736

P2B-P0 — IU/UV Schema Inspection Report

12 min read Revision 1
p2bp0schemaiuunit_versionread-only

P2B-P0 — IU/UV Schema Inspection Report

Date: 2026-05-05 | Mode: READ-ONLY | Status: COMPLETE Source: live PG inspection on VPS 38.242.240.89 (docker exec -i postgres psql -U directus -d directus) Controlling prompt: P2B-P0 rev2


§3.1 Actual columns

information_unit (15 cols)

# column type nullable default INSERT requirement
1 id uuid NO gen_random_uuid() optional
2 canonical_address text NO REQUIRED
3 unit_kind text NO REQUIRED (vocab-checked)
4 lifecycle_status text NO 'draft' optional
5 content_anchor_ref text YES REQUIRED at COMMIT (L2)
6 version_anchor_ref uuid YES REQUIRED at COMMIT (L2, FK→unit_version, DEFERRABLE)
7 owner_ref text NO REQUIRED
8 parent_or_container_ref uuid YES optional
9 conformance_status text NO 'open' optional
10 identity_profile jsonb NO '{}' REQUIRED non-empty (title, owner_lookup_ref, primary_section_type_ref by L1)
11 created_at timestamptz NO now() optional
12 updated_at timestamptz NO now() optional (auto via trg_iu_updated_at)
13 created_by text NO REQUIRED
14 updated_by text NO REQUIRED
15 deleted_at timestamptz YES optional

NOT NULL without default → must be in payload: canonical_address, unit_kind, owner_ref, created_by, updated_by. Plus identity_profile defaults to {} but L1 trigger requires non-empty fields, so effectively REQUIRED with content.

unit_version (9 cols)

# column type nullable default INSERT requirement
1 id uuid NO gen_random_uuid() optional
2 unit_id uuid NO REQUIRED (FK→information_unit)
3 body text NO REQUIRED
4 content_hash text NO REQUIRED
5 version_seq integer NO REQUIRED
6 lifecycle_status text NO 'draft' optional
7 content_profile jsonb NO '{}' optional
8 created_at timestamptz NO now() optional
9 created_by text NO REQUIRED

NOT NULL without default → must be in payload: unit_id, body, content_hash, version_seq, created_by.


§3.2 Constraints summary

information_unit

  • PK: information_unit_pkey on (id)
  • UNIQUE: information_unit_canonical_address_key on (canonical_address)
  • FK: fk_iu_version_anchorunit_version(id) DEFERRABLE INITIALLY DEFERRED
  • Constraint trigger: trg_iu_birth_gate_layer2 (DEFERRABLE INITIALLY DEFERRED) — L2 birth gate

unit_version

  • PK: unit_version_pkey on (id)
  • UNIQUE: uq_unit_version_seq on (unit_id, version_seq)
  • FK: unit_version_unit_id_fkeyinformation_unit(id) (NOT deferrable)

FK references INTO IU/UV (Q11)

Inbound (public schema relevant):

  • information_unit.version_anchor_refunit_version(id) (DEFERRABLE)
  • unit_version.unit_idinformation_unit(id)

Sandbox-only refs (irrelevant to pilot): sandbox_tac.change_set_member, sandbox_tac.publication_member.

Circular FK between IU and UV is resolved via DEFERRABLE on fk_iu_version_anchor — must INSERT inside a transaction with SET CONSTRAINTS DEFERRED or rely on default-deferred behavior.


§3.3 Triggers summary

information_unit (4 triggers)

trigger timing function role
trg_iu_birth_gate_layer1 BEFORE INSERT fn_iu_birth_gate_layer1 row-level field + vocab validation
trg_birth_information_unit AFTER INSERT fn_birth_registry_auto('__birth_synthetic_id__') auto-write into birth_registry
trg_iu_birth_gate_layer2 AFTER INSERT/UPDATE (CONSTRAINT, DEFERRED) fn_iu_birth_gate_layer2 commit-time anchor consistency
trg_iu_updated_at BEFORE UPDATE fn_iu_updated_at maintains updated_at

unit_version

0 triggers. No birth trigger on UV (consistent with birth_code_strategy='subordinate').

Birth-gate logic extracted (operational impact on INSERT):

  • L1 (BEFORE INSERT) rejects unless ALL of:
    • canonical_address, unit_kind, owner_ref non-empty
    • lifecycle_status, conformance_status non-NULL
    • unit_kind exists as dot_config key vocab.unit_kind.<value>
    • identity_profile->>'title' non-empty
    • identity_profile->>'owner_lookup_ref' non-empty
    • identity_profile->>'primary_section_type_ref' non-empty AND in dot_config vocab.section_type.<value>
    • identity_profile->>'publication_type_ref' (if present) must be in dot_config vocab.publication_type.<value>
    • WARNING-only (PILOT): publication_authority_ref, publication_type_ref missing — production will block
  • L2 (deferred AFTER) rejects unless:
    • content_anchor_ref non-empty
    • version_anchor_ref non-NULL
    • version_anchor_ref points to a unit_version row with unit_id = NEW.id
    • content_anchor_ref = version_anchor_ref::text

Implication: IU pilot INSERT must be done in a single transaction with the matching UV row, with SET CONSTRAINTS ALL DEFERRED (or rely on default deferred), and content_anchor_ref must equal the UV uuid as text.


§3.4 Current counts (baselines)

table count expected
information_unit 0 0 ✅
unit_version 0 0 (baseline captured)
birth_registry (collection_name=information_unit) 0 0 ✅
birth_registry (collection_name=unit_version) 0 0 (baseline captured)

All four tables are empty. Clean slate for P2B-P1 pilot.


§3.5 Collection metadata (collection_registry)

collection governance_role birth_code_strategy birth_code_column birth_identity_source description_policy
information_unit observed synthetic_id (null) manual structured_exempt
unit_version observed subordinate (null) manual structured_exempt

Notes:

  • IU uses synthetic_id strategy → birth code = __birth_synthetic_id__ sentinel passed to trigger; identity captured from identity_profile.
  • UV is subordinate → no own birth row (consistent with 0 birth triggers on UV).
  • description_policy=structured_exempt → no description backfill required from PROV-HUMAN gate.

§3.6 Audit invariants

invariant observed note
fn_birth_registry_auto md5 1f729b3571a74963089bb3ef388217f3 record for drift detection
trg_birth_* count (NOT internal) 31 governance baseline

§3.7 RLS status

  • pg_policies for IU/UV: 0 policies.
  • relrowsecurity = false, relforcerowsecurity = false for both tables.
  • (Note: a sandbox copy of unit_version appears in pg_class; public schema rows are RLS-off.)

Impact on INSERT: none. Direct PG inserts as directus role unaffected.


§3.8 Minimum INSERT payload — candidate fields

Hard-required by NOT-NULL-without-default

information_unit:

  • canonical_address (text, UNIQUE)
  • unit_kind (text, must match vocab.unit_kind.<value> in dot_config)
  • owner_ref (text)
  • created_by (text)
  • updated_by (text)

unit_version:

  • unit_id (uuid → IU.id)
  • body (text)
  • content_hash (text)
  • version_seq (integer; UNIQUE per unit_id)
  • created_by (text)

Hard-required by birth-gate L1 (effectively required even though identity_profile has default)

identity_profile JSONB must contain at minimum:

{
  "title": "<non-empty>",
  "owner_lookup_ref": "<non-empty>",
  "primary_section_type_ref": "<value with matching vocab.section_type.<value> row in dot_config>"
}

Optional but warned: publication_authority_ref, publication_type_ref.

Hard-required by birth-gate L2 (commit-time)

  • version_anchor_ref = UV row id
  • content_anchor_ref = version_anchor_ref::text
  • The referenced UV row must have unit_id = IU.id

Default-able / can be omitted

IU: id, lifecycle_status, conformance_status, identity_profile (but see L1), created_at, updated_at, parent_or_container_ref, deleted_at UV: id, lifecycle_status, content_profile, created_at

Proposed minimum valid payload shape (single transaction)

BEGIN;
SET CONSTRAINTS ALL DEFERRED;

WITH iu AS (
  INSERT INTO information_unit (
    id, canonical_address, unit_kind, owner_ref,
    created_by, updated_by, identity_profile
  ) VALUES (
    gen_random_uuid(),
    'iu/pilot/0001',
    '<unit_kind_in_vocab>',
    '<owner_ref>',
    '<actor>',
    '<actor>',
    jsonb_build_object(
      'title','Pilot IU 0001',
      'owner_lookup_ref','<owner_lookup>',
      'primary_section_type_ref','<section_type_in_vocab>'
    )
  ) RETURNING id
), uv AS (
  INSERT INTO unit_version (
    id, unit_id, body, content_hash, version_seq, created_by
  )
  SELECT
    gen_random_uuid(), iu.id,
    '<body>', '<sha256-or-similar>', 1, '<actor>'
  FROM iu
  RETURNING id, unit_id
)
UPDATE information_unit
SET version_anchor_ref = uv.id,
    content_anchor_ref = uv.id::text
FROM uv
WHERE information_unit.id = uv.unit_id;

COMMIT;  -- L2 fires here, then trg_birth_information_unit on the IU insert (AFTER INSERT, already fired immediately)

Note: trg_birth_information_unit is AFTER INSERT (NOT deferred), so it fires immediately after the IU INSERT, before UV exists. This is fine because the birth trigger only writes to birth_registry from identity_profile and does not require anchors. The L2 deferred constraint trigger handles anchor consistency at commit.


§3.9 Risks / blockers before P2B-P1

  1. vocab dependencies (BLOCKING): Need confirmed values for:
    • dot_config key vocab.unit_kind.<X> — pick a valid unit_kind
    • dot_config key vocab.section_type.<Y> — pick a valid primary_section_type_ref
    • (optional) vocab.publication_type.<Z> if publication_type_ref is set → P2B-P1 must query these before crafting payload.
  2. Circular FK timing: Requires single-tx flow with deferred constraints, or NULL-then-UPDATE sequence as drafted above. Cannot do plain two separate inserts.
  3. content_anchor_refversion_anchor_ref equality: Hard-coded equality check (text == uuid::text). Don't deviate.
  4. Birth-trigger AFTER-INSERT ordering: Birth registry row is written from identity_profile snapshot at IU INSERT time — make sure profile is final before INSERT (not patched later).
  5. canonical_address UNIQUE: Pilot must use a distinct address.
  6. owner_ref / owner_lookup_ref: Format/vocabulary not enforced by trigger but expected by downstream registries — confirm shape with GPT/User before P1.
  7. No PROV-HUMAN backfill needed: description_policy=structured_exempt.
  8. No RLS: Direct PG insert path is unblocked at row-security layer.

Non-issues observed:

  • Empty UV table is consistent with empty IU; no orphaned UVs to migrate.
  • 31 birth triggers is consistent with prior governance baseline; not abnormal.

§3.10 Hard-stop confirmation

P2B-P0 COMPLETE. HARD STOP. Chờ GPT/User review trước P2B-P1.

No DDL executed. No INSERT/UPDATE/DELETE executed. No row mutations. Read-only queries only.


Generated 2026-05-05 by Opus 4.7 (Claude Code) via P2B-P0 rev2 prompt. 12 PG queries + 2 supplementary function-def reads for trigger logic capture.