KB-1698

17 — SB-2 Object/Axis Ownership Edge — Detailed Technical Design (T4, design-only, 2026-06-01)

19 min read Revision 1
one-roof-governanceimplementation-indexsb-2c-1track-t4governance-object-ownershipresponsibility-scopeowner-per-scopecontainer-grain-inheritanceno-migration-riskadditive-tableresolution-viewno-hardcodeno-islanddesign-only2026-06-01

17 — SB-2 Object/Axis Ownership Edge — Detailed Technical Design

Track: T4 (doc 04). Scaffold: doc 06. Pairs with council decision C-1. Tier: detailed technical design, uncommitted (contract, not DDL-to-commit). Mutation footprint: none (KB doc only); zero PG/Directus/Qdrant/Nuxt/law/approval mutation; no DDL committed; the relations CHECK is not widened. Evidence base: doc 18 (live read-only PG, 2026-06-01). Concept base: canon 01 (M-DEF-1/2 governed-object, M-DEF-3 responsibility-scope/owner-per-scope, M-DEF-7 grain + invariant v3 + owner-link-only inheritance), canon 02 (M-DEF-8/9 axis-as-object + Axis Registry). House law: no-hardcode, Design-Only Macro Mode, reversible-by-default. Blocker status: SB-2 OPEN. This doc designs the ownership substrate completely enough for implementation review; it does not create the table or any row.


0. The question SB-2 answers

Governance must express who is the accountable owner of a governed object or axis, per responsibility scope (M-DEF-3). Today this is impossible: governance_relations admits only law/agency endpoints (doc 18 §2.1), so an object (a pivot, a collection, an IU class, an axis) cannot be a relation endpoint. SB-2 = add the missing fourth leg of relational ownership (object/axis → owner-agency), as additive substrate that scales to 10⁸ children with Δtotal = 0 and adds no migration risk to the 8 live agency→law edges.


1. Pattern decision — additive table + resolution view (HYBRID), NOT CHECK-widen, NOT view-only

The mission asked us to choose among: new table / extend the relation table / view over existing tables / hybrid.

Candidate Verdict Why
Widen governance_relations CHECK to admit object/axis REJECTED (a) ALTER on a live CHECK = migration touching the 8 edges + 2 validate triggers; (b) the relations table has no scope, owner_kind, effective_from/to, lifecycle, approval_ref columns → many further ALTERs; (c) UNIQUE(source_code,target_code,relation_type) cannot express one-accountable-owner-per-scope; (d) it conflates a different grain (agency↔law contract edges vs object→agency scoped ownership). High risk, poor fit.
View only (compute ownership from existing tables) REJECTED as storage There is no existing table that stores object→owner; a view has nothing to read. (A view is, however, the right tool for resolution — see below.)
Extend a domain table with owner_gov_code columns REJECTED Violates the system-wide relational-ownership decision (no table carries owner_gov_code; canon M-DEF-2). Bolting columns onto 169 catalog classes = anti-pattern + per-row ownership.
New additive table governance_object_ownership + a resolution VIEW CHOSEN (hybrid) The table is the authoritative storage of owner rows at the governance grain; the view computes the effective owner for any object (incl. owner-link inheritance). Greenfield (table absent live), zero migration risk, expresses scope/owner_kind/dates/lifecycle natively. This is the C-1 recommended default.

HYBRID = governance_object_ownership (storage) + v_object_effective_owner (resolution view) + reuse of registry_changelog/event_outbox (audit/event). Plus one tiny additive reference table for the 6 law-defined scopes (no-hardcode, §3).


2. governance_object_ownership — table contract (NOT DDL to commit)

A row = one owner link for (object × scope) of a given owner_kind, time-boxed and lifecycled. Contract:

