KB-6506

03 · Registry-Driven Axis Binding Design (axis_registry as SSOT; kill the 6-way UNION)

8 min read Revision 1
terminal2registry-drivenaxis-registryaxis-bindingno-hardcodeP02026-06-05

03 · Registry-Driven Axis Binding Design

Area 2 verdict: PARTIAL → effectively MISSING for binding (P0). The contract is view-driven-with-metadata, not registry-driven. axis_registry already holds the binding schema but the contract does not read it to generate nodes.


1. Is the design truly registry-driven?

No. Two independent proofs:

  1. The node universe, routes, child rules, substrate refs, and the job:cut special case are all hardcoded literals inside v_rp_universal_node_ui_contract (the 8239-char 6-way UNION ALL).
  2. The only place axis_registry is read is the reliability decorator: LEFT JOIN axis_registry ar ON ar.axis_code = u.axis_code → used solely to compute source_scope = synthetic_axis|registered_axis. It does not drive node generation.

So the system is registry-aware for one label, view-driven for everything else. Adding an axis today requires editing the UNION view (and, if routes differ, the UI) — the opposite of registry-driven.

2. What axis_registry already gives us (reuse-first, law §5)

axis_registry columns already model most of a binding contract: axis_code, axis_name, domain, node_source, node_filter(jsonb), relation_source, root_rule, child_rule, lifecycle_field, owner_governance_ref, substrate_resolver, pivots_ref, status, approval_ref, notes.

Design decision: extend axis_registry; do NOT create a parallel axis_source_binding table (the prior T2 audit floated axis_source_binding; discover-first/reuse-first overrides — axis_registry is the SSOT and is already governance-owned). A parallel table would split the binding across two governed objects and re-introduce drift.

Missing binding columns to add (additive ALTER, birth-free; governance note below)

New column Purpose Example values
source_kind how nodes are produced DB_VIEW,DB_FN,DB_TABLE,HOST_ADAPTER,SYNTHETIC_DERIVED,CROSS_PRODUCT
governance_class governance status of the axis itself OFFICIAL,CANDIDATE,SYNTHETIC,DERIVED
route_prefix UI route base (kills hardcoded prefixes) /registries-pivot/node/AX-BASE
count_semantics additive contract for the axis's counts ADDITIVE,NON_ADDITIVE,PARTIAL,MIXED
reliability_source which lane/detector feeds reliability v_rp_axis_lane_split:AX-BASE
child_grouping_policy_ref grouping threshold source rp_grouping_policy:default
ui_render_kind generic render hint tree,grouped_list,leaf_list,derived_cross
derived_from for DERIVED/CROSS axes, the parent axes ["AX-PROCESS","AX-TRIGGER"] (AX-PXT)

Governance caveat: axis_registry is an owner-governed table (Điều 32/39 — "AI proposes candidates only; never auto-active"). Adding columns (DDL) is engineering and birth-free; adding rows for new axes is a candidate proposal (status=CANDIDATE/SYNTHETIC), never auto-ACTIVE. See doc 06 for the synthetic-axis rows.

3. Target architecture — generic node generator

Replace the 6-way UNION with a registry-driven dispatcher:

axis_registry (rows, one per axis: AX-BASE, AX-PROCESS, AX-PXT, AX-TOPIC, AX-TRIGGER, …)
        │  node_source, node_filter, child_rule, substrate_resolver, route_prefix, governance_class, count_semantics …
        ▼
fn_rp_axis_nodes(axis_code)   -- generic; reads the registry row and resolves nodes for that axis
        │  dispatches to the axis's source view/fn named in node_source / substrate_resolver
        ▼
v_rp_universal_node_base       -- UNION over fn_rp_axis_nodes(axis) for axis IN (SELECT axis_code FROM axis_registry WHERE governance_class <> 'RETIRED')
        ▼
+ grouping decorator (reads child_grouping_policy_ref → rp_grouping_policy.threshold, NOT a hardcoded 50)
+ reliability decorator (joins lane_split/detectors per reliability_source)
        ▼
