11.2.10 `POST /api/v1/work-items/{key}/archive` and `/restore` — the reversible soft-remove, gated on `work_items:archive`
The only removal operation /api/v1 exposes, and the reason it can be exposed at all: archiving is a reversible soft-remove that does not cascade (workItemsService.archiveWorkItem sets archivedAt on ONE row and records a revision; children are untouched). The irreversible subtree delete stays unexposed — ADR §3 rejects it for the first cut, work_items:delete is off by default in DEFAULT_TOKEN_SCOPES, and exposing it later is additive under §8 while withdrawing it could not be.
Two routes: app/api/v1/work-items/[key]/archive/route.ts and …/restore/route.ts, both POST, both scope: 'work_items:archive' — its own scope, distinct from work_items:write, exactly as the ADR §3 map says. A token that may edit an item may not therefore remove it.
Scopes NARROW, never widen. The services gate on projectAccessService.assertCanEdit; the scope gate is an ADDITIONAL condition, so a work_items:archive token held by someone without project edit rights is still refused. Assert both directions — the scope without the role, and the role without the scope.
What to build
POST …/archive→archiveWorkItem(id, ctx), returning the updated resource with itsarchivedAtset.POST …/restore→unarchiveWorkItem(id, ctx), returning it witharchivedAtnull.- Neither service raises an already-archived / already-restored error — re-archiving simply re-stamps — so both endpoints are idempotent and say so in their schema description rather than inventing a conflict status the services do not produce.
- The only domain code either raises is
WORK_ITEM_NOT_FOUND→ 404, already mapped by 11.2.2; add no duplicate row.
Scope BOUNDARY
Ends at archive and restore of ONE item. It exposes no delete — and the story's test gate asserts that no /api/v1 route reaches deleteWorkItem at all, so the omission cannot be undone by accident. It does not archive a subtree (the service does not cascade, and a client wanting that walks the children itself), does not expose the archived-items LIST (listArchivedWorkItems serves the archive-management surface; a v1 archive collection is not in this story's endpoint set), and changes no service or repository.
Acceptance criteria
POST …/archivearchives the item and returns it with a non-nullarchivedAt;POST …/restoreclears it — both asserted by reading the row back.- Archiving does NOT touch children: an item with children is archived and every child's
archivedAtstays null. - An archived item disappears from
GET /api/v1/projects/{projectKey}/work-itemsand reappears after restore — the behaviour a client actually observes, asserted through the endpoints rather than the repository. - Both endpoints are idempotent: a second archive (or restore) succeeds with the same result.
- A token with
work_items:writebut NOTwork_items:archiveis 403 on both; a token withwork_items:archivewhose owner lacks project edit rights is also refused — the narrowing rule proven in both directions. - A cross-tenant or unknown key is 404 on both.
- Both routes compose
withV1Route, declarescope: 'work_items:archive', and touch no Prisma and no transaction. - The per-file coverage floor (≥90%) holds on every new file.
Context refs
lib/services/workItemsService.ts—archiveWorkItem/unarchiveWorkItem(single-row, revision-recording, non-cascading) anddeleteWorkItem, the one this story does not expose.lib/mcp/scopes.ts—work_items:archivevswork_items:delete, andDEFAULT_TOKEN_SCOPES.lib/mcp/tools/archiveWorkItem.ts— the shipped tool proving both service paths and the single-item scope.docs/decisions/public-api-conventions.md— §3's scope map and its rejected "expose delete in the first cut".- Producer: 11.2.2. Parent story: 11.2.