column type null meaning / rule
id bigint PK no surrogate
object_type text no governed-object class — resolves against a governed registry (coverage-profile class catalog / meta_catalog.entity_type), not an enum (no-hardcode §6). 'axis' is one value (M-DEF-8).
object_ref text no the specific object's code/id, resolvable in that class's registry_collection (from meta_catalog); for axes = axis_code (Axis Registry).
scope text no FK → governance_responsibility_scope(scope_code) — the 6 law-defined scopes (§3).
owner_kind text no accountable | supporting | delegated | exception (CHECK or FK to a small ref). Exactly one active accountable per (object_type,object_ref,scope) (§4); supporting/delegated may be many.
owner_gov_code text no FK → governance_registry(code) (reuses the live registry; the agency must exist).
is_inherited_anchor bool no true if this row is a container/root anchor whose owner-link inherits to descendants (M-DEF-7); default per grain rule (§5).
effective_from timestamptz no start of validity (default now()).
effective_to timestamptz yes end of validity; mandatory & future when owner_kind='delegated' (TTL); null = open-ended for accountable.
lifecycle_status text no active | superseded | revoked | expired (CHECK). Mirrors governance_registry.status discipline.
approval_ref text yes FK/soft-ref → approval_requests(code) — the APR that authorised this row (SB-1 assign_governance_owner / delegate_authority / assign_axis_owner). Required for any active accountable/delegated row (no owner without an approval).
audit_ref text yes soft-ref → registry_changelog(code) row written on assign/supersede.
rollback_ref text yes how to revert (prior owner / end-date plan).
source_law_ref text yes FK-soft → normative_registry(code) — the law that makes this object governed (e.g. NRM-LAW-37 hub, or the specialized law).
source_design_ref text yes KB path of the design that introduced the object/owner (provenance).
supersedes_id bigint yes self-FK → the owner row this one replaces (audit chain; never hard-delete).
created_at/by, updated_at/by standard provenance (matches IU/registry convention).

Indexes:

  • PRIMARY KEY (id).
  • Partial UNIQUE (object_type, object_ref, scope) WHERE owner_kind='accountable' AND lifecycle_status='active' — enforces one accountable owner per scope (M-DEF-3) at write time (§4).
  • non-unique (object_type, object_ref) for resolution; (owner_gov_code); (scope, lifecycle_status).

FKs: owner_gov_code → governance_registry(code) (this is stronger than the live governance_relations, which has no FK on its codes — a deliberate integrity improvement); scope → governance_responsibility_scope(scope_code). (approval_ref/source_law_ref may be hard or soft FKs per council; soft-ref avoids cross-schema RESTRICT friction.)


3. governance_responsibility_scope — tiny additive reference table (no-hardcode for scope)

The 6 responsibility scopes are law-defined (Đ37 §4.15-bis / M-DEF-3) but must be data, not a code enum, so a 7th (if law ever adds one) is a row, not DDL. Contract: scope_code (PK) ∈ seed {policy, health, execution, render, approval, audit}, description, default_owner_hint (the OP-B-style default agency, advisory), status. Seeded once, governed (changing it = Đ32). governance_object_ownership.scope FK → here.

Why a table and not a CHECK: a CHECK list is the same hardcode in schema form (adding a scope = ALTER). A reference table keeps "a scope is a governed row." Six rows today; the model is open.


4. Owner uniqueness, conflict & missing-owner detection

  • One accountable owner per scope (uniqueness): the partial UNIQUE index (§2) makes a second active accountable row for the same (object,scope) fail at write. The SB-1 assign_governance_owner handler must therefore supersede (end-date prior, set supersedes_id) within one transaction, never insert a parallel accountable row.
  • Double-owner detection (legacy/repair): a detector view groups WHERE owner_kind='accountable' AND lifecycle_status='active' by (object_type,object_ref,scope) HAVING count(*)>1 → emits governance.owner.conflict (reuse system_issues; SB-4 type). The unique index prevents new doubles; the detector catches any introduced out-of-band.
  • Missing-owner detection (OWNER_GAP): v_object_owner_gap = the governed-object inventory (from meta_catalog + per-class registries, at the governance grain) LEFT JOIN v_object_effective_owner per required scope; rows with no resolvable accountable owner for a profile-mandatory scope → OWNER_GAP (reuse thiếu_quan_hệ). Birth precedence (M-DEF-4): objects not yet born/registered are excluded (deferred to the birth scanner) — no governance OWNER_GAP for an unborn object.
  • These three feed the coverage invariant v3: total = covered + orphans + approved_exceptions + retired/ignored + stale, closing at the governance grain (§5).

5. Inheritance, grain & the 10⁸ scale proof (Δtotal = 0)

Storage grain = governance grain (M-DEF-7): owner rows are stored only at roots + non-inheriting objects + containers. A container/root row has is_inherited_anchor=true.

Resolution view v_object_effective_owner(object_type, object_ref, scope) → owner_gov_code, owner_kind, source_anchor: for a queried object, it returns the object's own owner row if present, else the nearest ancestor anchor's owner row (walking the containment hierarchy) — i.e. owner-link inheritance only.

