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

The permission CATALOG — `lib/permissions/catalog.ts`, its domain groups, and the totality guards that keep it honest

Add lib/permissions/catalog.ts — the single, code-owned, exhaustive list of the permissions Motir enforces. This card ships the vocabulary and nothing that consumes it; the policy re-expression is the next card.

Pure data and pure helpers, no IO — the same shape as lib/projects/access.ts and lib/mcp/scopes.ts, so it imports cleanly from the server, the client and a test.

The shape

  • PERMISSIONS — a frozen tuple of keys in resource:action form, the mirror's convention (Plane: workitem:edit; Jira's permission names read the same way). The set covers exactly what the eleven shipped predicates in lib/projects/access.ts decide today, one key per predicate, plus the umbrella project:administer: project:browse · work_item:edit · comment:add · comment:moderate · attachment:create · attachment:delete_any · watcher:manage · project:administer · public_request:submit · public_request:upvote · public_request:comment. Deriving the initial list from the shipped predicates rather than from the mirror's full catalog is deliberate — a key with no enforcement point behind it is a promise the product cannot keep.
  • PermissionKey — the union type derived from the tuple, so every consumer is exhaustive by construction.
  • A DOMAIN for each permission — the group the UI renders it under (project, work_item, comment, attachment, watcher, public_request), plus i18n label and description keys under a permissions. namespace in messages/en.json and messages/zh.json.
  • Helpers: isPermissionKey(value) narrowing an untrusted string; permissionsByDomain() returning the render order.

Naming note, so a later reader does not merge two vocabularies

lib/mcp/scopes.ts deliberately says "scopes, NOT permissions" — an API-token scope NARROWS its owner's role and is a separate axis. That comment is still correct after this card and gets more so; this card does not touch that module, and the two vocabularies stay distinct.

Acceptance criteria

  • lib/permissions/catalog.ts exports PERMISSIONS, the PermissionKey type, the per-permission domain + i18n keys, isPermissionKey and permissionsByDomain.
  • Every key in PERMISSIONS has a label and a description key present in BOTH messages/en.json and messages/zh.json — a Vitest guard asserts both catalogs, so a key added without its Chinese twin fails.
  • A Vitest guard asserts the catalog is a set: no duplicate key, every key matching the resource:action shape, every key carrying a domain that appears in the domain order.
  • A Vitest guard asserts every domain in the render order has at least one permission, so an emptied domain cannot survive as a heading with nothing under it.
  • The module imports no Prisma client and performs no IO — asserted by the existing import-boundary guard pattern used elsewhere in lib.
  • Per-file coverage on the new module meets the project's ≥90% branch/function/line floor.

Context refs

  • lib/projects/access.ts — the eleven shipped predicates the initial key list is derived from, one key each.
  • lib/mcp/scopes.ts — the frozen-tuple + derived-union + is… narrowing pattern this module follows, and the module whose header records why scope and permission are separate words.
  • messages/en.json, messages/zh.json — the two catalogs every new key needs an entry in.
  • The parent story — the model this vocabulary serves.