The no-ungoverned-operation guard — fail the build when an actor-initiated write reaches the DB with no permission gate
Turn the inventory from an audit into a contract. A document describing which operations are governed is true on the day it is written and drifts the first time someone adds a route; a test that fails when an operation reaches the database without passing a permission gate keeps it true.
This is the INVERSE of the orphan guard. The story test gate asserts every key has a consumer — no permission exists that nothing enforces. This card asserts every actor-initiated operation has a gate — no operation exists that no permission covers. Together they close the model in both directions; either alone leaves one side unguarded, and it is this side that let 82 of 122 services ship ungoverned.
The shape
- Walk the operations, not the file names. Enumerate
app/api/**/route.tsand the'use server'actions, resolve each to the service method it calls, and fail when a method that writes on behalf of an actor is reachable without passing anassertCan*/ permission check. - An explicit allowlist, and every entry carries a REASON. A background job, meter, sweep or webhook handler has no actor and legitimately has no gate; so does an operation the inventory decided is workspace-scoped. Those go in the allowlist with the inventory row that justifies them — not as a bare path list, which is how an allowlist becomes a place to hide things.
- The allowlist is pinned to the inventory. An entry with no corresponding decision in
docs/decisions/permission-inventory.mdfails the test, so the two cannot drift apart.
Scope boundary
In: the guard, its allowlist, the allowlist↔inventory pinning, and the negative cases that prove the guard can fail.
Out: fixing the operations it finds ungoverned — this card MEASURES and records; wiring the gates is the per-domain split. If the guard's first run surfaces operations the inventory did not anticipate, log them as bugs rather than absorbing the fix here (notes.html #27).
Acceptance criteria
- A Vitest guard enumerates every
app/api/**/route.tsand every'use server'action and resolves each to its service method — asserted by a count check, so a new route cannot slip past by not being enumerated. - The guard fails when an actor-initiated write path is reachable with no permission gate and no allowlist entry.
- Every allowlist entry carries a written reason and a pointer to its inventory row; an entry without one fails the test.
- The guard's negative case is proven: deliberately removing a gate from a governed service makes it go red. A guard that has never been seen to fail is not evidence.
- The guard passes on this card's own branch — every operation in the repo at that point is either gated or carries a justified allowlist entry. (Stated against the branch, not against post-merge
main: this card must be satisfiable inside its own scope.)
Context refs
- The inventory — the document the allowlist is pinned to.
- The catalog growth — the keys the gates will consult.
tests/planning/planChangeArchitecture.test.ts— the shipped precedent for a filesystem-walking architecture guard in this repo.lib/services/projectAccessService.ts— theassertCan*surface a governed path passes through.