Anti-hiding rule (non-negotiable, M-DEF-7): only the owner-link inherits. Risk-bearing links — approval-path, rollback, DOT-authority, exception — never inherit. A child that carries its own policy/approval still raises APPROVAL_PATH_GAP even under a covered parent. The resolution view therefore resolves owner_kind='accountable' ownership downward, but owner_kind='exception'/delegated/approval-bearing rows are scoped to their own object only and are not inherited.

Scale proof: adding 10⁶ (or 10⁸) children under an already-anchored container inserts 0 rows into governance_object_ownership (children resolve their owner via the view from the container anchor). Therefore:

  • ownership-row count is independent of child population ⇒ no per-row owner edges;
  • the coverage-invariant total is counted at the governance grain (leaf children inherit, do not count separately) ⇒ Δtotal = 0 when children are added under a covered container;
  • this matches the live anti-spam scale already proven by system_issues.template_gap=183,378 with coalesce_key/occurrence_count (doc 18 §6.3) — the machinery for grain-level aggregation exists.

Test: +10⁶ children under a covered container → 0 new ownership rows, Δtotal=0; a child carrying its own policy → APPROVAL_PATH_GAP still raised (inheritance did not hide it).


6. Object-type / axis vocabulary — no-hardcode

  • object_type resolves against a governed registry — the coverage-profile class catalog (M-DEF-2, itself a Class-2 governed registry) and/or meta_catalog.entity_type (169 rows, live). It is not a hardcoded enum; a new governed-object class is a registry row, and ownership for it works with no schema change.
  • object_ref resolves in that class's registry_collection (from meta_catalog), so the owner link points at a real, birth-registered object (M-DEF-4 precedence).
  • Axis (M-DEF-8) is just object_type='axis', object_ref=axis_code. The axis_code lives (end-state) in the Axis Registry (M-DEF-9); interim it may reference an existing dimension (a pivot_definitions.group_spec dimension, or a classification/pivot law-domain axis). Independent of SB-3 (doc 18 §7): owning an axis needs only this row + an axis identifier — not the generic axis-value store. A 4th, 5th… axis becomes an Axis-Registry row + an ownership row, never DDL here.

7. No migration risk — proof against the 8 live edges

  1. to_regclass('public.governance_object_ownership') = null and governance_responsibility_scope = null (doc 18 §2.2) ⇒ greenfield; no object is renamed/dropped/altered.
  2. The design does not touch governance_relations: its 2 CHECKs (source_type/target_type ∈ {law,agency}), its UNIQUE, its PK, and its 2 triggers (fn_birth_registry_auto_id, fn_relations_validate) are unchanged; the 8 agency→law edges are unchanged (0 rows migrated).
  3. No existing column gains/loses a constraint; no domain table gains an owner_gov_code column.
  4. New FKs point into existing tables (governance_registry, the new scope ref) — they constrain only the new table, never the parents' existing rows (FK validation on an empty child table is a no-op).
  5. Reversible-by-default: until SB-1 Phase-B handlers exist, nothing writes here; the table can be DROPped with zero downstream effect. Rollback = DROP TABLE governance_object_ownership, governance_responsibility_scope + DROP VIEW v_object_effective_owner, v_object_owner_gap.

Migration risk to the live governance substrate = none. The new table sits beside governance_relations, not inside it.


8. Integration summary (per surface)

Surface How SB-2 integrates No-island / no-migration proof
Điều 37 hub the table is the One-Roof object-ownership store Đ37 §4.15-bis references; owner-per-scope realised here single store; Đ37 owns the contract, this is the substrate
Điều 32 / SB-1 written only by SB-1 action-type handlers (assign_governance_owner/delegate_authority/assign_axis_owner) via APR; approval_ref back-links the APR no write path outside the approved spine
governance_registry owner_gov_code FK → governance_registry(code) (reuses 9 agencies; FK is an integrity improvement over relations) reuse, not duplicate
governance_relations untouched (object/axis ownership deliberately not forced into law/agency edges) the 8 edges + CHECKs + triggers unchanged
registry_changelog every assign/supersede/revoke writes a generic entity audit row (entity_type='governance_object_ownership') reuse existing audit (68,323 rows); governance_audit_log stays relation-scoped
event_outbox / event_type_registry emits governance.owner.assigned/.conflict, governance.axis.owner_assigned, governance.authority.delegated/.expiringregistered first under the GOV-SIV governance domain register-before-emit; no unregistered emit
system_issues OWNER_GAPthiếu_quan_hệ, conflict→new SB-4 type, drift→sai_lệch_dữ_liệu reuse existing issue substrate
coverage invariant v3 / scanner (T6) the table + v_object_effective_owner/v_object_owner_gap are the accounting the scanner reconciles; container-grain keeps Δtotal=0 grain-level, never per-row
future Axis Registry / SB-3 axis owned via object_type='axis'; independent of iu_three_axis_envelope axis-as-object decoupled from axis-value store
Registries-Pivot (T9) / IU (T10) a pivot/collection/IU class is just an object_type; IU owner-binding (OP-B) lands here once C-3/H-2 rule one substrate for all governed objects

