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-2466Done

The actor's permission SET reaches the client — `ProjectAccessProvider` carries the resolved set and a `can(key)` reader

Every client-side affordance decision the product makes today is made from two booleans. app/(authed)/_components/ProjectAccessProvider.tsx carries { canEdit, canManage }, resolved in app/(authed)/layout.tsx by projectAccessService.getSettingsCapabilities. Two bits cannot express a role that holds board:configure and not member:manage, so every sibling card in this story is waiting on the same thing: the actor's real permission SET, in the client tree, once per request.

The read already exists. The permission model story shipped projectAccessService.getPermissions and its serialisable getPermissionsDTO — an array in catalog order, because a Set cannot cross to a client island — and deliberately left the five getXCapabilities methods alone, noting that re-pointing them belongs with the surfaces that consume the set. This card is that re-pointing, for the one call the shell makes.

The approach, and why it changes no pixel

lib/projects/access.ts is already a thin layer over the set: canBrowse is hasPermission(i, 'project:browse'), canEdit is hasPermission(i, 'work_item:edit'), canManageProject is hasPermission(i, 'project:administer'). So the three booleans the layout spends a round trip on are derivable from the set by construction, and swapping the source is provably behaviour-neutral rather than merely believed to be.

  • The layout calls getPermissionsDTO instead of getSettingsCapabilities — one resolveInputs round trip, exactly as today. Not both: two calls would double the trip for no gain.
  • ProjectAccessProvider takes the key array and exposes can(key). canEdit and canManage stay on the context value, derived from the set, so not one of the ~15 components reading useProjectAccess() changes in this PR — retiring them is the affordance sweep's job, deliberately separated so this card is reviewable as a pure substitution.
  • The settingsAccess prop SidebarNav reads keeps its shipped { canBrowse, canManage } shape here, derived the same way. The registry's predicate is the settings-registry card's to change.
  • The no-provider fallback must keep its shipped direction. useProjectAccess() outside a provider answers { canEdit: true, canManage: true } today, so that a component mounted in a unit test keeps its pre-gating behaviour — the gate only ever TIGHTENS. The set-based reader inherits that: absent provider ⇒ can() is true.

Scope boundary

In: app/(authed)/layout.tsx, app/(authed)/_components/ProjectAccessProvider.tsx, and their tests. Out: every consumer of useProjectAccess() (unchanged here by design), the settings-nav registry, the command palette, the project nav, and any server-side gate — this card adds no enforcement and removes none.

Acceptance criteria

  • ProjectAccessProvider accepts the actor's permission keys and exposes can(key: PermissionKey): boolean; canEdit and canManage remain readable from useProjectAccess() and are DERIVED from the set rather than passed separately.
  • The authed layout resolves the actor's permissions with getPermissionsDTO and makes exactly one projectAccessService call for the active project: a grep for getSettingsCapabilities in app/(authed)/layout.tsx returns nothing.
  • A test proves the substitution is behaviour-neutral: across every combination of project access level × workspace role × project role, { canBrowse, canEdit, canManage } derived from the resolved set equals what projectAccessService.getSettingsCapabilities returns for the same actor.
  • useProjectAccess() with no provider present answers true for every key, preserving the shipped tighten-only fallback; a test asserts it.
  • The permissions cross the server/client boundary as the shipped ActorPermissionsDTO array in catalog order — deterministic ordering, no Set in a prop.
  • No component outside the two files above is modified: git diff --name-only lists only the layout, the provider, and test files.
  • The rendered shell is unchanged for all three built-in roles: the settings rail, the palette and every affordance render identically to the branch's own merge base — compare the branch against that baseline, which is a check this PR can run before it lands.
  • The new and changed lines meet the repo's per-file coverage floor.

Context refs

  • app/(authed)/_components/ProjectAccessProvider.tsx — the provider being widened, and its documented tighten-only fallback.
  • app/(authed)/layout.tsx — the single settings-capabilities round trip, around line 110.
  • lib/services/projectAccessService.tsgetPermissions, getPermissionsDTO, getSettingsCapabilities, and the note that re-pointing the capability methods belongs to this story.
  • lib/dto/permissions.ts + lib/mappers/permissionMappers.tsActorPermissionsDTO and its mapper.
  • lib/projects/access.ts — the three predicates whose equivalence to the set makes this substitution provable.
  • The permission model story — the story that shipped the read this card consumes.