motir-core: the retire mutation route + client, guarded by the retire permission
The core half of the write: client methods for retire and un-retire, and the mutation route that calls them.
Guarded by the RETIRE permission, not the view one. The two keys exist precisely so a role can read the list without changing what the planner does; checking the wrong key here collapses that distinction on the first call site and nobody notices, because an admin holds both.
Check before calling, as the read route does — a caller without the permission is refused and no upstream request is made.
Pass the acting user through. The upstream records who retired the lesson, and it can only do that if core sends it. This is the field most likely to be dropped as "not needed to make it work", and its absence is invisible until somebody opens the detail view and finds an audit line that says nothing.
Map the upstream's typed refusals to something the UI can render — a global lesson refused, a lesson in another project, a lesson that no longer exists. A generic 500 for a refusal the product deliberately makes is a design failure one layer down.
⚠️ Amendment (2026-08-23) — two client methods over one axis, and the server picks apply's value
Authored 2026-08-21; the parent story MOTIR-3330's 2026-08-23 exempt from retirement amendment outranks this card's original wording.
The upstream (MOTIR-3344) exposes retire and apply over one humanOverride axis and decides apply's value itself from the row's clock. So core sends no override value: retireLesson and applyLesson carry the acting user and nothing else. The retire permission is lesson:manage (MOTIR-3336); lesson:view must not satisfy it.
Upstream refusals to map: not_found (unknown id, another project's id, and a global lesson — deliberately the same answer) and the unavailable/config arms projectLessonsService already degrades on for the reads.
Acceptance criteria
- Client methods for retire and un-retire, matching the neighbouring methods' shape and error handling.
- The route checks the retire permission specifically; a caller holding only the view permission is refused, asserted.
- No upstream call is made when the permission check fails, asserted on the call count.
- The acting user is sent upstream and appears on the read-back, asserted end to end.
- Upstream typed refusals surface as distinguishable, renderable errors — not a generic failure.
- Route tests cover the guard, the pass-through and each refusal; no UI in this card.
Context refs
motir-corelib/ai/motirAiClient.ts— the client conventions.motir-corelib/permissions/resolve.ts— the check.- MOTIR-3336 — the two keys, and why the retire one is the right one here.
- MOTIR-3344 — the upstream endpoint and its typed errors.
- MOTIR-3337 — the read seam this mirrors.