9. Failure modes

  1. Per-row ownership at scale — writing an owner row per child. Mitigation: container-grain anchors + resolution view (§5); the unique index + handler write only at the grain.
  2. Double accountable owner — two active accountable rows for a scope. Mitigation: partial UNIQUE index (write-time) + detector view (legacy).
  3. Risk-link inheritance hiding a gap — a child inheriting approval/rollback. Mitigation: only owner-link inherits; risk links scoped to own object (§5).
  4. Owner = retired/draft agency — assigning to an inactive agency. Mitigation: handler validates governance_registry.status='active' (except explicit TTL delegation to a draft agency, e.g. GOV-MOUT under C-5, which is owner_kind='delegated' with mandatory effective_to).
  5. Orphan ownership — owning an unborn object. Mitigation: birth precedence (M-DEF-4); v_object_owner_gap excludes unregistered objects.
  6. Local island — a surface writing ownership to a private table. Mitigation: this is the single store; CI dual-channel island scan (canon §5); handler contract pins this table.
  7. Hard delete breaking audit chain — deleting a superseded owner. Mitigation: never delete; supersedes_id chain + lifecycle_status.

10. Apply-readiness gate

Creating the table requires: C-1 ruled (council: new table vs widen — default new table) + the human enact/approval path (H-1) + rehearsal log (doc 19). Writing the first owner row additionally requires: SB-1 resolved (the assign_governance_owner handler exists and is ratified, C-2) + sovereign sign-off path (H-2/SB-6; os_proposal_approvals=0 ⇒ COMMIT_FORBIDDEN) + the governance_responsibility_scope seed (itself a governed insert). IU owner-binding into this table additionally needs OP-B (C-3)+C-4. No gate may be self-approved.


11. No-hardcode / no-local-island attestation

  • No-hardcode: object_type resolves to a governed registry (coverage-profile catalog / meta_catalog); scope is a reference-table FK (6 rows, open); owner_gov_code FK to governance_registry; inheritance is computed (resolution view), never an enumerated child list; axis support is the same contract applied to a new dimension (no fixed axis array).
  • No-island: one ownership store under the Đ37 roof, written only by the Đ32 spine via SB-1 action-types, audited via registry_changelog/event_outbox. No second owner store, no owner_gov_code column sprawl, no parallel approval. governance_relations (agency↔law) and governance_object_ownership (object/axis→agency) are the two complementary legs of one relational-ownership model.

12. Acceptance (T4 PASS criteria — doc 04)

✔ additive table contract specified (§2) + scope reference table (§3) + resolution/gap views (§4–5); ✔ no-migration-risk proven against the 8 live edges (§7); ✔ container-grain inheritance demonstrated (Δtotal=0 at 10⁸; owner-link-only; anti-hiding) (§5); ✔ axis-ownership path defined and decoupled from SB-3 (§6); ✔ owner-per-scope uniqueness + double/missing-owner detection (§4); ✔ all M-DEF-3/6/7 owner kinds covered (accountable/supporting/delegated/exception; health/execution/render/audit = scope rows); ✔ effective dates/lifecycle/approval/audit/rollback/source refs present (§2); ✔ rollback drafted (DROP, reversible) + rehearsal deferred to doc 19; ✔ no committed mutation; ✔ no-hardcode/no-island attestation (§11). SB-2 design = COMPLETE for implementation review. Implement = NO (C-1 + H-1 + SB-1 + sovereign path).

Back to Knowledge Hub knowledge/dev/reports/architecture/one-roof-governance-technical-addendum-and-implementation-index-2026-06-01/17-sb2-object-axis-ownership-detailed-technical-design.md