v_rp_universal_node_ui_contract_current

The UNION over axes is the only set-level construct, and it iterates axis_registry — so the list of axes is data, not code. Per-axis specifics (which view/function produces the nodes, the substrate resolver) are named in registry columns and invoked by the dispatcher.

What stays "axis-specific" (and that's acceptable)

  • The substrate resolver functions (fn_process_node_substrate, fn_topic_node_substrate, a new fn_base_node_substrate, etc.) remain per-domain PG functions — they are the adapter layer, analogous to source adapters in doc 08. They are referenced by name from the registry (substrate_resolver column), not inlined in the contract view.
  • Adding an axis with an existing source shape (another taxonomy-like axis) = registry row only, no new function.
  • Adding an axis with a new source shape = registry row + one new resolver function. Never a contract-view edit, never a UI edit.

4. What remains hardcoded after the refactor (and the plan)

Hardcode today After refactor Residual
6 axis-code literals in UNION iterate axis_registry none
route prefixes (/…/AX-PXT) route_prefix column none
job:cut special branch (job_kind ~~ 'cut.%') folded into AX-PROCESS resolver / node_filter resolver-internal (named in registry)
grouping threshold (count>1 / 50) rp_grouping_policy.threshold via child_grouping_policy_ref none
has_children=false hardcoded on PXT computed from grouping surface none
substrate resolver fn names substrate_resolver column function bodies remain per-domain (adapter layer, acceptable)

5. "Add a new axis without UI edit" — acceptance scenario

A new axis AX-EXAMPLE:

  1. Owner (or AI as candidate) INSERT INTO axis_registry (axis_code, node_source, node_filter, child_rule, substrate_resolver, route_prefix, governance_class='CANDIDATE', …).
  2. If node_source points to an existing-shaped view/fn → done: nodes appear in _current, grouping/reliability/invariant all apply generically, UI renders it via the generic drill renderer (doc 09) with a CANDIDATE badge.
  3. If it needs a new substrate shape → add fn_axample_node_substrate, name it in substrate_resolver. Still no contract-view edit, no UI edit.

This is the test of "truly registry-driven": new axis = data + (optional) one resolver fn; never view/UI surgery.

6. Classification

Issue Class Severity
Contract not registry-driven (hardcoded 6-way UNION) ARCHITECTURE_GAP P0
Route prefixes / grouping threshold / job:cut hardcoded ARCHITECTURE_GAP P0
axis_registry missing binding columns (source_kind, governance_class, route_prefix, count_semantics, …) ARCHITECTURE_GAP P1
Synthetic axes not present as registry rows GOVERNANCE_BLOCKED (rows) / DATA_DEBT P1

7. Required technical spec for T1 (Area 2)

  1. ALTER TABLE axis_registry ADD COLUMN the 8 binding columns (§2) — additive, nullable, birth-free. Backfill AX-TOPIC/AX-PROCESS.
  2. Propose the 3 synthetic rows as CANDIDATE/SYNTHETIC (doc 06) — governance_class set, status not ACTIVE.
  3. Build fn_rp_axis_nodes(axis_code) generic dispatcher + per-axis resolver references; build v_rp_universal_node_base as UNION over SELECT axis_code FROM axis_registry.
  4. Repoint v1 base internals (or build a new base view) to be registry-iterated; keep output columns byte-identical to today's v1 contract (surgical-drift only — law §4G) so v2/reliability/_current inherit unchanged.
  5. Move grouping threshold read to rp_grouping_policy; move route prefixes to route_prefix.
  6. Acceptance: _current still returns 87 nodes with identical node_codes; adding a test registry row makes a new axis appear in _current with zero view/UI edits; grep shows no axis-code literal in the base view except the registry iteration.
Back to Knowledge Hub knowledge/dev/reports/architecture/parallel-terminal2-rp-canonical-contract-design-alignment-technical-spec-2026-06-05/03-registry-driven-axis-binding-design.md