KB-5494

16 — SB-1 Governance APR Action-Types — Detailed Technical Design (T3, design-only, 2026-06-01)

26 min read Revision 1
one-roof-governanceimplementation-indexsb-1c-2track-t3apr-action-typestechnical-designfail-closedrisk-level-quorumhandler-unimplementedgoverned-exceptionno-hardcodeno-islanddesign-only2026-06-01

16 — SB-1 Governance APR Action-Types — Detailed Technical Design

Track: T3 (doc 04). Scaffold: doc 05. Pairs with council decision C-2. Tier: detailed technical design, uncommitted. Mutation footprint: none (KB doc only); zero PG/Directus/Qdrant/Nuxt/law/approval mutation; no DDL/DML committed; no action-type registered live. Evidence base: doc 18 (live read-only PG, 2026-06-01). Concept base: canon 01 (M-DEF-3 responsibility-scope, M-DEF-6 governed-exception), canon 02 (§4 Đ37 hub: approval REFERENCE → Đ32 + apr_action_types). House law: no-hardcode absolute, Design-Only Macro Mode, live-apply hard gate, register-before-emit. Blocker status: SB-1 OPEN. This doc designs the 4 action-types completely enough for future implementation review; it does not register, enact, or approve anything.


0. The question SB-1 answers

The apply/remediation path needs APR action-types so a DOT (or human) can propose a governance change — assign an owner, grant a governed exception, delegate authority, assign an axis owner — through the one approval spine (Điều 32), not a local one. Today apr_action_types has 6 rows, none of them governance. SB-1 = add the missing vocabulary as governed data, fail-closed, with no second roof.


1. Pattern decision — reference-table rows (PG-first), NOT enum/config/Directus-vocab

Decision: the 4 action-types are rows in the existing apr_action_types reference table. Rationale, against the alternatives the mission asked us to weigh:

Candidate pattern Verdict Why
PG reference-table rows in apr_action_types CHOSEN The table already exists, is the FK target of approval_requests.proposed_action_code, and is already how risk_level→quorum and handler_ref→executability are resolved. Adding rows = pure data, no code branch, no-hardcode satisfied. Reuses the live reserved-action precedent (amend_law/enact_nrm).
PostgreSQL enum type REJECTED An enum is schema; adding a value is ALTER TYPE (DDL) = hardcode-in-schema, breaks "a new action-type is data". The live design already chose a table, not an enum.
Separate config rows (new table) REJECTED A parallel action-type registry beside apr_action_types = a second vocabulary the approval spine doesn't read = local governance island.
Directus/APR vocabulary rows (Directus-managed) REJECTED as the source of truth apr_action_types is PG-owned and FK-enforced; Directus surfaces it but must not become a competing writer. (Five-layer sync: PG is SSOT; Directus reads.)

No-hardcode compliance: action-type names, risk, and handler are all data in apr_action_types; nothing is enumerated in code. Quorum is resolved from risk_level by the existing fn_apr_quorum_check; executability from handler_ref by fn_apr_block_unimplemented_handler. No new branch, no new enum, no new table for SB-1.


2. The fail-closed registration model (the heart of SB-1)

The live substrate gives us a safe two-phase path that lets SB-1's vocabulary gap close without enabling any apply:

Phase A — Vocabulary registration (data, fail-closed, reversible). Register the 4 rows with status='active', an appropriate risk_level, and handler_ref='unimplemented'. Consequence (proven from fn_apr_block_unimplemented_handler, doc 18 §4.3): any APR that references one of these codes RAISES … Reserve-only, cannot execute. ⇒ the vocabulary exists and is risk-rated, but zero apply capability is created. This is exactly how amend_law and enact_nrm sit live today (registered, risk='high', handler_ref='unimplemented'). SB-1's "missing vocabulary" gap is closeable at Phase A with no apply risk — but even Phase A is not done in this macro (it is a committed INSERT; it requires C-2 + the human enact/approval path).

