05 — Governed Exception Model (Branch E) (2026-06-01)
05 — Governed Exception Model (Branch E)
Reviews the exception handling scattered across decision-pack docs 02 §2.3 (last row), 03 §3.4, 06 (DOT-GOV-EXCEPTION-REVIEW), 07 #12, 08 §4.17/§8.6 §VIII-ext. Adversarial. The Direct-PG API exception must become a fully-governed exception pattern.
E0. What the pack says (summary)
The pack treats an exception as an approval_requests row with an exception action-type + TTL + an overdue path that reuses the live admin_fallback_log → fn_admin_fallback_overdue_scan() → fallback_audit_overdue mechanism (Đ35 §6.5). The Direct-PG read-only adapter is the worked case: it must be ratified or recorded as an approved exception with a law-ref (Đ33 §13), read-only enforcement, TTL, and a vps_deploy_log ledger entry (today un-ledgered). Exceptions are excluded from orphan emission while unexpired and re-raised on TTL expiry (DOT-GOV-EXCEPTION-REVIEW).
Overall: the reuse instinct (admin_fallback pattern) and TTL discipline are right. Three real defects: the required field set is incomplete (no replacement plan), the action-type it relies on does not exist, and severity rests on a fact the scanner can't verify.
E1 — Incomplete field set: no replacement_plan, no review_cadence distinct from expiry
- Original: the pack's exception carries owner / approval / TTL / overdue path / law-ref / read-only enforcement.
- Gap (mission §8): the mission requires eleven fields:
exception_type, scope, owner, reason, risk, approval, expiry, review_cadence, rollback, replacement_plan, issue/event-if-expired. The pack is missingreplacement_plan(the exit strategy) and treatsreview_cadenceas identical toexpiry. An exception with no replacement plan is a permanent island in disguise — it renews at each TTL forever (red-team #5: "temporary but never expires"). - Hardened wording — the full exception record contract:
Field Required Meaning exception_type✅ e.g. direct_pg_readonly_adapter,emergency_hotfix,legacy_grandfatherscope✅ exactly what is exempted (this route / this adapter / this object) — never "the module" accountable_owner✅ who is answerable (per Branch C) reason✅ why the bypass is justified now risk✅ computed risk if abused (drives severity) approval_ref✅ the Đ32 approval that granted it (quorum per risk) expiry(TTL)✅ hard end date; after which → orphan/critical review_cadence✅ distinct from expiry — periodic re-justification interval (e.g. monthly) even before expiry rollback_ref✅ how the exempted state is reverted if pulled replacement_plan✅ the path to NOT needing the exception (e.g. "ratify Directus PK-less view support" / "build route registry") — mandatory; absence ⇒ auto- criticalissue_on_expiry✅ the system_issues/event raised when expired or review missed - Acceptance test: an exception record missing
replacement_plancannot be granted (validation fails); an exception pastreview_cadence(even if beforeexpiry) raises awarning; pastexpiryraisescritical. - Open question: OQ-E1 — default
review_cadenceand max renewal count before a replacement plan must execute? (Recommend: max 2 renewals, then the replacement plan is itself gated.)
E2 — No grant_exception APR action-type exists (live-verified trap)
- Original: doc 02 §2.4 / doc 04 §4.3 / doc 08 §4.17: an exception is "an
approval_requestsrow, action-type for exception, with TTL." - Live fact:
apr_action_typeshas exactly 6 codes —add_field, amend_law, create_item, enact_nrm, patch_ops_code, update_item(re-verified this session). There is no exception action-type. So "an approval_requests row of an exception action-type" is not currently expressible; the pack assumes a row type that doesn't exist. - Misimplementation risk: an implementer either (a) shoehorns an exception into
update_item/create_item(losing TTL/replacement-plan semantics → an ungoverned exception, i.e. an island), or (b) can't create the exception at all (so the Direct-PG adapter stays a raw orphan). - Hardened wording: state as a prerequisite that a new
apr_action_typegrant_exception(and likelyassign_governance_owner,delegate_authority— see C7, I1) must be registered first (a governedaddtoapr_action_types, COUNCIL-owned, withrisk_levelper scope and a handler). Until it exists, exceptions are recorded in the liveadmin_fallback_log(which already has the overdue machinery) as the interim governed home — not inapproval_requests. Be explicit about the interim vs target home. - Acceptance test: the exception flow references an existing action-type (after registration) or the interim
admin_fallback_log; it never writes a free-shaped row. - Open question: OQ-E2 — does
grant_exceptionneed a dedicated handler (TTL enforcement, replacement-plan check), or can the overdue scan + a CHECK suffice? (Likely a small handler.)
E3 — Severity split (read-only vs DDL) rests on a fact the scanner can't see
- Original: doc 03 §3.4 grades Direct-PG
critical (if it can reach DDL) / high (read-only). - Trap: "can it reach DDL?" is a privilege fact of the pg role behind the adapter, not a registry fact. If the scanner assumes read-only, a role that was silently granted write becomes an undetected
critical. Memory-dependence again. - Hardened wording: the exception record must declare the enforced privilege as a verifiable claim: the read-only role name + the assertion "this role has no INSERT/UPDATE/DELETE/DDL grant," and the scanner verifies it against
information_schema.role_table_grants(or equivalent) every scan. Severity is then computed from the verified grant set, not the declared one. A mismatch (declared read-only, actually has write) is itselfcritical. - Acceptance test: flipping the adapter role's grants to include INSERT (in a rehearsal) flips the computed severity to
criticalautomatically — severity tracks reality, not the label. - Open question: none — this is the right reuse of the read-only-role discipline the pack already trusts for
query_pg.
E4 — Allowed vs forbidden exceptions are not enumerated
- Original: the pack says some bypasses "must be ratified or recorded" but never lists what may be exempted temporarily vs what may never be exempted (mission §8: "what exceptions are allowed temporarily; what exceptions are forbidden").
- Hardened wording — the exception allow/deny list (COUNCIL-owned):
May be a temporary exception May NEVER be exempted read-only Direct-PG adapter (Directus PK-less view limit) a write/DDL path outside DOT governance emergency hotfix (Đ35 §6.5 fallback, ≤24h) a local approval mechanism (rule 2 — never) legacy surface pending regularization (QUARANTINED, A3) a standalone policy table with no owner (rule 4 — never) a pre-registration read-only utility UI computing count/grouping/classification truth (Đ28 NT-D1 — never) — emitting an unregistered event_type (Đ45 §3.2 — never) - Rule: "An exception may waive coverage timing (when the link is resolved) but may never waive a safety invariant (no write without DOT governance, no local approval, no UI truth-math, no unregistered emit). Safety invariants are non-exemptable."
- Acceptance test: an attempt to grant an exception for a write-path-outside-DOT or a local-approval-flag is rejected at grant time (non-exemptable class).
- Open question: OQ-E4 — council to ratify the non-exemptable list (it is itself a governed policy).
E5 — Exception suppression of orphan emission can hide a changed object
- Original: doc 07 §7.5: "an object with an unexpired approved exception is excluded from orphan emission."
- Loophole: the exception was granted for a specific scope/state. If the exempted object changes (the adapter gains a new endpoint; the route starts mutating), the exclusion still suppresses the orphan — the exception now covers something it was never granted for (scope creep under an old waiver).
- Hardened wording: the exception suppresses orphan emission only while the object's signature matches the exempted scope. Bind the exception to a state fingerprint (e.g. the adapter's endpoint set / privilege set hash); if the fingerprint changes, the exception is auto-invalidated → the object re-enters orphan emission + a
exception_scope_driftissue fires. (Reuses the E3 verified-fact discipline.) - Acceptance test: mutating an exempted read-only adapter into a write adapter auto-invalidates the exception and raises
critical, rather than staying silently suppressed. - Open question: none.
E-summary — Branch E verdict
| ID | Severity | Type | Disposition |
|---|---|---|---|
| E1 | high | gap | full 11-field record; replacement_plan mandatory |
| E2 | high | trap (live) | grant_exception action-type is a prerequisite; interim = admin_fallback_log |
| E3 | high | memory-dependence | severity from verified grants, not declared |
| E4 | medium | gap | enumerate allowed/forbidden (non-exemptable safety invariants) |
| E5 | medium | loophole | bind exception to a state fingerprint (anti scope-creep) |
The exception model is the right idea but is under-built; E1+E2 must land before any apply DOT (doc 09) can record a real exception. Note the strong dependency on E2 ⟷ I1 ⟷ C7 (all need new APR action-types).