docs(pS-usage): record instance-usage detection — un-prunable guarantee, wire/liveness/fail-safe, deferred identity-persistence follow-up
This commit is contained in:
+106
@@ -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
|
||||
`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)
|
||||
|
||||
Reference in New Issue
Block a user