03 · Registry-Driven Axis Binding Design (axis_registry as SSOT; kill the 6-way UNION)
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:
- The node universe, routes, child rules, substrate refs, and the
job:cutspecial case are all hardcoded literals insidev_rp_universal_node_ui_contract(the 8239-char 6-wayUNION ALL). - The only place
axis_registryis read is the reliability decorator:LEFT JOIN axis_registry ar ON ar.axis_code = u.axis_code→ used solely to computesource_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_registryis 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 newfn_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_resolvercolumn), 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:
- Owner (or AI as candidate)
INSERT INTO axis_registry (axis_code, node_source, node_filter, child_rule, substrate_resolver, route_prefix, governance_class='CANDIDATE', …). - If
node_sourcepoints 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. - If it needs a new substrate shape → add
fn_axample_node_substrate, name it insubstrate_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)
ALTER TABLE axis_registry ADD COLUMNthe 8 binding columns (§2) — additive, nullable, birth-free. Backfill AX-TOPIC/AX-PROCESS.- Propose the 3 synthetic rows as
CANDIDATE/SYNTHETIC(doc 06) —governance_classset,statusnotACTIVE. - Build
fn_rp_axis_nodes(axis_code)generic dispatcher + per-axis resolver references; buildv_rp_universal_node_baseasUNIONoverSELECT axis_code FROM axis_registry. - 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.
- Move grouping threshold read to
rp_grouping_policy; move route prefixes toroute_prefix. - Acceptance:
_currentstill returns 87 nodes with identicalnode_codes; adding a test registry row makes a new axis appear in_currentwith zero view/UI edits;grepshows no axis-code literal in the base view except the registry iteration.