Phase B — Handler activation (T11, gated). Build the real handler (a DOT op that writes to governance_object_ownership / admin_fallback-or-exception-register / governance_relations delegation), then flip handler_ref from 'unimplemented' to the real ref — only after: (a) council ratifies C-2; (b) the handler exists and is itself a governed DOT; (c) SB-2 is resolved (the owner-write target exists); (d) sovereign sign-off path (H-2/SB-6) is available. Flipping handler_ref is an update_item on apr_action_types and is itself an APR.

This phased model is the no-island guarantee: the action-types are inert governed data until the one roof's own approval+handler path is ratified.


3. Approval routing & quorum (data-driven, no hardcode)

3.1 Quorum is keyed to risk_level

fn_apr_quorum_check (doc 18 §4.3) enforces, on the pending→approved transition, per apr_action_types.risk_level:

  • high≥1 president (human) + ≥2 ai_council approve;
  • medium ⇒ ≥1 president;
  • low ⇒ ≥1 approve;
  • any reject blocks; self-approve (proposer == approver) is prohibited.

⇒ Setting the right risk_level on each action-type is the quorum routing. No code, no hardcoded list.

3.2 The auto-approve bypass — mandatory mitigation

fn_auto_approve_add machine-approves any APR with action='add' on INSERT, before the quorum trigger can fire (quorum needs OLD.status='pending', absent on INSERT). Therefore every governance APR MUST be submitted with action ∈ {review, modify, delete} — never action='add'. Recommended default: action='review' (the APR is a request for a governed decision). This is a hard wiring rule, restated in every action-type spec below.

Defence-in-depth (T11 hardening, flagged not designed here): fn_auto_approve_add should additionally exclude any APR whose proposed_action_code maps to risk_level IN ('high','medium') (or to the governance action-type family). Until that hardening ships, the action≠'add' rule + the handler_ref='unimplemented' block are the two independent safeties.

3.3 Request-type binding

Governance APRs set request_type_code to a governed request type. Interim reuse: rule_change (owner/delegation/axis policy) or reclassify. Optional later: governed request types assign_owner / grant_exception (rows in apr_request_types, secondary to SB-1, not required for Phase A). proposed_action_code carries the real SB-1 action-type.


4. The four action-types — full specifications

Common to all four: pattern = apr_action_types row; Phase-A handler_ref = unimplemented (fail-closed); submission = approval_requests with action='review', proposed_action_code=<code>, target_collection set, source_context.proposer set (so self-approve is detectable); audit = registry_changelog + event_outbox (governance domain, register-before-emit); rollback = retire the action-type via update_item (status='deprecated'/'retired', retired_at=now()) — never delete (FK ON DELETE RESTRICT from approval_requests); relation to Đ37 = REFERENCE (Đ37 owns the contract; Đ32 + apr_action_types is the mechanism).

