The settings registry gates on named permissions — the rail, the ⌘K deep links, and the area door that disappears when nothing is behind it
This is the card the parent story is named for: a member stops seeing settings sections they cannot change anything in, and when none is left, the Project settings door goes with them.
lib/settings/projectSettingsNav.ts is already the single registry three surfaces read — the rail, the ⌘K deep links, and the route↔registry totality test — so the gate does not need building, only re-pointing. Today every entry but automation gates on browse, and the module's own comment records the intent it is time to retire: "a member VIEWS every section". The entry for Roles says it in as many words: "Turning that into a permission predicate is MOTIR-2258's job, not this card's."
The approach
- The predicate takes the actor's permission SET.
SettingsNavCapabilitiesis replaced by the resolved set from the client-set card; each entry names thePermissionKeyits destination requires. - Every pairing is VERIFIED, not assigned by name similarity. For each entry, open its destination and read the key its own server gate asserts —
board/page.tsxandboardsServicefor Board,projectAiSettingsServicefor AI planning,automationRulesServicefor Rules, and so on — and name that key. Where a destination gates on more than one key, the entry names the one that makes the page useful at all, and the notes say which and why. Two entries need a judgement rather than a lookup and must state it on the card: Details, whose page is the project-level acts that belong to no domain, and Roles, whose page is a read-only view of what every role can do — decide each against that page's own gate and record the reasoning in the PR body. - The rail renders only non-empty groups.
groupSettingsNavalready drops empty groups; the card's job is to prove it, because a heading with nothing under it reads as a loading failure (design panel 2). - The AREA door. The bottom-nav Settings row deep-links to
/settings/projectwhen a project is active. It is hidden when the actor's visible settings set is empty. With no active project the row still points at workspace settings — that path is untouched, because workspace settings are governed by the workspace role this epic does not change. - The palette follows the registry. Its settings block is a
visibleSettingsNavcall; update the call site to pass the set and nothing else. The palette's OWN action entries — create issue, plan with AI, plan sprints — belong to the nav-and-palette card.
Scope boundary
In: lib/settings/projectSettingsNav.ts, app/(authed)/_components/SidebarNav.tsx, the visibleSettingsNav call site in app/(authed)/_components/AppCommandPalette.tsx, the layout prop that feeds them, their i18n keys, and their tests.
Out: the destination PAGES' own render guards — a hidden entry whose page still renders a read-only form is the destination-guard card's to fix, and this card must not touch app/(authed)/settings/project/**/page.tsx; the palette's non-settings actions and the project nav rows (the nav-and-palette card); every service and route gate, which this story does not change.
Acceptance criteria
- Every entry in
PROJECT_SETTINGS_NAVnames aPermissionKey, and a grep oflib/settings/projectSettingsNav.tsforcanBrowseorcanManagereturns nothing. - A test walks
PROJECT_SETTINGS_NAVtotally — a new entry with no declared key fails to compile or fails the test — and asserts each entry's key against the key that destination's own gate asserts. - Fed a set holding
project:browseandboard:configureand no other administrative key,visibleSettingsNavyields Board and omits Members; fed a built-inmember's set it yields nothing. groupSettingsNavemits no group whose entries all filtered away, asserted directly.- With an active project and an empty visible settings set,
SidebarNavrenders no Project settings row; with no active project the row still targets/settings/workspace, unchanged. AppCommandPaletteoffers no settings deep link an actor cannot use, and holds no second copy of the settings list — its block is built fromvisibleSettingsNav.- The shipped route↔registry totality test stays green and still pairs every on-disk
settings/project/**/page.tsx1:1 withPROJECT_SETTINGS_ROUTE_PATHS, nested routes included. - The rail and the bottom nav match panels 1 and 2 of
design/projects/permission-gated-ui.mock.html. - An admin's rail is byte-for-byte the rail that ships today — every entry present, in the same order and groups.
- Every new
enmessage key has itszhtwin. - The changed files meet the repo's per-file coverage floor.
Context refs
lib/settings/projectSettingsNav.ts— the registry, itsaccesspredicate,visibleSettingsNav,groupSettingsNav, and the Roles entry's note deferring this work here.app/(authed)/_components/SidebarNav.tsx— the settings-area branch (~line 183) and the bottom-nav Settings row (~line 320).app/(authed)/_components/AppCommandPalette.tsx— thevisibleSettingsNavcall (~line 282).tests/settings/projectSettingsNav.test.ts— the totality guard that must stay green.lib/permissions/catalog.ts— the key set each entry draws from.- The client-set card — the provider this card reads the set from.
- The design card — panels 1 and 2 are this card's reference.