16 — SB-1 Governance APR Action-Types — Detailed Technical Design (T3, design-only, 2026-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_addshould additionally exclude any APR whoseproposed_action_codemaps torisk_level IN ('high','medium')(or to the governance action-type family). Until that hardening ships, theaction≠'add'rule + thehandler_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 intogovernance_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 insource_context.proposer. - Required approver / quorum:
risk_level='high'⇒ president + ≥2 ai_council. Owner assignment is authority-critical. - Validation rules: (a)
owner_gov_code∈governance_registry(code)andstatus='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_refresolves to a live registry row (no orphan — birth precedence M-DEF-4); (e)action≠'add'. - Payload schema (
approval_requests.proposed_actionjsonb): 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_changelogrow (entity_type='governance_object_ownership',action='assign_owner') and emitsgovernance.owner.assignedtoevent_outbox. - Rollback / retire: ownership rows are end-dated (
effective_to) not deleted (SB-2 §); the action-type itself retires viaupdate_item. - Relation to
governance_registry/relations/object_ownership: readsgovernance_registry(owner validity); writesgovernance_object_ownership(SB-2); does not touchgovernance_relations(object/axis can't live there — that is the whole point of SB-2). - Relation to
system_issues/event_outbox: consumes anOWNER_GAP/thiếu_quan_hệissue; on apply, resolves it and emitsgovernance.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-5handler_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 tocriticaland 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') + emitgovernance.exception.granted; on TTL expiry a scheduled DOT emitsgovernance.exception.expiredand opensissue_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 forcesdot_code/backup_path/patch_diffNOT NULL and auto-fires a DOT-repair retroactive APR). Recommended: the 11-field record is carried in the APRproposed_actionjsonb and, on apply, materialised into a governed-exception register (a small SB-2-adjacent table, or — interim — a typed projection).admin_fallback_logremains 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) mandatoryexpiry/TTL (a delegation without TTL is rejected — delegation is never permanent; a permanent change isassign_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_ownershipdelegation row (owner_kind='delegated',effective_to=TTL) +registry_changelog(action='delegate_authority') + emitgovernance.authority.delegated; near-expiry a scheduled DOT emitsgovernance.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, distinctowner_kind); readsgovernance_registry. Does not touchgovernance_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.expiringthen 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 anaxis_codethat (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. apivot_definitions.group_specdimension, aclassification/pivotlaw-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 setmediumfor low-impact display-only axes via C-2; default high.] - Validation rules: (a)
axis_codeis registered (Axis Registry row, or — interim — a recognised dimension) — an unregistered axis →axis_unregisteredissue, defer; (b)owner_gov_code∈ activegovernance_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_ownershiprow (object_type='axis') +registry_changelog(action='assign_axis_owner') + emitgovernance.axis.owner_assigned. - Rollback / retire: end-date the axis-owner row; retire the action-type as in §4.
- Relation to substrate: writes
governance_object_ownershipwithobject_type='axis'; reads the Axis Registry (future) / dimension source; does not depend oniu_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": "..." }
}
6. Governed-exception store — recommended resolution (refines C-2)
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):
- 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_exceptionhandler from the APR payload. This keeps the 11 fields queryable and FK-clean. admin_fallback_logstays reserved for genuine break-glass DOT repair (its auto-retroactive-APR trigger is correct there).- Interim, pre-register (if council insists on no new table before T11): carry the 11-field record only in the APR
proposed_actionjsonb (queryable, audited, not applied) — i.e. the exception is requested and recorded but not materialised until the register exists. This preserves the contract without abusingadmin_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_assigned — registered 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)
- Auto-approve bypass —
action='add'→ machine-approved without quorum. Mitigation: governance APRs useaction='review'; T11 hardensfn_auto_approve_addto exclude high-risk action-types. - Premature handler — flipping
handler_refto a real handler before C-2 + SB-2 + sovereign path → live apply with no ratification. Mitigation: Phase-Ahandler_ref='unimplemented'(fail-closed); flip is itself an APR gated on doc-03 §3.5. - Self-approval — proposer votes. Mitigation:
fn_apr_quorum_checkalready prohibits it (needssource_context.proposerset). - Exception abuse — exception granted without replacement_plan or to dodge a safety floor. Mitigation: 11-field validation + non-exemptable floors + max-2-renewals.
- 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). - Orphan precedence — assigning an owner to an unborn/unregistered object. Mitigation: validation defers to birth scanner (M-DEF-4).
- 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 fromrisk_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).