The eight keys enter the built-in role sets — and the 64-row parity table records exactly what each one takes away
The seam every wiring card in this story stands on, and the twin of assertPermission's card one story over. projectAccessService.assertPermission(projectId, ctx, key, tx?) already ships, so no new gate mechanism is needed — but a key that is in the catalog and in NO role set resolves to nobody, so a wiring card that lands before this one refuses every actor including the project admin. This card puts the eight keys into the sets the decision assigns them to, and does nothing else.
Adding a key to a role set is inert until a gate consults it — the twelve administrative keys sat in ROLE_GATED_PERMISSIONS for the whole of the split while their domains were wired one at a time. So this card changes no behaviour by itself, and its whole value is that the capability change becomes VISIBLE in one place: the parity truth table.
The shape
- All eight join
ROLE_GATED_PERMISSIONS— a key outside it is not role-holdable at all, and the workspace-manager always-pass rail resolves to exactly that array. - Each joins the
BUILTIN_ROLE_PERMISSIONSsets the decision assigns:admintakes all eight;membertakes the ones the decision puts at member;viewertakes only what the decision puts at viewer. IMPLICIT_WORKSPACE_MEMBER_PERMISSIONStakes exactly what the decision says and nothing more — it is a separate constant on purpose, and reusingmember's set here is the mistake the file's own comment warns about.levelGrantsinlib/permissions/resolve.tsis NOT touched unless the decision explicitly calls for a branch. The file states why: naming a key there breaks the per-level equivalence the parity table proves.enforcementinlib/permissions/catalog.tsstaysplannedfor all eight. It flips toenforcedon the card that actually wires that key's operations — the seam that lets naming and wiring land separately, and the reasontests/permissions/catalog.test.tscan pin the count honestly.
The parity table is the deliverable, not a chore
tests/permissions/accessParity.test.ts writes out all 64 combinations of access level × workspace role × project role by hand, deliberately not derived. For the administrative split it proved the twelve keys resolved identically to project:administer for every actor. Here it proves the opposite kind of thing, and that is the point: each row that changes is a capability somebody loses, and the diff of this file is the complete, reviewable list of them.
Acceptance criteria
ROLE_GATED_PERMISSIONSholds all eight keys, andBUILTIN_ROLE_PERMISSIONS/IMPLICIT_WORKSPACE_MEMBER_PERMISSIONShold exactly the subsetsdocs/decisions/member-facing-permissions.mdassigns — the file is the source, and any divergence from it is a bug in this card, not a judgement call.- The 64-row table in
tests/permissions/accessParity.test.tsis updated by hand, row by row, and every row whose expectation changes carries a comment naming the key and the actor whose capability moved. - A test asserts the workspace-manager rail holds all eight on every access level (the existing
ROLE_GATED_PERMISSIONSloop covers this once they are in the array — confirm it does rather than assuming). - A test asserts a project
viewerdoes NOT hold the keys the decision withholds, and thatIMPLICIT_WORKSPACE_MEMBER_PERMISSIONSgrew by exactly the keys the decision names. lib/permissions/catalog.tsis unchanged apart from comments:PLANNED_PERMISSIONSstill returns all eight, andENFORCED_PERMISSIONSstill has the lengthcatalog.test.tspins, because no gate consults a new key in this PR.- The comment block in
builtinRoles.tsthat reads "Today that leaves MOTIR-2291's eight member-facing keys outside" is corrected to describe what this card did. pnpm test tests/permissions/is green.
Context refs
lib/permissions/builtinRoles.ts— the three sets this card edits, and the comment that documents the twelve-key precedent.lib/permissions/resolve.ts—levelGrants, and its warning about adding a branch.tests/permissions/accessParity.test.ts— the 64-row table, hand-written on purpose.tests/permissions/catalog.test.ts— theENFORCED_PERMISSIONSlength pin this card must NOT move.assertPermissionand the twelve administrative keys — the shipped precedent for this exact move.- The decision — the assignment this card transcribes.