The work-item affordances ask the KEY — retire `canEdit` and `canManage` from the client context
Every work-item affordance in the product decides what to show from one of two booleans. canEdit answers may this person edit work items; canManage answers may this person administer the project — and canManage is what the ⋯ menu's Delete currently gates on, even though deleting a work item has had its own permission since the member-facing sweep wired work_item:delete. The affordances are right by coincidence for the three built-in roles and wrong for any role someone composes.
The client-set card put the real set in the context and kept the two booleans alive, derived, so that nothing had to change at once. This card spends that: every consumer asks can(<key>), and the booleans come off the context value.
The approach
- Each consumer's key is READ off the write it guards, never inferred from the boolean it replaces. For each component, find the server action or route its control invokes, grep the
assertPermission/assertCan*in that path, and use that key. The distinctions this surfaces are real and are the point of the card: applying an existing label to an item iswork_item:edit, while managing the project's label SET islabel:manage; deleting an item iswork_item:delete, notproject:administer. - The tighten-only fallback survives.
useProjectAccess()outside a provider answerstruefor every key, so a component mounted in a unit test keeps its ungated behaviour. That property is shipped and must not regress. - Treatment is unchanged. This card re-points WHICH permission an affordance consults; it does not turn a disabled control into a hidden one or the reverse. The 2026-06-09 in-place treatments — the disabled Create button and its
C/ ⌘K shortcut, board drag with its read-only banner, the inline field pickers — stay exactly as they render today, and panel 5 of the design asset draws them under that heading for exactly this reason. - Then the booleans go. With no consumer left,
canEditandcanManagecome off the context value, so the next component cannot reach for them.
Scope boundary
In: app/(authed)/_components/ProjectAccessProvider.tsx and every component that reads useProjectAccess(), plus their tests.
Out: the nav, the palette and the settings surfaces, which sibling cards own; the SERVER-side gates, which are already correct and are what this card reads FROM; any change to whether a control is hidden or disabled.
Acceptance criteria
- A grep of the repository for
canEditorcanManageread fromuseProjectAccess()returns nothing, and the two fields are gone from the context value's type — so the sweep is self-recounting and a consumer added between planning and running cannot be missed by a count. - Every re-pointed consumer names the key its own control's server gate asserts; the PR body carries the pairing table with the file and line of each gate that justified it.
work_item:delete— notproject:administer— gates the delete affordance wherever one is offered; a test asserts an actor holding delete but not administer is offered it.- Where a control's real key differs from the boolean it replaced, the change is called out individually in the PR body rather than folded into the sweep, because those are the only lines in this card that change behaviour for a built-in role.
useProjectAccess()with no provider still answerstruefor every key.- No control changes treatment: everything visible-and-disabled today is still visible-and-disabled, and nothing newly disappears — asserted for a viewer against the shipped board, issue-detail and list surfaces.
- The changed files meet the repo's per-file coverage floor.
Context refs
app/(authed)/_components/ProjectAccessProvider.tsx— the context this card narrows tocan()alone.app/(authed)/items/_components/WorkItemRowActions.tsx,app/(authed)/boards/_components/BoardCard.tsx,app/(authed)/items/archived/_components/ArchivedWorkItemsList.tsx— the three consumers readingcanManage, the ones whose key most likely changes.app/(authed)/items/[key]/_components/CoreFieldsPanel.tsx,LabelsCard.tsx,ComponentsCard.tsx,CustomFieldsSection.tsx,app/(authed)/items/_components/IssueInlineEdit.tsx,NewIssueButton.tsx,app/(authed)/_components/CreateIssueButton.tsx,ReportButton.tsx— thecanEditconsumers.lib/permissions/catalog.ts— the key set, and which of them are work-item scoped.lib/services/projectAccessService.ts—assertPermission, the gate each consumer's key is read from.- The design card — panel 5, the UNCHANGED in-place treatments this card must leave alone.