docs: settle removal/prune forks R-A..R-E; add owned-file manifest seam

R-A this-bank; R-B batched REAPER undo points (Phase-B-wide, SDK must-verify);
R-C trash-preferred w/ unlink fallback; R-D owned-file manifest, landed early as
new B-cap capture point; R-E manual action + bank_panel button.
This commit is contained in:
2026-07-24 04:59:28 -04:00
parent 960c5166ee
commit 878d807b12
3 changed files with 296 additions and 206 deletions
+101 -91
View File
@@ -8,9 +8,12 @@ authoritative technical detail in `CONTEXT.md` (§Sample removal, §Prune — fi
lifecycle). This doc holds the *why* — the workflow, the guardrails, the
index-vs-file boundary, and the forks that need a Daniel decision.
Status: framed by product-designer (2026-07-23). Sample-remove forks R-A/R-B and
prune forks R-C/R-D/R-E are surfaced below; **none are settled** — they need a
Daniel call before B5 / Phase R implementation.
Status: framed by product-designer (2026-07-23); **all five forks settled by Daniel
(2026-07-24)** — R-A this-bank-primary, R-B batched REAPER undo points
(Phase-B-wide), R-C trash-preferred-with-unlink-fallback, R-D owned-file manifest
(seam lands early in Phase B / capture), R-E manual action + panel button. The
decisions are folded into the fork sections below and into the B5 / Phase R spec
prose in CONTEXT.md and the tickable points in PLAN.md.
---
@@ -100,11 +103,13 @@ sharpnesses to guard:
still lives in another bank is cheap and reversible-in-spirit (re-copy it back)
and need not confirm. This makes the confirmation *earned* by actual risk rather
than fired on every remove.
- **Undo.** REAPER's own undo stack does not know about ext-state index mutations.
Whether remove is undoable — and how — is a genuine question shared with the
rest of Phase B (see fork R-B); it is not remove-specific, but remove is where
it bites first because remove is the first verb whose *only* effect is
destruction of an index entry with no relocation.
- **Undo.** REAPER's own undo stack does not natively cover ext-state index
mutations, so this is a Phase-B-wide decision (fork R-B, **settled**): bank/index
mutations integrate into REAPER's undo system as **batched undo points**
(`Undo_BeginBlock` / `Undo_EndBlock`), so one bank operation is one Ctrl-Z. Remove
is where the gap first bites — it is the first verb whose *only* effect is
destruction of an index entry with no relocation — but the fix is shared by every
Phase B verb.
### Where it lives
@@ -204,11 +209,14 @@ an OS trash — see fork R-C). It earns the strongest guardrails in the product:
attribute to the bank system's own leavings, not act as a general folder cleaner.
(This is a fork — R-D — because "how does prune know a file was ever ours"
depends on whether we track a manifest of owned files.)
- **Recoverability via the OS trash, if we can get it (fork R-C).** If prune can
route deletions to the platform recycle bin / trash instead of unlinking, an
accidental prune is recoverable outside the app. Whether SWELL/the platform
layer gives us a portable "move to trash" is a to-verify — but the *default*
should be the safest deletion the platform affords.
- **Recoverability via the OS trash (fork R-C, settled: trash-preferred).** Prune
routes deletions to the platform recycle bin / trash where a portable move-to-trash
is available, so an accidental prune is recoverable outside the app; it falls back
to unlink (behind the dry-run + confirm guardrail) only where the platform affords
no portable trash. Whether SWELL / the platform layer gives us that portable "move
to trash" is a to-verify per platform — but the *default* is the safest deletion
the platform affords, and "delete where possible" means recoverable-trash-preferred,
never plain unlink-by-default.
### Where it lives — and why it is its own phase, not a Phase B point
@@ -248,97 +256,99 @@ practically it should follow B5 so the two removal verbs ship as a coherent pair
---
## Open forks — need a Daniel decision
## Settled forks (Daniel, 2026-07-24)
### Sample-remove
**Fork R-A — remove scope: from-this-bank vs. from-everywhere vs. both.**
Does "remove sample" drop the entry from the bank in view (leaving copies in other
banks), purge every index entry across the whole book, or offer both as distinct
verbs?
- *from-this-bank* is the conservative, composable primitive (it is literally the
**Fork R-A — remove scope. SETTLED: THIS BANK (this-bank-primary).**
Remove drops the entry from the bank in view only, leaving copies in other banks
untouched. This is the core (and shipped) verb.
- *from-this-bank* is the composable primitive (it is literally the
`BankIndex::remove` the model already has); "from everywhere" is then "remove
from each bank that holds it," which the user can also achieve by removing per
bank. It matches the partition mental model (fork 3): a sample is in one bank, so
remove-from-this-bank usually *is* remove-from-everywhere.
- *from-everywhere* as a distinct verb only earns its keep once copy has scattered
a sample across several banks; then "purge this everywhere" is a real
convenience.
- **Product-designer lean:** ship **from-this-bank** as B5's core verb (it is the
model primitive, it composes, it matches the partition model), and treat
*from-everywhere* as an optional convenience layered on top *only if* the copy
workflow proves to scatter samples in practice. Design the seam (the action can
take a "scope: this-bank | all-banks" parameter) so from-everywhere is a
later flag, not a rewrite — but default the shipped affordance to this-bank.
**Needs Daniel:** confirm this-bank-primary, or elevate from-everywhere to a
first-class co-equal verb now.
- **Decision:** ship **from-this-bank** as B5's core verb and the only surfaced
affordance. Keep the `scope: this-bank | all-banks` seam in the action signature
as designed, but **this-bank is the settled default and the only shipped verb**;
all-banks stays a *latent parameter*, not a surfaced convenience — it can be
promoted later behind that seam without a rewrite if the copy workflow proves to
scatter samples in practice. (Settled 2026-07-24, confirming the product-designer
lean; from-everywhere is explicitly *not* elevated to a co-equal verb now.)
**Fork R-B — undo model for index mutations (shared across Phase B, surfaced by
remove).** REAPER's undo stack does not cover `"reasampler"` ext-state index
mutations. So move/copy/evacuate/delete-bank/**remove** are, as specced, not on
REAPER's Ctrl-Z. Options:
- *(i)* Accept it: index mutations are not undoable via REAPER undo; the safety net
is the confirmations + the fact that files survive (nothing is lost but the
entry, which the user can re-create by re-capture/re-move). Cheapest.
- *(ii)* A ReaSampler-internal one-step undo for the last bank/index mutation (a
single snapshot of the book restored on a "ReaSampler: undo last bank change"
action). Bounded, cheap-ish, covers the "oops" case for all Phase B verbs at
once.
- *(iii)* Hook into REAPER's undo system properly (`Undo_BeginBlock` /
`Undo_EndBlock` with ext-state as undo state) — most integrated, most work, needs
SDK verification that ext-state participates in undo blocks.
- **Product-designer lean:** this is bigger than remove and should be decided for
**all of Phase B at once**, not per-verb. Lean *(ii)* — a single-snapshot
internal undo of the last bank/index change — as the pragmatic middle: it turns
every index verb's "oops" into a one-key recovery without wading into REAPER's
undo internals. **Needs Daniel** (and it may retro-touch B1B4, so surfacing it
now rather than at B5 is deliberate — the seam is cheapest before B1 lands).
**Fork R-B — undo model for index mutations (Phase-B-wide, surfaced by remove).
SETTLED: BATCH UNDO POINTS (option (iii) — REAPER-integrated, batched).**
REAPER's undo stack does not natively cover `"reasampler"` ext-state index
mutations, so move/copy/evacuate/delete-bank/remove needed an undo story. Daniel
chose to integrate bank/index mutations into **REAPER's own undo system as batched
undo points** — the `Undo_BeginBlock` / `Undo_EndBlock` direction — batching the
related index mutations of one bank operation into a single undo point, so a bank
operation is one Ctrl-Z. The considered alternatives:
- *(i)* Accept no undo (rely on confirmations + files surviving) — **rejected**, too
weak once remove destroys an index entry with no relocation.
- *(ii)* A ReaSampler-internal single-snapshot "undo last bank change" — **rejected**
in favour of the more integrated (iii); the earlier product-designer lean toward
(ii) was overridden.
- *(iii)* **CHOSEN** — hook REAPER's undo system properly, batching related index
mutations into single undo points.
- **Scope — Phase-B-wide.** This is decided for **all of Phase B at once**, and it
**retro-touches B1B4**, not just B5: every index verb (create/rename/reorder/
delete-bank, move, copy, evacuate, remove) wraps its mutation in an undo block.
Surfaced with B1's open questions, not only at B5.
- **Must-verify-before-build (carry-forward).** The whole approach depends on
`"reasampler"` ext-state mutations participating correctly in
`Undo_BeginBlock`/`Undo_EndBlock` undo blocks. **Confirm against
`vendor/reaper-sdk` that ext-state changes are captured/restored by REAPER undo
blocks before building** — if they are not, the batched-undo-point approach does
not hold and the decision must be revisited. Flagged as a hard prerequisite.
(Settled 2026-07-24.)
### Prune
**Fork R-C — deletion mechanism: unlink vs. OS trash.** Does prune `remove()` the
file (irreversible) or route to the platform recycle bin/trash (recoverable
outside the app)? Trash is the safer default; the question is whether SWELL / the
platform layer offers a portable move-to-trash, or whether we'd hand-roll it per
platform (Win `SHFileOperation`/`IFileOperation`, macOS `NSFileManager
trashItemAtURL`, Linux XDG trash spec). **Needs Daniel + a to-verify** on platform
support. Lean: **trash if portably available, unlink with a strong dry-run+confirm
guardrail if not.**
**Fork R-C — deletion mechanism: unlink vs. OS trash. SETTLED: TRASH-PREFERRED,
UNLINK FALLBACK.** Prune routes deletions to the platform recycle bin / trash
(recoverable outside the app) **wherever the platform affords a portable
move-to-trash**, and falls back to unlink — behind the dry-run + confirm guardrail —
only where it does not. Trash is the settled default; "delete where possible" reads
as *recoverable-trash-preferred*, never plain unlink-by-default.
- **To-verify (carried, per platform):** whether a portable move-to-trash exists via
SWELL, or must be hand-rolled per platform — Win `SHFileOperation`/`IFileOperation`,
macOS `NSFileManager trashItemAtURL:`, Linux XDG trash spec. The move-to-trash
surface is an explicit to-verify before use, not an assumed capability. (Settled
2026-07-24.)
**Fork R-D — orphan attribution: manifest-tracked vs. index-diff vs.
folder-sweep.** How does prune know a file in the bank folder is a *reclaimable
orphan* (a bank file no index references) rather than *a file that was never
ours* (a user dropped it in by hand)?
- *(i) folder-sweep* — treat every file in the bank folder not referenced by the
book as an orphan and reclaim it. Simplest; **dangerous** — it will delete a
user's hand-placed file, violating "only reclaim our own leavings."
- *(ii) index-diff only* — never sweep the folder; instead, capture the file's
identity at the moment its *last* index reference is dropped (remove/delete-bank)
and prune only that recorded set. Safe (only ever touches files we know we
orphaned) but requires recording orphans-at-drop-time.
- *(iii) owned-file manifest* — the book tracks the set of files it has ever
created (capture writes to it); prune reclaims `(owned ∩ on-disk) referenced`.
Safest and most general (distinguishes "our orphan" from "user's file" and from
"already-gone"); costs a manifest that rides the ext-state.
- **Product-designer lean:** *(iii) owned-file manifest* — it is the honest
encoding of "prune reclaims only the bank system's own leavings," and it is a
**design-the-seam-now** call: the manifest is cheap to maintain from capture
onward but expensive to reconstruct retroactively (you cannot tell, later, which
folder files were ever ours). If we defer prune but *don't* start tracking owned
files now, we build a backfill cliff. **Strong recommendation: start the owned-
file manifest as part of Phase B / capture even though prune ships in Phase R** —
defer the feature, design the seam. **Needs Daniel** because it touches the
persisted shape (a new tracked set) and ideally lands earlier than Phase R.
folder-sweep. SETTLED: OWNED-FILE MANIFEST — and the seam lands EARLY (Phase B /
capture).** The book tracks the set of files it has created; prune reclaims
`(owned ∩ on-disk) referenced`. The considered alternatives:
- *(i) folder-sweep* — reclaim every unreferenced file in the folder. **Rejected**
it would delete a user's hand-placed file, violating "only reclaim our own
leavings."
- *(ii) index-diff only* — record a file's identity when its *last* index reference
drops and prune only that set. Safe but partial (misses files orphaned outside a
tracked drop path). Not chosen.
- *(iii) owned-file manifest* — **CHOSEN.** Safest and most general: distinguishes
"our orphan" from "user's file" and from "already-gone."
- **Seam lands early (accepted design-the-seam-now call).** The manifest is cheap to
maintain from capture onward but a **backfill cliff** to reconstruct later — you
cannot tell, after the fact, which folder files were ever ours. Daniel accepted the
recommendation to **start the owned-file manifest at capture time NOW, in Phase B,
even though prune (which consumes it) ships in Phase R.** So: **capture writes each
file it creates into an owned-file manifest persisted in the `"reasampler"`
ext-state**, and Phase R's R1/R2 *consume* that manifest. The exact persistence
shape — a sibling ext-state key vs. folded into the `banks` blob — is a small
residual to settle at build; the **manifest-now decision is firm**. (Settled
2026-07-24; the up-front point is added to Phase B / the capture path in PLAN.md.)
**Fork R-E — prune trigger: manual-only vs. offer-on-orphaning vs. periodic.**
When does prune run? Manual action only (user invokes "Prune bank folder"),
prompted opportunistically (after a non-empty delete-bank: "…evacuate, or delete
and prune now?"), or a background/periodic sweep? Lean: **manual-only as the core
(an explicit, dry-run-first action the user invokes deliberately)**; an *optional*
"…and prune now" affordance offered at the delete-bank confirmation as a
convenience, never a background sweep (silent irreversible file-deletion is exactly
what the guardrails forbid). **Needs Daniel** to confirm manual-primary and
whether the delete-time "prune now" convenience is wanted.
**Fork R-E — prune trigger: manual-only vs. offer-on-orphaning vs. periodic.
SETTLED: MANUAL ACTION + PANEL BUTTON.** Prune runs via a bindable manual action
(dry-run-first, confirm-to-delete) **and** a button in the `bank_panel` that fires
that same action. No background sweep. The earlier optional "…and prune now at the
delete-bank confirmation" convenience was **not** selected — it is dropped from the
settled spec (explicitly out of scope). A periodic/background sweep remains rejected
(silent irreversible file-deletion violates the guardrails). So R3 gains a
`bank_panel` button affordance alongside the action registration. (Settled
2026-07-24.)
---
@@ -350,5 +360,5 @@ whether the delete-time "prune now" convenience is wanted.
| Side of the line | index | index | **file** |
| Deletes bytes? | no | no | **yes (only op that does)** |
| Produces orphans? | yes (last-ref) | yes (non-empty) | — (it *reclaims* them) |
| Reversible? | re-add/re-capture entry | re-create bank + re-add | **no** (unless trash, R-C) |
| Reversible? | Ctrl-Z (batched undo, R-B) / re-capture | Ctrl-Z (batched undo, R-B) / re-create | **no in-app** (recoverable via OS trash, R-C) |
| Guardrail | confirm on last-ref | confirm on non-empty | dry-run + manifest confirm |