05 — Axis Storage Build-Ready Design (PG-first, CREATE-TABLE level)
05 — Axis Storage Build-Ready Design (PG-first, CREATE-TABLE level)
Package:
one-roof-axis-auth-proposal-operational-hardening-build-ready-design-2026-06-02Mode: DESIGN ONLY · READ-ONLY · NO COMMIT · NO MUTATION · paper DDL only Hardens: prior…-2026-06-01/04-axis-storage-model-pg-first.md. Closes GPT review gap #2 (storage half): exact CREATE-TABLE-level design foraxis_registry+axis_assignment, compatible withtaxonomy/taxonomy_facets/entity_labels/iu_relation/KG tables. Substrate names: SB-AXIS-1 =axis_registry, SB-AXIS-2 =axis_assignment.
5.0 Storage principle & the 3 zones
PG is truth and runtime. Three storage zones (NĐ-36-01 MT4 model — see §5.7 citation flag):
- Approved / born — official truth; in
taxonomy(active) /axis_registry/entity_labels(approved) / IUs; UI-visible. - Candidate — proposed, under review; in
axis_assignment(zone='candidate')/taxonomy(status='candidate'); AI-visible, NOT official UI. - Quarantine — missing provenance / failed input gate; in
axis_assignment(zone='quarantine'); never promoted, governed as input-quality.
Only two new tables (plus the registry-generalization of the existing envelope). Everything else reuses live substrate.
5.1 axis_registry (SB-AXIS-1) — column-level design (paper DDL)
Generalizes taxonomy_facets; each existing facet projects to one axis_registry row of axis_family='label' (lossless — §5.4).
| Column | Type | Null | Default | Constraint / notes |
|---|---|---|---|---|
axis_code |
text | NOT NULL | — | PK, e.g. AX-TOPIC. |
axis_name |
text | NOT NULL | — | Display name. |
axis_family |
text | NOT NULL | — | CHECK ∈ (structural,label,semantic,pivot,system). |
axis_kind |
text | NOT NULL | — | CHECK ∈ (deterministic,uncertain,system). Drives behaviour (doc 04 §4.2). |
owner_scope_ref |
text | NOT NULL | — | → governance_responsibility_scope (SB-2). Owner-per-scope. |
scope |
text | NOT NULL | 'global' |
CHECK ∈ (global,domain-local,object-local). |
vocabulary_source |
text | NULL | — | Where node values come from (e.g. taxonomy:FAC-08, information_unit, computed). |
node_kind |
text | NOT NULL | — | CHECK ∈ (taxonomy_node,information_unit,born_entity). Polymorphic node home. |
relation_store |
text | NOT NULL | — | CHECK ∈ (iu_relation,universal_edges,taxonomy_parent,entity_relations,none). |
assignment_store |
text | NOT NULL | — | CHECK ∈ (entity_labels,iu_metadata_tag,axis_assignment,iu_parent_ref,intrinsic). |
grouping_policy_ref |
text | NULL | — | Grouping/DOT policy row (Điều 35). |
coverage_rule_ref |
text | NOT NULL | — | → coverage rule row (confidence threshold, cardinality, coverage expectation). No literal thresholds. |
issue_path_ref |
text | NOT NULL | — | → event_type_registry (event_domain='governance') issue family for this axis. |
projection_view |
text | NULL | — | Named view the UI reads (doc 10). |
lifecycle_status |
text | NOT NULL | 'register' |
CHECK ∈ (birth,register,active,deprecate,retire). |
cardinality |
text | NOT NULL | 'multiple' |
CHECK ∈ (single,multiple). Carried from taxonomy_facets.cardinality. |
max_labels_per_entity |
integer | NOT NULL | 0 |
0 = not operationalized (matches FAC-08 today). |
created_by / created_at |
text / timestamptz | NOT NULL | now() |
Provenance. |
Constraints: PK(axis_code); CHECK enums above; FK-soft owner_scope_ref/coverage_rule_ref/issue_path_ref (soft text refs until SB-2/SB-11 exist, to avoid ordering deadlock; hardened to FK when those tables land). Index on (axis_family,axis_kind), (lifecycle_status).
Born: an axis_registry row of lifecycle_status='active' is a born governed object (Birth row + owner + coverage).
5.2 axis_assignment (SB-AXIS-2) — column-level design (paper DDL)
The confidence/zone/evidence-bearing assignment table for uncertain axes. (Deterministic axes use intrinsic stores and write no axis_assignment row.)
| Column | Type | Null | Default | Constraint / notes |
|---|---|---|---|---|
id |
uuid | NOT NULL | gen_random_uuid() |
PK. |
axis_code |
text | NOT NULL | — | FK → axis_registry.axis_code. |
entity_kind |
text | NOT NULL | — | CHECK ∈ (information_unit,born_entity,collection). |
entity_collection |
text | NOT NULL | — | The collection name (e.g. information_unit). Replaces the prior single entity_ref — see §5.3 key reconciliation. |
entity_code |
text | NOT NULL | — | The entity's code (matches entity_labels.entity_code / birth_registry.entity_code). |
entity_uuid |
uuid | NULL | — | Optional native id for IU rows (information_unit.id). |
node_ref |
text | NOT NULL | — | FK → taxonomy.code (the axis node). |
zone |
text | NOT NULL | 'candidate' |
CHECK ∈ (approved,candidate,quarantine). The governance state. |
match_score |
numeric(5,4) | NULL | — | Raw matcher score (similarity). Distinct from confidence (NĐ-36-01 MT4: never collapse score into approval state). |
confidence |
numeric(5,4) | NULL | — | Calibrated confidence; < coverage_rule threshold ⇒ stays candidate. |
evidence |
jsonb | NULL | — | [{kind, ref, weight}]. |
provenance |
jsonb | NULL | — | {source, source_authority, extraction_method, resolved_by, ts}. NULL ⇒ zone forced quarantine (CHECK or promote-guard). |
assigned_by |
text | NOT NULL | — | CHECK ∈ (dot,agent,human,rule). |
rule_ref |
integer | NULL | — | FK → label_rules.id (when rule-derived). |
review_decision_ref |
text | NULL | — | → approval_requests.code / doc_reviews (when reviewed). |
valid_time |
tstzrange | NOT NULL | tstzrange(now(),NULL) |
Bitemporal validity (matches iu_relation/universal_edges). |
revision |
integer | NOT NULL | 1 |
Bumped on supersession. |
superseded_by |
uuid | NULL | — | Self-FK to the replacing row. |
status |
text | NOT NULL | 'active' |
CHECK ∈ (active,superseded,retracted). |
created_by / created_at |
text / timestamptz | NOT NULL | now() |
— |
Constraints/indexes:
- PK(
id); FK(axis_code)→axis_registry; FK(node_ref)→taxonomy(code); FK(rule_ref)→label_rules(id); self-FK(superseded_by). - CHECK
provenance IS NOT NULL OR zone='quarantine'(provenance-or-quarantine). - CHECK
zone='approved'only set by the promotion txn (enforced operationally + a guard: agents may not writezone='approved', doc 02/06). - Partial UNIQUE
uq_axis_assignment_active:UNIQUE (axis_code, entity_collection, entity_code, node_ref) WHERE status='active'— one active assignment per (axis,entity,node). - For
single-cardinality axes, an additional partial-unique on (axis_code,entity_collection,entity_code) WHEREstatus='active' AND zone='approved'(one approved node per entity per single-axis). - Indexes: (
axis_code,zone), (entity_collection,entity_code), GIN onprovenance/evidence.
5.3 Key reconciliation — the canonical_address-NULL fix (named inconsistency)
Live fact (doc 01 §1.5): birth_registry.canonical_address is NULL in all 1,069,055 rows; information_unit.canonical_address exists but is unreliable as a universal key. The prior doc 04 wrote axis_assignment.entity_ref = 'canonical_address/code/uuid' — inconsistent with the live key.
Hardening: replace the single entity_ref with entity_collection + entity_code (+ optional entity_uuid), exactly matching the live addressing used by entity_labels.entity_code and birth_registry(collection_name, entity_code). The canonical candidate key is therefore:
candidate_key := entity_collection || ':' || entity_code -- NOT canonical_address
This aligns axis_assignment with the SB-10 candidate key COALESCE(canonical_address, collection_name||':'||entity_code) (which, since canonical_address is always NULL, evaluates to collection_name:entity_code). One key, used by assignment, coverage, and promotion. No design anywhere keys off canonical_address.
5.4 Compatibility — taxonomy_facets/taxonomy/entity_labels (build-ready)
| Live table | Relationship to new tables | Migration / compatibility |
|---|---|---|
taxonomy_facets (10) |
each facet → one axis_registry row (axis_family='label', assignment_store='entity_labels' for deterministic, axis_assignment for uncertain) |
lossless projection, additive: taxonomy_facets is kept; axis_registry references it via vocabulary_source='taxonomy:FAC-NN'. No ALTER of taxonomy_facets. |
taxonomy (58) |
the node store for label/semantic axes; node_ref FKs here |
unchanged; topic candidates added as status='candidate' rows under the ladder. |
entity_labels (787,723) |
the materialized approved projection for label axes | kept as-is (no ALTER). For uncertain axes (topic), axis_assignment is the SoT and entity_labels is reconciled from it on approval (§5.5). For deterministic label facets, entity_labels+label_rules remain authoritative; no axis_assignment rows. |
iu_metadata_tag (confidence-bearing) |
overlaps axis_assignment |
fold-in over time (a patch, doc 12): new uncertain-axis tags go to axis_assignment; existing iu_metadata_tag rows read-compat via a union view until migrated. No big-bang migration. |
label_rules (37) |
deterministic rule engine | unchanged; axis_assignment.rule_ref FKs here when a candidate is rule-derived. |
Why not just extend entity_labels? entity_labels has no confidence/evidence/zone/valid_time/provenance columns and is 787k rows at scale; adding nullable governance columns to it (ALTER) is both a migration risk and would mix approved truth with candidates. Keeping it as the approved projection and adding axis_assignment as the candidate/SoT layer is additive and reversible (the prior doc 04 recommendation, now hardened).
5.5 The reconcile path axis_assignment ⇄ entity_labels (concrete mechanism)
GPT "key claim to verify": the reconcile must be concrete. Promotion txn (doc 16) for a topic assignment:
-- inside the L3-authorized promotion transaction:
UPDATE axis_assignment SET zone='approved', review_decision_ref=:apr,
valid_time = tstzrange(lower(valid_time), NULL)
WHERE id = :assignment_id; -- candidate → approved
INSERT INTO entity_labels (entity_code, label_code, assigned_by, rule_id, assigned_at)
VALUES (:entity_code, :node_ref, 'axis_promote', NULL, now())
ON CONFLICT (entity_code, label_code) DO NOTHING; -- materialize approved projection
-- retraction reverses: zone='retracted' + DELETE/expire the entity_labels row
A daily reconcile detector (doc 09) flags drift: an entity_labels row with no backing axis_assignment(zone='approved') for an uncertain axis (label_orphan), or an approved assignment missing its entity_labels projection (projection_lag). Deterministic facets are exempt (entity_labels is their SoT).
5.6 Relations, multi-parent, and graph (reuse, no new relation table)
- Containment / multi-parent:
iu_relation(relation_type='contains') is a graph (an IU may have multiple parents);iu.parent_or_container_refnames the primary parent;iu_tree_pathis the closure projection (tree). Graph = truth, tree = projection (doc 07). - KG / cross-collection:
universal_edges(+v_kg_edges_all) carries typed weighted edges withconfidence/valid_time/provenancealready. entity_relations(decreed for NĐ-36-01 soft relations, unbuilt): do NOT build now. Interim, soft relations rideuniversal_edges(withedge_subtype).axis_registry.relation_store='entity_relations'is a declared-but-deferred option; building it is a future step (doc 12 Class 4) only ifuniversal_edgesproves insufficient. Confidence/evidence/provenance for relations are already native toiu_relation/universal_edges— no new columns needed.
5.7 Confidence / evidence / provenance / ruleset / snapshot storage (where each lives)
| Dimension | Storage |
|---|---|
match_score (raw) vs confidence (calibrated) |
separate columns on axis_assignment (uncertain) / iu_metadata_tag / universal_edges.confidence (relations) |
evidence |
jsonb on axis_assignment/iu_relation/universal_edges |
provenance |
jsonb on axis_assignment/iu_relation/universal_edges (NULL ⇒ quarantine) |
ruleset_version |
SB-12 governance_ruleset (canonical hash over enabled measurement_registry(142) ⊕ profile/axis/scope) |
snapshot / fingerprint |
SB-12 reuse of evolution_snapshots (per-group fingerprint in metrics jsonb) |
| coverage verdict (decaying) | SB-10 governance_candidate_state, keyed (candidate_key, snapshot, ruleset_version, scan_time) — no checked-forever boolean |
Citation flag (NĐ-36-01): the 3-zone (approved/candidate/quarantine) and score-vs-state separation are attributed in prior docs to "NĐ-36-01 MT4". State recovery (doc 00) found no distinct decree "NĐ-36-01" in the KB; the closest is Điều 36 (referenced by Điều 37) and the soft-relation/alias model. The design stands on its own (it is sound regardless of citation), but the citation must be verified before any law cross-reference is written (doc 12 patch L3).
5.8 Open decisions
- O-STORE-1:
iu_metadata_tagfold-in timing (read-compat union view vs migrate). - O-STORE-2: whether AX-EXPERTISE/AX-AUDIENCE adopt
axis_assignmentor stayentity_labels-native (doc 04 O-AXIS-4). - O-STORE-3: FAC-08 cardinality/max (doc 07) — affects the single-axis partial-unique.
- O-STORE-4: build
entity_relationsnow or defer (recommend defer; reuseuniversal_edges).
Forbidden-compliance: paper DDL only; nothing created/altered; no row written; read-only audit only.