docs(pS-usage): record instance-usage detection — un-prunable guarantee, wire/liveness/fail-safe, deferred identity-persistence follow-up

This commit is contained in:
2026-07-28 14:21:40 -04:00
parent c5f7d0e21f
commit 18dab9f150
2 changed files with 115 additions and 7 deletions
+9 -7
View File
File diff suppressed because one or more lines are too long
+106
View File
@@ -1943,6 +1943,112 @@ preview velocity **persists** via envelope-v6 `ComponentState`; the envelope ove
drawn needle and the drag hit-test can never drift — the same one-formula discipline as drawn needle and the drag hit-test can never drift — the same one-formula discipline as
`curveBoxFromRect`. `curveBoxFromRect`.
## Instance-usage detection — the un-prunable guarantee (pS-usage, 2026-07-28)
> **Additive sub-phase of Phase S.** Adds a new safety seam between the VST3 instrument
> and the extension's prune path: a live ReaSampler 9000 instance holding a capture
> makes that capture un-prunable. Build detail and module architecture: CLAUDE.md
> §Architecture. The pure core (`sample_usage`) and its tests (`sample_usage_tests`) are
> REAPER-free; the REAPER-facing shell (`usage_scan`) is read-only at prune-scan time.
### The guarantee
A capture held by any live ReaSampler 9000 FX instance in the project **can never be
deleted by `BANK_PRUNE_FOLDER`**. If the prune cannot determine with certainty which
captures are held — because any usage record is unreadable or ambiguous — the prune
**aborts entirely (deletes nothing)**. Over-protection (prune skips a reclaimable file
or refuses to run) is the accepted residual; under-protection (deleting a file an
instance may still be playing) is a data-loss bug.
### The wire: `rsusage_<instanceGuid>`
Each VST3 instance holds a **per-instance GUID** persisted in `ComponentState` v11
(`instanceGuid` field; pre-v11 blobs mint the guid on first publish). At the tail of
every `reloadInstrument` call (off audio thread) the processor publishes its held
`SampleRefs` paths to the ext-state key `rsusage_<instanceGuid>` in the `"reasampler"`
namespace via `reaper_bridge::writeUsageExtState`. That entry point is **prefix-guarded**
— it accepts only `rsusage_`-prefixed keys and refuses all others, so the read-only-bank
invariant is structurally enforced.
**Direction:** the instrument writes usage keys; the extension reads them. This is the
one sanctioned instrument→ext-state write (a deliberate exception analogous to
`assignment_request` on the other wire), and it never touches the bank, view, tail, or
any other extension-owned key.
### Liveness — no teardown clearing, no challenge/response
Usage records are **never cleared by the instrument** at teardown: REAPER destroys the
plugin instance when an FX chain is set offline (including Design View's CPU-park), so
a terminate-time clear would strip the record of an instance that still exists in the
project. Liveness is decided extension-side at prune-scan time by cross-referencing the
usage records against the live FX enumeration (`usage_scan`).
### The prune-scan fold
At prune-scan time `usage_scan` (the REAPER-facing shell):
1. Enumerates every `rsusage_*` ext-state key and decodes each record. A present-but-
unreadable record sets `abortPrune` — halting the prune, deleting nothing.
2. Enumerates every ReaSampler 9000 FX instance in the project: all tracks (master
included), normal + record/input chains, FX containers recursively, and take FX.
Truncated/partial enumeration → `abortPrune`.
3. Folds via `sample_usage::foldUsageRecords` / `usageHeldPaths` (pure, provable
without a DAW): a record counts iff its publishing track still hosts at least one
instance (offline FX included); a record with no track context counts while any
instance exists; and when records exist but zero instances were identified,
**every** record's paths are protected (identity-failure net — a matcher failure
must never degrade toward delete).
The resulting held-paths set feeds `prune_reconcile::mergeReferenced`, which unions it
into the bank-referenced set. The orphan computation is therefore
`(owned ∩ present) (bankRefs liveInstanceHolds)`.
### Collision safety (FX copy / track duplication)
A persisted GUID is copyable. Two design mechanisms close the copy gap in the
fail-safe direction:
- **`ownerNonce`** — a per-lifetime nonce minted fresh in memory at instance creation,
never persisted. Proves "exactly this incarnation wrote the key last."
- **`unioned` flag** — a sticky multi-writer poison. Once a same-track sibling is
detected, the key enters union-forever mode: holds only accumulate, never drop.
The publish plan (`planUsagePublish`) resolves every collision toward over-protect:
same-nonce + not-unioned → clean replace; same-track foreign nonce or unioned → union;
cross-track foreign nonce → remint under a fresh key. All three directions over-protect
at worst; none can under-protect.
### Fail-safe summary
| Situation | Outcome |
|---|---|
| Normal: record readable, instance live | Held paths added to `referenced` |
| Record readable, instance gone (stale key) | Paths excluded — not protected |
| Records exist, zero instances identified | ALL records' paths protected |
| Any record unreadable | Prune aborts — deletes nothing |
| Same-track copy detected | Union of holds; `unioned` flag set forever |
| Cross-track copy detected | Remint under fresh key |
### `actions` integration
`BANK_PRUNE_FOLDER` checks `PruneReport.abortedUnreadableUsage`; when set it halts
before any deletion and prints the offending `rsusage_*` key names with instructions
(the keys name which FX instances need attention). This is the only user-visible surface
of an abort — the dry-run path shows the same abort signal before any confirm step.
### Deferred follow-up
**Persist instance identity (TODO.md):** the per-instance `ownerNonce` is minted fresh
each incarnation and is NOT persisted. After save→reopen an instance cannot recognize its
own prior-session usage record — it looks foreign, so the instance unions and marks the
record `unioned` (append-only) forever. Net effect: after any reopen, prune stops
reclaiming captures an instance once held but no longer uses. Safe (never deletes a live
capture), but the bank folder grows without bound. The fix — persist the nonce in
`ComponentState` so an instance recognizes its own last-session record and does a
clean-replace — is deferred because a persisted nonce is inherited by a Ctrl+D in-place
FX duplicate, and a divergent clone must still be detected and protected fail-safe without
reintroducing the sibling-drop bug.
--- ---
# Look-and-feel — visual design language (Phase L) # Look-and-feel — visual design language (Phase L)