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

`getRoleCatalog` grows what the drill-down actually reads — the role-gated row set, the level-gated card beside it, the per-role member count, and the `N of M` the list row shows

Widen the read behind the Roles & permissions screens so the page card can render the asset MOTIR-2259 shipped. Carved out of the page card on 2026-08-07 (see the story's re-plan note): the design draws values the shipped DTO cannot express, so the page has no data source for its own list screen.

⚠️ AMENDED 2026-08-08, mid-run, by the card's own implementation. A FOURTH gap of the same class surfaced while reading the mock's panel 0 against this card: the list screen draws a level-gated card beneath the roles, and nothing produced its rows either. This card's original text NAMED those keys — "the level-gated public_request:* keys … which get their own card" — and then assigned that card no producer, which is precisely the shape the planning bug records. Fixed here rather than in the page card, because the page card's scope boundary forbids it to touch lib/dto, lib/mappers, lib/repositories or lib/services — and the alternative (a settings component importing the catalog to decide which keys are level-gated) would put a second definition of "what the screen means" in the UI layer, the exact thing this card's rationale argues against.

The four gaps, each read off origin/main at 1273ff32

  1. The row set is the ROLE-GATED set, not the whole catalog. lib/mappers/permissionMappers.ts's toPermissionDomainDTOs() calls permissionsByDomain() with no options, which returns every key in PERMISSIONS — 31 today. The screens draw the role-gated set (ROLE_GATED_PERMISSIONS in lib/permissions/builtinRoles.ts, 28 keys today). The difference is the level-gated public_request:* keys, which no role can hold or withhold — rendering them as role rows would draw a permission nobody can ever be granted. permissionsByDomain offers include: 'enforced' | 'all' and no role-gated arm.
  2. RoleDTO carries no member count. The list row reads 3 members beside each role. projectMembershipRepository has countAdmins and no grouped-by-role count; nothing else in lib/services/projectAccessService.ts exposes one.
  3. Nothing carries the M in N of M. The row reads 10 of 28 permissions. N is RoleDTO.permissions.length; M is the size of the role-gated set, which the client must not re-derive by importing the catalog — the whole point of the service read is that MOTIR-2257 makes the answer project-scoped.
  4. Nothing carries the LEVEL-GATED card's rows (added 2026-08-08). Panel 0 of roles-permissions.mock.html ends in a Public requests card — the mock's own CSS comment calls it "the level-gated card — three rows, kept OUT of the role list" — explaining that those grants come from the project's access level rather than any role. Without it the screen answers "what can a role do" and silently drops three permissions the product does govern, which is the under-description the epic exists to fix.

Filter by the CONSTANT, never by a literal. ROLE_GATED_PERMISSIONS grew from 20 to 28 when MOTIR-2349 added the eight member-facing keys; a hardcoded 28 would have been wrong then and right by accident now.

Scope

In: the DTO shape, the mapper (both groupings), the grouped membership read, and the getRoleCatalog thread-through, with their unit + integration tests.

Out: the page and its components (MOTIR-2263); custom roles making the catalog genuinely project-scoped (MOTIR-2257); deciding or wiring the eight member-facing keys (MOTIR-2291). This card changes no permission's grant and no role's set.

Acceptance criteria

  • permissionsByDomain gains a role-gated arm (an include: 'role_gated' option or equivalent) that narrows to ROLE_GATED_PERMISSIONS, drops domains left empty by the filter, and preserves catalog order; the existing 'enforced' and 'all' arms are unchanged.
  • ⚠️ The arm must NOT make lib/permissions/catalog.ts import anything. tests/permissions/catalog.test.ts asserts "has no import at all outside the module (it is a leaf)", so a named 'role_gated' arm — which would have to import ROLE_GATED_PERMISSIONS from builtinRoles.ts — is the one shape that is closed. Take the key SET as the option's value and let the mapper pass the constant; do not relax the purity guard.
  • PermissionDomainDTO groups produced for the role screens contain exactly the keys in ROLE_GATED_PERMISSIONS, asserted against that constant rather than a literal count, so the assertion stays true when the constant grows.
  • RoleCatalogDTO ALSO carries the level-gated groups the role rows leave out, derived as the complement of the role-gated set over the catalog rather than a second hand-written key list; the two groupings together cover every catalog key exactly once, asserted.
  • RoleDTO carries a memberCount, resolved from a single grouped read over ProjectMembership for the project — not one query per role.
  • RoleCatalogDTO carries the role-gated total the list row's N of M needs, so no client re-derives it by importing the catalog.
  • projectMembershipRepository gains the grouped count as a single-Prisma-op leaf taking an optional tx, matching the 4-layer contract in motir-core/CLAUDE.md; a role with zero members resolves to 0, never a missing key.
  • getRoleCatalog returns the widened DTO through its existing 404-not-403 gate, and the integration test proves a project in another workspace still raises ProjectNotFoundError before any count is read.
  • An integration test on real Postgres seeds a project with a membership at each role and reads the counts back through getRoleCatalog, asserting the numbers against the seeded rows.
  • Per-file coverage on every file this card changes meets the ≥90% floor.

Context refs

  • lib/permissions/builtinRoles.tsROLE_GATED_PERMISSIONS and BUILTIN_ROLE_PERMISSIONS, the row set and the marks.
  • lib/permissions/catalog.tspermissionsByDomain, PERMISSIONS, sortByCatalogOrder.
  • lib/dto/permissions.tsRoleDTO, RoleCatalogDTO, PermissionDomainDTO.
  • lib/mappers/permissionMappers.tstoRoleCatalogDTO, toBuiltinRoleDTO, toPermissionDomainDTOs.
  • lib/services/projectAccessService.tsgetRoleCatalog and its resolveInputs gate.
  • lib/repositories/projectMembershipRepository.ts — where the grouped count lands; countAdmins is the nearest existing shape. lib/repositories/workItemRepository.ts is a groupBy precedent in this layer.
  • design/projects/design-notes.md — the Roles & permissions section; the list row's N of 28 permissions + 3 members, the 28-row table, and the level-gated card.
  • The design · the page that consumes this · the service read this widens.