`work_item:triage` + `work_item:delete` — accepting an inbound request, and destroying a subtree, stop being the same permission as editing a field
Two keys whose operations all reach a gate today, so the guard reports nothing — and the gates are the wrong ones. This card is a re-pointing with three corrections the inventory does not carry, each read off origin/main.
work_item:triage — the moderation acts, not the submission
triageService gates with assertCanBrowse (4 sites) and assertCanEdit (2). The inventory maps both its routes to work_item:triage, and one of those rows is wrong: createSubmission is somebody SUBMITTING an inbound request, which the catalog already governs with public_request:submit — Plane draws exactly this line, letting a Guest or Commenter submit an intake item while only Admin and Contributor accept or decline one. Triage is what happens to a submission after it arrives.
acceptTriageItem,promoteTriageItem,declineTriageItem,markDuplicateTriageItem,snoozeTriageItem,unsnoozeTriageItem→work_item:triage.getTriageQueue,getTriageQueueByKey,getTriageItemDetail→work_item:triageas well: the queue is a moderation surface, and its contents are requests from outside the team that have not been accepted yet.createSubmission→ stays where the catalog already puts submitting, and its inventory row is CORRECTED topublic_request:submit.
work_item:delete — and the hard delete the inventory files under work_item:edit
workItemsService.archiveWorkItem/unarchiveWorkItem(assertCanEdittoday) andgetDeletePreview(assertCanManagetoday) →work_item:delete.DELETE /api/work-items/[id]is mapped towork_item:editin the inventory while its preview is mapped towork_item:delete. A destroy and its own dry run cannot be governed by different keys — the row is corrected towork_item:delete, and the service method that performs it moves with it. This is the third correction, and it is the one that matters: today the preview is admin-gated and the delete itself is looser.
Jira's default permission scheme grants Delete Issues to the Administrators project role, which is what docs/decisions/member-facing-permissions.md follows: a member keeps every edit and loses the cascade.
Acceptance criteria
- Each triage moderation method asserts
assertPermission(projectId, ctx, 'work_item:triage');createSubmissionkeeps a submit-shaped gate and does NOT assert the triage key. archiveWorkItem,unarchiveWorkItem,getDeletePreviewand the hard-delete service method each assertwork_item:delete.- A unit test proves a project
membercan still edit a work item and can no longer archive or delete one, and that aviewercan neither triage nor delete. - The two corrected inventory rows (
triage/submissions→public_request:submit,DELETE /api/work-items/[id]→work_item:delete) each carry a one-line note saying what was corrected and why, so the change is not silent. - Both keys flip to
enforcement: 'enforced'inlib/permissions/catalog.ts, withtests/permissions/catalog.test.ts's enforced list and length pin extended by exactly these two. - The guard's pinned counts are re-derived by running it on this branch — this card gates operations that already read as gated, so the
PENDINGfigure may not move at all, and that is the expected result rather than a sign nothing happened. pnpm test tests/permissions/ tests/services/is green.
Context refs
lib/services/triageService.ts— the ten methods and their currentassertCanBrowse/assertCanEditsplit.lib/services/workItemsService.ts—archiveWorkItem,unarchiveWorkItem,getDeletePreview, and the hard delete.docs/decisions/permission-inventory.md— theR23andR42rows, plus theR41row this card corrects.docs/decisions/member-facing-permissions.md— the role assignment for both keys.- The role-set seam — the prerequisite that makes both keys resolve.
comment:moderate's shipped shape — the model story's precedent for a moderation key that is narrower than editing.