Decision authority wasn't missing. It was three unset switches.

Nothing about ratifying or superseding a governance Decision required new capability. The role/grant system, the per-transition role gate, the state machine — all of it already existed and already worked. It was just never switched on for these two specific transitions, and the switch itself had three ways to silently do nothing when flipped.

Nothing about ratifying or superseding a governance Decision required new capability. The role/grant system, the per-transition role gate, the state machine — all of it already existed and already worked. It was just never switched on for these two specific transitions, and the switch itself had three ways to silently do nothing when flipped.

An unset column, not a missing mechanism

orchDecisionFlow()'s proposed → ratified and ratified → superseded transitions were registered as bare {From, To} pairs — no RequiredRole. validateTransition already reads a required_role column and checks it against the caller's grants via RoleGranted. The two transitions that matter most for authority just never had anything in that column to check.

That alone wasn't the whole gap. validateTransition had three separate fail-open branches guarding the role check itself: a nil RoleStore (governance not wired), an empty actor ID (no authenticated caller), and — the one that was easy to miss — the smeldr_transitions lookup query itself erroring for a reason other than "no such row." All three returned nil (allowed) unconditionally, for every transition, regardless of whether that transition had a role set. Setting required_role on the two Decision transitions without fixing this would have shipped a gate that looked real and wasn't: a SQLITE_BUSY under write contention or a deployment that forgot to wire App.Governance would let the transition through, silently, exactly when the property "no unratified decision counts as a premise" matters most.

The fix is a new Strict field on Transition, opt-in per row — every existing transition keeps today's lenient behaviour unchanged — plus one global change: the query-error branch now fails closed for every transition, strict or not, since it runs before Strict is ever consulted. Shipping the Strict column alone, without that second change, would not have closed the gap it was built to close.

The check that would have been silently wrong

The admin-role check is layered onto updateHandler — the one call site that can currently move a Decision through a status change at all (MCPUpdate explicitly restores the existing status after decoding a request, so no MCP tool reaches this path today). updateHandler decodes a full-replace PUT body into a fresh struct, not a merge — so a request body that omits a field zeroes it in the decoded item.

The first version of a second, scope-aware authorization layer read that decoded item's own Scope field to decide which role to require. A test written specifically to exercise that branch — grant the actor the generic role, withhold the scope-specific one, expect a 403 — came back 200 instead. The decoded item's Scope was empty, because the test's request body only set Status. The check wasn't failing to reject; it was checking the wrong copy of the item; against the value the request was *about to write*, not the value describing what the item *currently is*. Fixed by checking the item as loaded from the repository before decoding, not after — which also closes a real path where a caller could otherwise pair a status change with a favorable scope change in the same request and have the check see the wrong one.

What actually enforces it today

The scope-aware layer ships, but empty — no per-scope role policy has been decided yet, so it's a deliberate no-op for now, a mechanism ready for a future decision rather than a rule of its own. What's live today is the plain, generic gate: Decision ratify and supersede require the admin role, full stop. Whether the actual account that has been ratifying decisions up to now holds that role is a live deployment fact no amount of reading the source code can confirm — worth checking before this reaches anyone whose next ratify attempt would otherwise be the first time they find out.