Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2292Done

The permission guards are RED on `main` — an inline object type in a parameter hides the gate, and one route is missing from the inventory

pnpm test tests/permissions/ fails three assertions on origin/main at 9aed45b0 — verified by running it, 2026-08-06, not inferred:

× names every app/api route            → /api/ai/coding-convention/audit-coverage
× every gated operation is one the inventory also knows about → the same route
× pins how many operations still await MOTIR-2256            → expected 95 to be 94

Two independent causes, both from the audit-coverage work merging alongside the permission model — each branch green against its own base, red together.

Cause 1 — a route the map never saw. app/api/ai/coding-convention/audit-coverage/route.ts landed in e4a6bb53; the inventory was written against the tree before it. It is gated (it calls auditCoverageService.getCoverage, which asserts), so it needs a row, not a gate.

Cause 2 — and this one matters far more than a red build. serviceBodies() in tests/permissions/noUngovernedOperation.test.ts extracts a service method's body by taking src.indexOf('{', …) from just after the method NAME. A parameter carrying an inline object type gets there first:

async reaudit(
  projectId: string,
  ctx: …,
  identifier: string,
  options: { repoKeys?: string[] },   // ← the walk thinks THIS is the body
): Promise<…> {
  await projectAccessService.assertCanManage(projectId, ctx);   // ← never seen

So the walk reports a gated method as ungated, and every route reaching a gate only through such a method is counted as an ungoverned operation. e4a6bb53 added exactly that parameter to reaudit, which is why the pinned count moved by one — but the bug is not one route. Correcting the extractor locally (walk the parameter list to its closing ) first, then find the body's {) moves the guard's own numbers from 52 gated / 95 pending / 38 unconfirmed to 76 gated / 81 pending / 33 unconfirmed. Twenty-four operations the model story reported as ungoverned already have a gate.

That is the reason this is highest and first: every sibling card under this story is sized and closed against those counts, and a walk that under-reports gates in the alarming direction would have had them adding a second assertPermission to code that already asserts.

This card changes NO product behaviour. It touches two test files, the inventory document and two comment blocks. It adds no gate, removes no gate, and moves no permission — a diff here that touches lib/services/** or app/api/** handler logic is out of scope and belongs to the domain card for that area.

Acceptance criteria

  • serviceBodies() finds a method's real body: it advances past the parameter list (paren-depth to the closing )) before locating the opening {. The memoised methodIsGated cache and the fresh-regex-per-call discipline the file already documents are preserved.
  • The existing the guard can actually fail block gains a control for this exact shape: a fixture method whose only assertCan* sits after a parameter with an inline object type is reported gated, and a method with no gate at all is still reported ungated. Without both halves the fix is indistinguishable from making the walk answer true more often.
  • methodIsGated('aiConventionService.reaudit') is true. It is false today, and that single expectation is the whole bug in one line.
  • docs/decisions/permission-inventory.md carries a row for /api/ai/coding-convention/audit-coverage in its ai section, with a permission, a decision and a reason that resolves in the Reasons list — ai:plan / new / R5, matching its three siblings. Note in the row's gate-today cell that it is gated via auditCoverageService.getCoverage. (Whether ai:plan is the right key for a route that is admin-gated today is a LOOSENING question, and it belongs to MOTIR-2291 along with its three siblings — do not re-point any gate here.)
  • Both pinned numbers are re-derived from the run on this branch, not copied from this card: the PENDING pin and the CLAIMED_BUT_UNVERIFIED pin are set to what the corrected walk produces, and the commit message states both before/after pairs. (Expected 95→81 and 38→33 measured at 9aed45b0; if the branch produces different numbers, the run wins and the PR body says why.)
  • The comments that assign the whole planned list to one story are corrected in both places they appear — lib/permissions/catalog.ts (the enforcement doc block and PLANNED_PERMISSIONS) and the guard's PENDING block — so they name MOTIR-2256 for the twelve administrative keys, MOTIR-2291 for the other eight, and PENDING reaching zero as the two stories' joint definition of done.
  • pnpm test tests/permissions/ is green, and pnpm lint + the prettier check pass repo-wide (not over a scoped glob — a path with a route group in it matches nothing and exits 0).

Context refs

  • tests/permissions/noUngovernedOperation.test.ts — the walk, serviceBodies(), both pins, and the can actually fail controls.
  • tests/permissions/inventoryCoverage.test.ts — the route↔document totality assertion that fails on the missing row.
  • docs/decisions/permission-inventory.md — the map; its ai section holds the three coding-convention sibling rows to match.
  • lib/services/aiConventionService.tsreaudit, getAudit, getConvention; each asserts assertCanManage, and reaudit's fourth parameter is the shape that reproduces the bug.
  • app/api/ai/coding-convention/audit-coverage/route.ts — the route with no inventory row.
  • The guard that this card repairs · the inventory it is pinned to · the story that takes the other eight keys.