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:
@@ -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 B1–B4, 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 B1–B4**, 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 |
|
||||
|
||||
Reference in New Issue
Block a user