# Runbook — Yemaya study workspace: rights expiry and the retention sweep

**Checklist:** YSD-22058 (`expiry`) · drill kind `expiry` · YSD-3062 **Decision
owner role:** the rights owner for the study workspace **Machinery:**
`apps/yemaya/svc-study-workspace/src/persistence/expiry-enforcement.integration.spec.ts`

This one had no nearest guidance at all — not a shortfall in another document,
nothing. It is also the procedure most likely to be run under someone else's
deadline, because a lapsed grant is a rights question before it is an
operational one.

**Running this document IS the drill.** Record it in
`procedure-exercise-register.json`.

## The two boundaries, which are not the same day

A grant lapsing and its bytes being removed are separate events, and confusing
them is how a workspace either keeps material it no longer has rights to, or
destroys material it was still allowed to hold.

| Boundary                        | What happens                                               | Exercised on every change |
| ------------------------------- | ---------------------------------------------------------- | ------------------------- |
| **The lapse**                   | Every surface suppresses the work **with no reprojection** | yes                       |
| **End of the retention window** | The sweep removes bytes and schedules derivative cleanup   | yes                       |

**"With no reprojection" is the load-bearing phrase.** Suppression is decided at
read time against the live grant, not by rewriting projections at the boundary.
So there is no reprojection job to watch, and **a work still appearing after its
lapse is not a stale index — it is a read path that did not ask.** That is the
finding, and it is a defect rather than a backlog.

## Before the lapse

While licensed, the work surfaces everywhere and its media is embedded. Nothing
to do.

## At the lapse

1. **Confirm suppression on the read surfaces**, not in the database. The
   database still holds the work — that is correct and is not a leak.
2. **Confirm queued embedding work enqueued before the lapse is CANCELLED at
   drain time, not failed.** Cancellation is the correct outcome: the job was
   legitimate when queued and is not permitted now, and a failure would page
   somebody about a system working properly.
3. If any surface still shows the work, that read path does not consult the
   grant. Treat it as a rights incident, not a cache problem.

## At the end of the retention window

4. The sweep removes bytes and schedules derivative cleanup. Derivatives
   (`proxy`, `caption`, `thumbnail`, `crop`, `waveform`, `stem`,
   `analysis-artifact`) are purged with the original they came from.
5. Confirm the original is gone from object storage **and** that its derivatives
   were scheduled — a derivative that outlives its original outlives the rights
   basis it was made under.

## The objective

**Expiry time objective: 45 minutes**, from the lapse being noticed to "Done
means" being satisfied. Set 2026-08-15.

The five steps above are not budgeted individually because four of them are the
same act — confirming one more read surface — and a per-surface budget would
invite skipping the last one to make the total.

Measured, and the measurement is a **floor**. `scripts/time-expiry.ts` crosses
both boundaries against a running deployment **in real time**: it writes a grant
with a `validUntil` twenty seconds out and a zero-day retention window, waits
for wall-clock time to pass it, and then reads every surface. No clock is moved
and no record is edited to look expired, which is the stronger version of this
drill's own verification. Against the deployment on 2026-08-15: the five read
surfaces answered in **0.03 seconds** and the sweep took **0.05**.

Forty-five minutes is therefore for the operator: confirming the suppression on
each surface rather than one, and deciding — at step 3 — whether what they are
looking at is a rights incident. That decision is the procedure.

**What the run found, and it is the good news:** at the lapse, with no sweep
run, playback refused, `allowed-actions` dropped `playback`, and search returned
**zero hits while naming the work in `excludedWorks` with the reason
`grant … expired at …`**. That is the read path consulting the live grant, which
is what "with no reprojection" means, and it is worth writing down that the
honest refusal carries the work's id — a check written as "the work id does not
appear in the response" fails against a system that is behaving correctly, and
would report a rights leak where there is a reason being given.

**The library is not one of the suppressed surfaces, and should not be.**
`GET /sources` is an inventory: an operator whose grant lapsed has to see the
work in order to renew it. It carries a rights summary, and after the lapse that
summary reads `{ active: 0, expired: 1 }`. Confirm the summary, not the absence.

## Detection

The sweep is scheduled, so the failure mode is silence rather than an alert: **a
sweep that did not run leaves expired material in place and says nothing.**
Check that the sweep ran within its interval before concluding that nothing was
due.

## Communication

| Audience                                                            | Within   |
| ------------------------------------------------------------------- | -------- |
| The rights owner, when a grant lapses on material in active use     | 24 hours |
| The learner, when their in-flight work referenced the lapsed source | 24 hours |

## Done means

- Every read surface suppresses the work.
- Queued work is cancelled, not failed.
- After the window: bytes gone, derivatives scheduled, tombstone in place.
- Recorded in the procedure exercise register.

## Known shortfall

Both boundaries are driven against real stores by the integration suite. What no
drill has done is cross them **on material somebody was actually using at the
time** — the lapse mid-session case, where a learner has the work open. The
suite crosses the boundary; a person has not.