4.1 assign_governance_owner

  • Purpose: propose the accountable owner for a (governed_object × responsibility_scope) pair (M-DEF-3) — the canonical write into governance_object_ownership (SB-2).
  • Target object classes: any governed object class in the coverage-profile catalog / meta_catalog (collection, pivot, axis, IU class, registry, mother, route, …). Object identified by (object_type, object_ref) resolving to a registry row (no-hardcode §1).
  • Allowed requester: the GOV-SIV coverage scanner DOT (proposing remediation of an OWNER_GAP), or a human via the Đ37 hub. Requester recorded in source_context.proposer.
  • Required approver / quorum: risk_level='high' ⇒ president + ≥2 ai_council. Owner assignment is authority-critical.
  • Validation rules: (a) owner_gov_codegovernance_registry(code) and status='active' (cannot assign a draft/retired agency — except an explicit TTL-delegation, see C-5/delegate_authority); (b) scope ∈ the 6 responsibility scopes; (c) no existing active accountable owner for the same (object_type, object_ref, scope) unless the APR is an explicit re-assignment carrying the prior owner for supersede; (d) object_ref resolves to a live registry row (no orphan — birth precedence M-DEF-4); (e) action≠'add'.
  • Payload schema (approval_requests.proposed_action jsonb): see §5.1.
  • Required evidence: the coverage-scan finding (issue id / coalesce_key), the object's current coverage profile, and the rationale that this scope is unowned.
  • Risk level: high.
  • Audit path: on apply (Phase B), the handler writes the ownership row and a registry_changelog row (entity_type='governance_object_ownership', action='assign_owner') and emits governance.owner.assigned to event_outbox.
  • Rollback / retire: ownership rows are end-dated (effective_to) not deleted (SB-2 §); the action-type itself retires via update_item.
  • Relation to governance_registry/relations/object_ownership: reads governance_registry (owner validity); writes governance_object_ownership (SB-2); does not touch governance_relations (object/axis can't live there — that is the whole point of SB-2).
  • Relation to system_issues/event_outbox: consumes an OWNER_GAP/thiếu_quan_hệ issue; on apply, resolves it and emits governance.owner.assigned.
  • Failure modes: owner is draft/retired → reject; double-owner for scope → governance.owner.conflict (M-DEF-3) → reject; object orphan → defer to birth scanner; action='add' → auto-approved bypass (forbidden — validation (e)).
  • Test cases: TC-1 assign owner for an unowned scope, high quorum met → applied + changelog + event; TC-2 second active owner same scope → blocked; TC-3 owner_gov_code='GOV-XYZ' (absent) → FK reject; TC-4 submitted with action='add' → rejected by validation before insert; TC-5 handler_ref='unimplemented' (Phase A) → any submission RAISES (fail-closed).

4.2 grant_governance_exception

  • Purpose: record an M-DEF-6 governed exception (an object that knowingly fails a coverage profile, under a time-boxed, owned, replacement-planned waiver).
  • Target object classes: any governed object class.
  • Allowed requester: the accountable owner of the object's policy scope (or COUNCIL); never the scanner alone.
  • Required approver / quorum: risk_level='high' ⇒ president + ≥2 ai_council. An exception suspends an invariant → highest scrutiny.
  • Validation rules: the 11 fields must all be present and valid — exception_type, scope, accountable_owner (∈ active governance_registry), reason, risk, approval_ref, expiry (future), review_cadence, rollback_ref, replacement_plan (NON-NULL — cannot grant without it), issue_on_expiry; a state fingerprint is computed (auto-invalidate on signature change); max 2 renewals (the 3rd auto-escalates to critical and the replacement_plan must execute); non-exemptable floors (no write outside DOT, no local approval, no UI-truth, no unregistered emit, no reconstruction/vector exemption) are refused.
  • Payload schema: §5.2.
  • Required evidence: the failing coverage finding + the named replacement_plan + the rollback ref.
  • Risk level: high.
  • Audit path: apply → exception register row + registry_changelog (action='grant_exception') + emit governance.exception.granted; on TTL expiry a scheduled DOT emits governance.exception.expired and opens issue_on_expiry.
  • Rollback / retire: exceptions are revocable (status='revoked', fingerprint-invalidated) — they are not owner rows; retire the action-type as in §4.
  • Store decision (the C-2 mismatch): do NOT shoehorn the 11 fields into admin_fallback_log (doc 18 §5 — it forces dot_code/backup_path/patch_diff NOT NULL and auto-fires a DOT-repair retroactive APR). Recommended: the 11-field record is carried in the APR proposed_action jsonb and, on apply, materialised into a governed-exception register (a small SB-2-adjacent table, or — interim — a typed projection). admin_fallback_log remains reserved for genuine break-glass DOT repair, not as the canonical exception store. This is a deliberate, evidence-grounded improvement on the scaffold's C-2 default and is surfaced to council (doc 21).
  • Failure modes: missing replacement_plan → cannot grant; >2 renewals → auto-critical; underlying state changed → fingerprint auto-invalidates the exception; attempt to exempt a non-exemptable floor → reject.
  • Test cases: TC-1 full 11-field, quorum met → granted + event; TC-2 no replacement_plan → rejected; TC-3 RO→write adapter changes signature → auto-invalidate; TC-4 TTL passes → issue_on_expiry + governance.exception.expired; TC-5 3rd renewal → escalate critical.

4.3 delegate_authority

  • Purpose: record a time-boxed delegation of a responsibility scope from the canonical owner to another agency (the C-5 render case: COUNCIL delegates render to GOV-MOUT under TTL while GOV-MOUT is draft; also covers temporary cover during agency stand-up such as OP-B interim).
  • Target object classes: a (governed_object × scope) or a whole domain.
  • Allowed requester: the canonical accountable owner of that scope, or COUNCIL.
  • Required approver / quorum: risk_level='high' ⇒ president + ≥2 ai_council (delegation moves authority).
  • Validation rules: (a) delegator is the current active accountable owner for (object, scope); (b) delegate ∈ governance_registry(code); (c) mandatory expiry/TTL (a delegation without TTL is rejected — delegation is never permanent; a permanent change is assign_governance_owner); (d) delegation does not create a second accountable owner — it records a delegate with a TTL and a pointer to the canonical owner (no M-DEF-3 double-owner conflict); (e) action≠'add'.
  • Payload schema: §5.3.
  • Required evidence: the reason (e.g. "GOV-MOUT draft; render needs an interim owner per C-5"), the TTL, and the end-state (activation plan).
  • Risk level: high.
  • Audit path: apply → governance_object_ownership delegation row (owner_kind='delegated', effective_to=TTL) + registry_changelog (action='delegate_authority') + emit governance.authority.delegated; near-expiry a scheduled DOT emits governance.delegation.expiring.
  • Rollback / retire: revoke = end-date the delegation row early; the canonical owner is unaffected (it never lost accountability).
  • Relation to substrate: writes governance_object_ownership (delegate row, distinct owner_kind); reads governance_registry. Does not touch governance_relations.
  • Failure modes: no TTL → reject; delegator not the canonical owner → reject; delegate retired → reject.
  • Test cases: TC-1 COUNCIL→GOV-MOUT render, TTL 90d, quorum met → delegated; TC-2 no TTL → rejected; TC-3 non-owner delegates → rejected; TC-4 TTL passes → governance.delegation.expiring then auto-end-date.

4.4 assign_axis_owner

  • Purpose: assign the accountable owner of an axis (M-DEF-8: an axis is a governed object) — the dimension along which objects are classified/grouped/pivoted. Distinct from owning the objects the axis organises.
  • Target object classes: an axis (profile AXIS), identified by an axis_code that (end-state) lives in the Axis Registry (M-DEF-9). Independent of SB-3 (doc 18 §7): owning an axis needs only the ownership row + an axis identifier; it does not require the generic axis-value store. Interim, an axis_code may reference an existing dimension (e.g. a pivot_definitions.group_spec dimension, a classification/pivot law-domain axis) pending the Axis Registry.
  • Allowed requester: GOV-SIV scanner (axis-coverage gap) or the axis's specialized-law owner (e.g. classification→Đ24, pivot→Đ26).
  • Required approver / quorum: risk_level='high' (an axis governs how the whole system counts/groups → authority-critical). [Council may set medium for low-impact display-only axes via C-2; default high.]
  • Validation rules: (a) axis_code is registered (Axis Registry row, or — interim — a recognised dimension) — an unregistered axis → axis_unregistered issue, defer; (b) owner_gov_code ∈ active governance_registry; (c) one accountable owner per (axis × scope); (d) vocabulary home stays specialized (the owner of the axis ≠ owner of the label/pivot vocabulary law — cross-link, do not absorb, per canon §5); (e) action≠'add'.
  • Payload schema: §5.4.
  • Required evidence: the axis's registry entry (or the axis-unregistered finding), the grouping-policy reference.
  • Risk level: high (default).
  • Audit path: apply → governance_object_ownership row (object_type='axis') + registry_changelog (action='assign_axis_owner') + emit governance.axis.owner_assigned.
  • Rollback / retire: end-date the axis-owner row; retire the action-type as in §4.
  • Relation to substrate: writes governance_object_ownership with object_type='axis'; reads the Axis Registry (future) / dimension source; does not depend on iu_three_axis_envelope (SB-3).
  • Failure modes: axis unregistered → defer (axis_unregistered); double axis-owner → governance.owner.conflict; trying to own a vocabulary law via this action → reject (wrong instrument).
  • Test cases: TC-1 assign owner to a registered axis, quorum met → assigned; TC-2 unregistered axis → axis_unregistered, no assign; TC-3 second owner same axis/scope → conflict; TC-4 SB-3 absent → still succeeds (axis ownership independent of axis-value store).

5. Payload schemas (approval_requests.proposed_action jsonb)

Payloads are safe metadata (Điều 45: signal, not data). They reference object/owner by code; they do not embed bulk content.

5.1 assign_governance_owner

{
  "action": "assign_governance_owner",
  "object_type": "<meta_catalog.entity_type | coverage-profile class>",
  "object_ref":  "<registry code / id of the governed object>",
  "scope":       "policy|health|execution|render|approval|audit",
  "owner_gov_code": "GOV-...",
  "owner_kind":  "accountable",
  "supersedes_owner_gov_code": "GOV-... | null",
  "source_finding": { "issue_id": 0, "coalesce_key": "..." },
  "rationale": "..."
}

5.2 grant_governance_exception (M-DEF-6, 11 fields)

{
  "action": "grant_governance_exception",
  "exception_type": "...", "scope": "policy|...|audit",
  "object_type": "...", "object_ref": "...",
  "accountable_owner": "GOV-...",
  "reason": "...", "risk": "low|medium|high|critical",
  "approval_ref": "<apr code>",
  "expiry": "2026-..Z", "review_cadence": "P30D",
  "rollback_ref": "...", "replacement_plan": "<NON-NULL plan>",
  "issue_on_expiry": true,
  "state_fingerprint": "<sha of governed-state signature>",
  "renewal_count": 0
}

5.3 delegate_authority

{
  "action": "delegate_authority",
  "object_type": "...|domain", "object_ref": "...",
  "scope": "render|...", "from_owner_gov_code": "GOV-COUNCIL",
  "to_delegate_gov_code": "GOV-MOUT", "owner_kind": "delegated",
  "expiry": "2026-..Z", "end_state_plan": "GOV-MOUT activation",
  "rationale": "C-5 interim delegation"
}

5.4 assign_axis_owner

{
  "action": "assign_axis_owner",
  "object_type": "axis", "axis_code": "<axis registry code>",
  "axis_family": "classification|pivot|label|kg|iu|...",
  "scope": "policy|...|audit", "owner_gov_code": "GOV-...",
  "vocabulary_law_ref": "NRM-LAW-24|26|...",
  "source_finding": { "issue_id": 0, "coalesce_key": "..." }
}

Finding (doc 18 §5): the scaffold's interim store admin_fallback_log is DOT-repair-shaped and not a governed-exception register. Recommendation to council (C-2):

  1. Canonical store = a small additive governed-exception register (co-located with SB-2's substrate, sharing its lifecycle/approval/audit columns), materialised by the grant_governance_exception handler from the APR payload. This keeps the 11 fields queryable and FK-clean.
  2. admin_fallback_log stays reserved for genuine break-glass DOT repair (its auto-retroactive-APR trigger is correct there).
  3. Interim, pre-register (if council insists on no new table before T11): carry the 11-field record only in the APR proposed_action jsonb (queryable, audited, not applied) — i.e. the exception is requested and recorded but not materialised until the register exists. This preserves the contract without abusing admin_fallback_log.

7. Integration summary (per surface)

Surface How SB-1 integrates No-island proof
Điều 32 approval spine proposed_action_code FK → apr_action_types; quorum via fn_apr_quorum_check(risk_level); auto-approve avoided via action≠'add' reuses the one spine; no parallel approver
Điều 37 hub REFERENCE: Đ37 owns the owner/exception/delegation contract; Đ32+apr_action_types is the mechanism it points to mechanism stays specialized; no duplicate
GOV-COUNCIL / GOV-SIV / GOV-DOT COUNCIL = approver (quorum president+council); SIV = proposer (scan findings); DOT = executor (Phase-B handler) federation of owners under one roof
governance_registry / relations / object_ownership reads registry (owner validity); writes governance_object_ownership (SB-2); never widens governance_relations object/axis ownership lives in SB-2, not a bolt-on column
system_issues consumes thiếu_quan_hệ/OWNER_GAP; resolves on apply (SB-4 governance issue types via T7) reuse existing issue substrate
event_outbox / event_type_registry emits governance.owner.assigned/.conflict, governance.exception.granted/.expired, governance.authority.delegated, governance.axis.owner_assignedregistered first under a GOV-SIV governance domain (register-before-emit) no unregistered emit
registry_changelog every apply writes a changelog row (generic entity audit) reuse existing audit
future Axis Registry assign_axis_owner targets axis_codes; independent of SB-3 axis-as-object owned via SB-2

8. Failure modes (consolidated)

  1. Auto-approve bypassaction='add' → machine-approved without quorum. Mitigation: governance APRs use action='review'; T11 hardens fn_auto_approve_add to exclude high-risk action-types.
  2. Premature handler — flipping handler_ref to a real handler before C-2 + SB-2 + sovereign path → live apply with no ratification. Mitigation: Phase-A handler_ref='unimplemented' (fail-closed); flip is itself an APR gated on doc-03 §3.5.
  3. Self-approval — proposer votes. Mitigation: fn_apr_quorum_check already prohibits it (needs source_context.proposer set).
  4. Exception abuse — exception granted without replacement_plan or to dodge a safety floor. Mitigation: 11-field validation + non-exemptable floors + max-2-renewals.
  5. Local island — a handler writing owner edges to a private table instead of governance_object_ownership. Mitigation: handler contract pins the SB-2 table + Đ37 hub; CI island scan (canon §5 dual channel).
  6. Orphan precedence — assigning an owner to an unborn/unregistered object. Mitigation: validation defers to birth scanner (M-DEF-4).
  7. FK lock on retire — deleting an in-use action-type. Mitigation: retire (status/retired_at), never delete (FK RESTRICT).

9. Apply-readiness gate (what must be true before any of these is registered/used)

Phase A (register the 4 rows) requires: C-2 ruled (council bundle) + the human enact/approval path (H-1) + a rehearsal log (doc 19). Phase B (flip a handler / first live use) additionally requires: SB-2 resolved (owner-write target exists) + the handler built as a governed DOT + sovereign sign-off path (H-2/SB-6, os_proposal_approvals currently 0 ⇒ COMMIT_FORBIDDEN) + dot_coverage_required rows for the new propose/apply ops (SB-8/T6). No gate may be self-approved.


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

  • No-hardcode: action-type names, risk, handler are rows in apr_action_types; quorum derives from risk_level; object/owner/scope/axis resolve to registries (meta_catalog, governance_registry, the scope reference, the Axis Registry). No enum, no code-branch list, no literal owner.
  • No-island: SB-1 adds vocabulary to the existing spine and reuses fn_apr_quorum_check/fn_apr_block_unimplemented_handler/apr_approvals/registry_changelog/event_outbox. It mints no new approver, no new lifecycle, no second action-type registry. Đ37 stays the single roof (REFERENCE to Đ32).

11. Acceptance (T3 PASS criteria — doc 04)

✔ 4 action-types fully specified as additive data (§4); ✔ handler vs council-review routing defined (Phase A unimplemented / Phase B handler; quorum via risk_level) (§2–3); ✔ interim exception store named and corrected (§6); ✔ apply-readiness gate stated (§9); ✔ rollback drafted (retire-not-delete) + rehearsal deferred to doc 19; ✔ no committed mutation; ✔ no-hardcode attestation (§10). SB-1 design = COMPLETE for implementation review. Implement = NO (C-2 + H-1 + SB-2 + sovereign path).

Back to Knowledge Hub knowledge/dev/reports/architecture/one-roof-governance-technical-addendum-and-implementation-index-2026-06-01/16-sb1-apr-action-types-detailed-technical-design.md