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
+93 -63
View File
@@ -771,13 +771,12 @@ model capability.
container. Individual samples **can** be removed from the pool — otherwise the container. Individual samples **can** be removed from the pool — otherwise the
pool would be a one-way trap. Remove-from-pool is the pool's own "drop this pool would be a one-way trap. Remove-from-pool is the pool's own "drop this
sample" verb and is allowed. sample" verb and is allowed.
- **Remove scope (fork R-A, OPEN — needs Daniel).** Whether "remove" drops the - **Remove scope (fork R-A, SETTLED 2026-07-24 — this-bank).** Remove drops the
entry from *this* bank only (leaving copies elsewhere), purges the sample from entry from *this* bank only, leaving copies in other banks untouched — the core
*every* bank, or offers both. Spec-of-record until settled: **remove-from-this- and only shipped verb. The action carries a `scope: this-bank | all-banks` seam,
bank** is the core verb (it is the model primitive and composes into but **this-bank is the settled default and the only surfaced affordance**;
from-everywhere); design the action to carry a `scope: this-bank | all-banks` all-banks stays a latent parameter (promotable later behind the seam without a
parameter so from-everywhere is a later flag, not a rewrite. See product notes rewrite), never a surfaced verb now. See product notes §Fork R-A.
§Fork R-A.
## Precision / invariant implications ## Precision / invariant implications
@@ -796,12 +795,18 @@ model capability.
remove of a sample still referenced by another bank is cheap and re-derivable remove of a sample still referenced by another bank is cheap and re-derivable
(re-copy it back) and needs no confirmation. The confirmation is *earned by (re-copy it back) and needs no confirmation. The confirmation is *earned by
actual orphan risk*, not fired on every remove. actual orphan risk*, not fired on every remove.
- **Undo (fork R-B, OPEN — shared across all of Phase B).** REAPER's undo stack - **Undo (fork R-B, SETTLED 2026-07-24 — batched REAPER undo points, Phase-B-wide).**
does not cover `"reasampler"` ext-state mutations, so remove — like Bank/index mutations integrate into REAPER's undo system as **batched undo points**
move/copy/evacuate/delete-bank — is not on REAPER's Ctrl-Z as specced. The undo (`Undo_BeginBlock` / `Undo_EndBlock`): the related index mutations of one bank
model is a Phase-B-wide decision, not a remove-local one; see product notes operation are batched into a single undo point, so one bank operation is one
§Fork R-B. Surfaced here because remove is the first verb whose *only* effect is Ctrl-Z. This is a **Phase-B-wide** decision — it applies to
index-entry destruction with no relocation, so it is where the gap first bites. create/rename/reorder/delete-bank, move, copy, evacuate, *and* remove, retro-
touching B1B4, not just B5. **Must-verify before build:** confirm against
`vendor/reaper-sdk` that `"reasampler"` ext-state mutations participate correctly
in `Undo_BeginBlock`/`Undo_EndBlock` undo blocks — the whole approach depends on
it. Surfaced with remove because remove is the first verb whose *only* effect is
index-entry destruction with no relocation, so it is where the gap first bit; the
fix is shared. See product notes §Fork R-B.
## Module architecture (preserve the pure/shell split) ## Module architecture (preserve the pure/shell split)
@@ -821,13 +826,14 @@ No new REAPER API. Pure model + a new action command-id string under the sampler
family prefix + a panel affordance on the existing M5 LICE surface. Verify the family prefix + a panel affordance on the existing M5 LICE surface. Verify the
command-id/gaccel/hookcommand usage against `main.cpp` (unchanged contract). command-id/gaccel/hookcommand usage against `main.cpp` (unchanged contract).
## Open questions ## Settled forks (Daniel, 2026-07-24)
- **Fork R-A — remove scope (this-bank | all-banks | both).** Open; needs Daniel. - **Fork R-A — remove scope.** Settled: **this-bank** (this-bank-primary, all-banks
Spec-of-record: this-bank primary with a scope-parameter seam. Product notes a latent seam-only parameter). Folded into Settled decisions above.
§Fork R-A. - **Fork R-B — undo model for index mutations.** Settled: **batched REAPER undo
- **Fork R-B — undo model for index mutations.** Open; a Phase-B-wide decision points** (`Undo_BeginBlock`/`Undo_EndBlock`), Phase-B-wide (retro-touches B1B4),
surfaced by remove. Product notes §Fork R-B. with the ext-state-participation SDK check as a must-verify-before-build. Folded
into Guardrails above and the Phase B / B1 plan points.
--- ---
@@ -902,9 +908,19 @@ pure/shell split as `bank_model` / `view_mode_model` / `bank_book`.
irreversible file-delete. irreversible file-delete.
- **Scope is the bank system's own leavings, not the folder at large.** Prune - **Scope is the bank system's own leavings, not the folder at large.** Prune
reclaims files that *were* bank files and are now unreferenced — never a file a reclaims files that *were* bank files and are now unreferenced — never a file a
user hand-dropped into the folder. How prune attributes ownership is fork R-D user hand-dropped into the folder. Prune is a reclaimer of ReaSampler's own
(below). Prune is a reclaimer of ReaSampler's own orphans, not a general folder orphans, not a general folder cleaner.
cleaner. - **Orphan attribution is an owned-file manifest (fork R-D, SETTLED 2026-07-24).**
The book tracks the set of files it has created (an **owned-file manifest**);
prune reclaims `(owned ∩ on-disk) referenced`. This is the honest encoding of
"reclaim only our own leavings" and rejects folder-sweep (which would delete
hand-dropped files). **The seam lands early:** because the manifest is cheap to
maintain from capture onward but a backfill cliff to reconstruct later, **capture
writes each file it creates into the owned-file manifest starting in Phase B**,
even though prune consumes it only in Phase R. R1/R2 consume the manifest; they do
not build it. The manifest is persisted in the `"reasampler"` ext-state; the exact
persistence shape (a sibling key vs. folded into the `banks` blob) is a small
build-time residual, but the manifest-now decision is firm.
## Precision / invariant implications ## Precision / invariant implications
@@ -928,35 +944,41 @@ pure/shell split as `bank_model` / `view_mode_model` / `bank_book`.
- **Never a referenced file; never a non-bank file.** The union-across-all-banks - **Never a referenced file; never a non-bank file.** The union-across-all-banks
rule protects referenced files; the ownership-attribution rule (fork R-D) rule protects referenced files; the ownership-attribution rule (fork R-D)
protects hand-dropped files. protects hand-dropped files.
- **Safest platform deletion available (fork R-C, OPEN — needs Daniel + verify).** - **Safest platform deletion available (fork R-C, SETTLED 2026-07-24 — trash-
Route deletions to the platform recycle bin / trash if a portable move-to-trash preferred, unlink fallback).** Route deletions to the platform recycle bin / trash
is available (recoverable outside the app); fall back to unlink with the dry-run + wherever a portable move-to-trash is available (recoverable outside the app); fall
confirm guardrail if not. Default to the safest deletion the platform affords. back to unlink — behind the dry-run + confirm guardrail — only where the platform
- **Manual, explicit trigger (fork R-E, OPEN).** Prune is a deliberate action the affords no portable trash. "Delete where possible" means recoverable-trash-
user invokes, dry-run-first — never a silent background sweep. An optional "…and preferred, never plain unlink-by-default. The move-to-trash surface is an explicit
prune now" convenience may be offered at the delete-bank confirmation; a periodic per-platform **to-verify** (see REAPER/platform API surface).
background sweep is rejected (silent irreversible file-deletion violates the - **Manual, explicit trigger (fork R-E, SETTLED 2026-07-24 — manual action + panel
button).** Prune runs via a bindable manual action (dry-run-first, confirm-to-
delete) **and** a `bank_panel` button that fires that same action — never a silent
background sweep. The earlier optional "…and prune now at the delete-bank
confirmation" convenience was **not** selected and is out of scope; a periodic
background sweep remains rejected (silent irreversible file-deletion violates the
guardrails). guardrails).
## Module architecture (preserve the pure/shell split) ## Module architecture (preserve the pure/shell split)
Pure (no REAPER types, unit-tested — the mirror of `reconcile`): Pure (no REAPER types, unit-tested — the mirror of `reconcile`):
- **Prune-reconcile core** — given `{ files present in the bank folder }` and - **Prune-reconcile core** — given `{ files present in the bank folder }`,
`{ files referenced by the book }` (and, per fork R-D, `{ files the book owns }`), `{ files referenced by the book }`, and `{ files the book owns }` (the owned-file
compute the orphan set. REAPER-free, filesystem-free, unit-tested hard (the prune manifest, R-D), compute the orphan set `(owned ∩ present) referenced`.
null test lives here). The referenced-set is unioned across all banks by asking REAPER-free, filesystem-free, unit-tested hard (the prune null test lives here).
the `bank_book`. The referenced-set is unioned across all banks by asking the `bank_book`.
REAPER-facing / filesystem-facing (thin): REAPER-facing / filesystem-facing (thin):
- `persist` / session — supplies the referenced-set (union across the book) and, - `persist` / session — supplies the referenced-set (union across the book) and the
per fork R-D, the owned-file manifest; resolves the current project bank folder owned-file manifest (R-D, written from capture onward in Phase B); resolves the
via the M4 project-relative machinery. current project bank folder via the M4 project-relative machinery.
- A **prune shell** — enumerates the bank folder (filesystem I/O), feeds the - A **prune shell** — enumerates the bank folder (filesystem I/O), feeds the
pure core, presents the dry-run manifest, and on confirmation deletes the orphan pure core, presents the dry-run manifest, and on confirmation deletes the orphan
set (via trash if available — fork R-C — else unlink). Filesystem I/O only; the set (via OS trash where portably available — fork R-C — else unlink). Filesystem
decision stays in the pure core. I/O only; the decision stays in the pure core.
- `actions` (entry) — "Prune bank folder" (dry-run-first, confirm-to-delete); - `actions` (entry) — "Prune bank folder" (dry-run-first, confirm-to-delete),
registered with the `command_id`/`gaccel`/`hookcommand` contract. registered with the `command_id`/`gaccel`/`hookcommand` contract; **plus a
`bank_panel` button** (R-E) that fires the same action.
## REAPER / platform API surface (verify all signatures) ## REAPER / platform API surface (verify all signatures)
@@ -965,13 +987,15 @@ No new REAPER *audio* API. New surfaces to verify before use:
project bank folder. **Verify** the portable approach against SWELL / the existing project bank folder. **Verify** the portable approach against SWELL / the existing
file-handling in `persist` / `capture` (which already resolve and write files); file-handling in `persist` / `capture` (which already resolve and write files);
prefer reusing whatever path/file machinery M4 established. prefer reusing whatever path/file machinery M4 established.
- **Move-to-trash (fork R-C)** — if adopted, verify a portable move-to-trash - **Move-to-trash (fork R-C, settled trash-preferred)** — verify a portable
exists (SWELL, or per-platform: Win `IFileOperation`/`SHFileOperation`, macOS move-to-trash exists (SWELL, or per-platform: Win `IFileOperation`/
`NSFileManager trashItemAtURL:`, Linux XDG trash spec). This is a **to-verify**, `SHFileOperation`, macOS `NSFileManager trashItemAtURL:`, Linux XDG trash spec).
not an assumed capability. This is a **must-verify per platform** before use, not an assumed capability;
- **Persistence (fork R-D, if owned-file manifest adopted)** — a new tracked set in where it is unavailable, fall back to unlink behind the dry-run/confirm guardrail.
the `"reasampler"` ext-state (or folded into the `banks` blob); shared M4 blob - **Owned-file manifest persistence (fork R-D, settled)** — a new tracked set in
machinery, new data only. the `"reasampler"` ext-state (a sibling key or folded into the `banks` blob —
build-time residual); shared M4 blob machinery, new data only. **Written from
capture onward in Phase B** (the seam lands early), consumed by prune in Phase R.
- **Actions** — the `command_id`/`gaccel`/`hookcommand` contract from `main.cpp` - **Actions** — the `command_id`/`gaccel`/`hookcommand` contract from `main.cpp`
(unchanged), a new command-id string under the sampler family prefix. (unchanged), a new command-id string under the sampler family prefix.
@@ -990,18 +1014,24 @@ No new REAPER *audio* API. New surfaces to verify before use:
- **Additive only.** Prune reads the book and the folder; it does not modify - **Additive only.** Prune reads the book and the folder; it does not modify
`BankIndex`, `bank_book`, the capture roadmap, or Design View semantics. `BankIndex`, `bank_book`, the capture roadmap, or Design View semantics.
## Open questions to resolve during build ## Settled forks (Daniel, 2026-07-24)
- **Fork R-C — deletion mechanism: OS trash vs. unlink.** Open; needs Daniel + a - **Fork R-C — deletion mechanism.** Settled: **trash-preferred, unlink fallback.**
platform to-verify. Lean: trash if portably available, else unlink with strong Route to the OS trash where a portable move-to-trash is available (recoverable),
dry-run/confirm. Product notes §Fork R-C. else unlink behind the dry-run/confirm guardrail. Per-platform trash surface is a
- **Fork R-D — orphan attribution: owned-file manifest vs. index-diff vs. must-verify. Folded into Settled decisions + Guardrails + API surface above.
folder-sweep.** Open; needs Daniel. Lean: **owned-file manifest**, and — because Product notes §Fork R-C.
the manifest is cheap to maintain from capture onward but a backfill cliff to - **Fork R-D — orphan attribution.** Settled: **owned-file manifest**, `(owned ∩
reconstruct later — **start tracking owned files as part of capture / Phase B present) referenced`; folder-sweep rejected as unsafe. The **seam lands early** —
even though prune ships in Phase R** (defer the feature, design the seam). capture writes each created file to the manifest starting in Phase B, prune
Folder-sweep is rejected as unsafe (deletes hand-dropped files). Product notes consumes it in Phase R. Persistence shape (sibling key vs. `banks` blob) is a
§Fork R-D. build-time residual. Folded into Settled decisions + Module architecture + API
- **Fork R-E — trigger: manual-only vs. offer-on-orphaning vs. periodic.** Open. surface above, and added as an up-front Phase B / capture plan point. Product
Lean: manual-primary (dry-run-first), an optional "…and prune now" at the notes §Fork R-D.
delete-bank confirmation, no background sweep. Product notes §Fork R-E. - **Fork R-E — trigger.** Settled: **manual action + `bank_panel` button**, dry-run-
first, confirm-to-delete; no background sweep. The delete-time "…and prune now"
convenience was not selected (out of scope). Folded into Guardrails + Module
architecture above and the R3 plan points. Product notes §Fork R-E.
**Build-time residual (not a fork):** the owned-file manifest's exact persistence
shape (sibling `"reasampler"` ext-state key vs. folded into the `banks` blob).
+102 -52
View File
@@ -194,6 +194,35 @@ active id. Legacy `bank_index` JSON parses into `{ pool }` with zero named banks
with dest collapse; cross-bank same-hash coexistence; dest collapse on move into a with dest collapse; cross-bank same-hash coexistence; dest collapse on move into a
bank already holding the hash; JSON lossless; legacy migration. bank already holding the hash; JSON lossless; legacy migration.
> **Phase-B-wide undo (fork R-B, settled 2026-07-24 — batched REAPER undo points).**
> Every index verb across B1B5 (create/rename/reorder/delete-bank, move, copy,
> evacuate, remove) wraps its bank/index mutation in a **batched REAPER undo point**
> (`Undo_BeginBlock` / `Undo_EndBlock`), so one bank operation is one Ctrl-Z. This is
> a cross-cutting decision that retro-touches B1B4, not a B5-local one; the
> per-verb points above inherit it. **Must-verify before build:** confirm against
> `vendor/reaper-sdk` that `"reasampler"` ext-state mutations participate correctly
> in `Undo_BeginBlock`/`Undo_EndBlock` undo blocks — the whole approach depends on
> it. See CONTEXT.md §Sample removal (Guardrails) + product notes §Fork R-B.
## B-cap — owned-file manifest seam (capture writes; prune consumes in Phase R)
**Goal:** Capture writes each file it creates into an **owned-file manifest**
persisted in the `"reasampler"` ext-state, so Phase R prune can later distinguish
the bank system's own orphans from hand-dropped files. Consumed only in Phase R
(R1/R2) — landed early here because reconstructing the manifest retroactively is a
backfill cliff (fork R-D, settled 2026-07-24: *defer the feature, design the seam*).
CONTEXT.md §Prune (Settled decisions — orphan attribution) + product notes §Fork R-D.
**Verify:** every file the capture path creates is recorded in the owned-file
manifest; the manifest round-trips through the `"reasampler"` ext-state (Save / Save
As / reopen); relative-paths-only preserved. Prune's consumption of it is Phase R.
**Depends on:** the capture add-path (M7) + persist blob machinery (M4 / B2).
- [ ] Capture records each created file into an owned-file manifest (the set of
files the book has created), persisted in the `"reasampler"` ext-state (sibling
key or folded into the `banks` blob — persistence shape is a small build-time
residual, not a fork).
- [ ] Manifest round-trips: survives Save / Save As / reopen via the M4 blob
machinery; relative-paths-only. (Consumed by Phase R R1/R2 — not consumed here.)
## B2 — persist slice (banks ↔ project ext state) ## B2 — persist slice (banks ↔ project ext state)
**Goal:** Serialize the book under the `banks` key in `"reasampler"` alongside the **Goal:** Serialize the book under the `banks` key in `"reasampler"` alongside the
existing sections, with the pool folded in as bank-zero; migrate a legacy existing sections, with the pool folded in as bank-zero; migrate a legacy
@@ -282,8 +311,9 @@ timeline item).
- [ ] Surface `BankIndex::remove` through `bank_book`: remove a `Sample` from a - [ ] Surface `BankIndex::remove` through `bank_book`: remove a `Sample` from a
bank's index; pool contents removable, pool-container privileges unchanged. bank's index; pool contents removable, pool-container privileges unchanged.
- [ ] "Remove selected sample(s)" action (`command_id`/`gaccel`/`hookcommand`), - [ ] "Remove selected sample(s)" action (`command_id`/`gaccel`/`hookcommand`),
MIDI-bindable; carries a `scope: this-bank | all-banks` seam (fork R-A — default MIDI-bindable; carries a `scope: this-bank | all-banks` seam (fork R-A, settled
this-bank until Daniel settles). 2026-07-24: **this-bank** is the default and only surfaced affordance; all-banks
stays a latent seam-only parameter, not shipped).
- [ ] `bank_panel` remove affordance on the current selection (reuse M5 selection - [ ] `bank_panel` remove affordance on the current selection (reuse M5 selection
model, as move/copy do). model, as move/copy do).
- [ ] Confirm-on-last-reference guardrail: remove that orphans a file (no other - [ ] Confirm-on-last-reference guardrail: remove that orphans a file (no other
@@ -305,12 +335,16 @@ ready to scope into implementation waves. One polish detail remains:
vs. lit-tab. "Unmistakable" is settled; only placement is open. Polish detail. vs. lit-tab. "Unmistakable" is settled; only placement is open. Polish detail.
(touches B4) (touches B4)
- **B5 sample-remove forks (NEW, unsettled — need Daniel):** **R-A** — remove scope **B5 sample-remove forks — settled 2026-07-24:**
(this-bank | all-banks | both); spec-of-record is this-bank-primary with a - **R-A — remove scope.** Settled: **this-bank**. Removes the entry from the bank in
scope-parameter seam. **R-B** — undo model for `"reasampler"` ext-state index view only; the `scope: this-bank | all-banks` seam stays in the action signature
mutations (affects *all* of Phase B, surfaced by remove; move/copy/evacuate/ but all-banks is a latent parameter, not a surfaced verb. Folded into the B5 action
delete-bank/remove are not on REAPER Ctrl-Z as specced). Lean: a single-snapshot point above.
ReaSampler-internal "undo last bank change." Both in `docs/product/removal-and-prune.md`. - **R-B — undo model (Phase-B-wide).** Settled: **batched REAPER undo points**
(`Undo_BeginBlock`/`Undo_EndBlock`), one bank op = one Ctrl-Z. Applies across
B1B5 (retro-touches B1B4) — captured as the cross-cutting note under B1 above,
with the ext-state-participation SDK check as a must-verify-before-build.
Both in `docs/product/removal-and-prune.md` §Fork R-A / §Fork R-B.
--- ---
@@ -338,68 +372,84 @@ ready to scope into implementation waves. One polish detail remains:
> **not** depend on the B3/B4 UI. > **not** depend on the B3/B4 UI.
## R1 — prune-reconcile core (pure) ## R1 — prune-reconcile core (pure)
**Goal:** REAPER-free, filesystem-free reconciler — given the set of files present **Goal:** REAPER-free, filesystem-free reconciler — given the files present in the
in the bank folder and the set of files referenced by the book (unioned across all bank folder, the files referenced by the book (unioned across all banks, pool
banks, pool included), compute the orphan set. The mirror of included), and the **owned-file manifest** (fork R-D, written from capture onward by
B-cap), compute the orphan set `(owned ∩ present) referenced`. The mirror of
`ViewModeModel::reconcile(liveGuids)`, one level down (files instead of GUIDs). `ViewModeModel::reconcile(liveGuids)`, one level down (files instead of GUIDs).
CONTEXT.md §Prune (Module architecture — pure). CONTEXT.md §Prune (Module architecture — pure).
**Verify:** CTest green. **Prune null test:** a folder whose every file is **Verify:** CTest green. **Prune null test:** a folder whose every file is
referenced deletes nothing; prune returns exactly `present referenced` and referenced deletes nothing; prune returns exactly `(owned ∩ present) referenced`
nothing else. Referenced-set unioned across every bank (a file referenced by any and nothing else. Referenced-set unioned across every bank (a file referenced by any
bank — including via a copy — is never an orphan). bank — including via a copy — is never an orphan); a present-but-not-owned file (a
hand-dropped file) is never an orphan.
- [ ] Prune-reconcile pure function: `(present, referenced) → orphans`, referenced - [ ] Prune-reconcile pure function: `(present, referenced, owned) → orphans`,
unioned across the whole book (copies keep a file alive). computing `(owned ∩ present) referenced`; referenced unioned across the whole
- [ ] Tests: prune null test (all-referenced → empty); orphan = presentreferenced; book (copies keep a file alive).
a copied file referenced by a second bank survives; empty folder / empty book edge - [ ] Tests: prune null test (all-referenced → empty); orphan = (owned∩present)
cases. referenced; a copied file referenced by a second bank survives; a present-but-
unowned (hand-dropped) file is never reclaimed; empty folder / empty book / empty
manifest edge cases.
## R2 — prune shell + persist wiring (filesystem I/O, thin) ## R2 — prune shell + persist wiring (filesystem I/O, thin)
**Goal:** Enumerate the current project bank folder (M4 project-relative resolution), **Goal:** Enumerate the current project bank folder (M4 project-relative resolution),
supply the referenced-set (and, per fork R-D, the owned-file set) from the session, supply the referenced-set and the **owned-file manifest** (from B-cap) from the
feed the pure core, and produce a dry-run manifest. No deletion in this wave — the session, feed the pure core, and produce a dry-run manifest. No deletion in this
report path only. CONTEXT.md §Prune (persist / prune shell). wave — the report path only. CONTEXT.md §Prune (persist / prune shell).
**Verify (in DAW):** Dry-run reports the orphan count + reclaimed size (+ file list **Verify (in DAW):** Dry-run reports the orphan count + reclaimed size (+ file list
for a small set) against the resolved current bank folder; resolves paths the same for a small set) against the resolved current bank folder; resolves paths the same
way the index does (survives a Save-As relocation); deletes nothing. way the index does (survives a Save-As relocation); deletes nothing.
**Depends on:** R1, B1, B2. **Depends on:** R1, B1, B2.
- [ ] Prune shell: enumerate the resolved current bank folder; feed the pure core. - [ ] Prune shell: enumerate the resolved current bank folder; feed the pure core.
- [ ] Session supplies the referenced-set (union across the book); resolve the bank - [ ] Session supplies the referenced-set (union across the book) **and the
folder via the M4 project-relative machinery. owned-file manifest** (written by B-cap); resolve the bank folder via the M4
project-relative machinery.
- [ ] Dry-run manifest: orphan count + reclaimed size (+ files for a small set); - [ ] Dry-run manifest: orphan count + reclaimed size (+ files for a small set);
**no deletion in this wave.** **no deletion in this wave.**
## R3 — deletion + action (the destructive step, guarded) ## R3 — deletion + action (the destructive step, guarded)
**Goal:** The confirmed deletion step and the bindable "Prune bank folder" action: **Goal:** The confirmed deletion step, the bindable "Prune bank folder" action, and
dry-run-first, confirm-with-manifest, then reclaim the orphan set — via OS trash if a `bank_panel` prune button: dry-run-first, confirm-with-manifest, then reclaim the
portably available (fork R-C), else unlink. CONTEXT.md §Prune (guardrails, API). orphan set — via OS trash where portably available (fork R-C), else unlink.
**Verify (in DAW):** "Prune bank folder" reports first, deletes only on explicit CONTEXT.md §Prune (guardrails, API).
confirm, and reclaims exactly the orphan set — never a referenced file, never a **Verify (in DAW):** "Prune bank folder" (action or panel button) reports first,
hand-dropped non-bank file; the referenced/owned-set safety holds; non-bank and deletes only on explicit confirm, and reclaims exactly the orphan set — never a
capture invariants untouched. referenced file, never a hand-dropped non-bank file; the referenced/owned-set safety
**Depends on:** R2. **Forks R-C / R-D / R-E must be settled before this wave.** holds; deletions route to OS trash where available; non-bank and capture invariants
untouched.
**Depends on:** R2 (and B-cap's owned-file manifest). All forks settled 2026-07-24.
- [ ] "Prune bank folder" action (`command_id`/`gaccel`/`hookcommand`), - [ ] "Prune bank folder" action (`command_id`/`gaccel`/`hookcommand`),
dry-run-first, confirm-to-delete. dry-run-first, confirm-to-delete.
- [ ] Deletion mechanism (fork R-C): OS trash if a portable move-to-trash is - [ ] `bank_panel` prune button (fork R-E) that fires the "Prune bank folder"
verified available, else unlink with the dry-run/confirm guardrail. **Verify the action through the existing command-id contract — the panel affordance alongside
platform move-to-trash surface before use.** the bindable action; split: button hit-test/layout is pure (mirror of
- [ ] Orphan attribution (fork R-D): reclaim only the bank system's own leavings, `mode_switch`/`bank_grid`), draw + dispatch is bank_panel shell.
not hand-dropped folder files (owned-file manifest per the lean — see below). - [ ] Deletion mechanism (fork R-C, settled trash-preferred): route to OS trash
- [ ] Trigger (fork R-E): manual-primary; optional "…and prune now" offered at the where a portable move-to-trash is verified available, else unlink behind the
delete-bank confirmation; **no** background sweep. dry-run/confirm guardrail. **Verify the platform move-to-trash surface before use
(per platform).**
- [ ] Orphan attribution (fork R-D, settled owned-file manifest): reclaim only
`(owned ∩ present) referenced` — the bank system's own leavings, never a
hand-dropped folder file. (Manifest written by B-cap; consumed via R1/R2.)
## Phase R open questions (unsettled — need Daniel) ## Phase R forks — settled 2026-07-24
- **Fork R-C — deletion mechanism (OS trash vs. unlink).** Lean: trash if portably - **Fork R-C — deletion mechanism.** Settled: **trash-preferred, unlink fallback.**
available (recoverable), else unlink with strong dry-run/confirm. Needs Daniel + Route to OS trash where a portable move-to-trash is available (recoverable), else
a platform to-verify (SWELL / per-platform trash APIs). unlink behind strong dry-run/confirm. Per-platform trash surface (SWELL / Win
- **Fork R-D — orphan attribution (owned-file manifest vs. index-diff vs. `SHFileOperation`·`IFileOperation` / macOS `trashItemAtURL:` / Linux XDG) is a
folder-sweep).** Lean: **owned-file manifest**. **Design-the-seam call:** the **must-verify before use**. Folded into R3.
manifest is cheap to maintain from capture onward but a backfill cliff to - **Fork R-D — orphan attribution.** Settled: **owned-file manifest**,
reconstruct later — so **start tracking owned files as part of capture / Phase B `(owned ∩ present) referenced`; folder-sweep rejected as unsafe. **Seam lands
now, even though prune ships in Phase R.** Folder-sweep rejected as unsafe (deletes early** — the manifest is written from capture onward (new **B-cap** point in
hand-dropped files). Needs Daniel (touches the persisted shape + ideally lands Phase B), not reconstructed at prune time; R1/R2 consume it. Persistence shape
earlier than R3). (sibling `"reasampler"` key vs. `banks` blob) is a small build-time residual.
- **Fork R-E — trigger (manual-only vs. offer-on-orphaning vs. periodic).** Lean: - **Fork R-E — trigger.** Settled: **manual action + `bank_panel` button**,
manual-primary + optional delete-time "prune now"; no background sweep. Needs Daniel. dry-run-first, confirm-to-delete. No background sweep. The earlier optional
delete-time "…and prune now" convenience was **not** selected — out of scope.
Folded into R3.
Both docs of record: `docs/product/removal-and-prune.md` §Fork R-C/R-D/R-E and
CONTEXT.md §Prune (Settled forks).
+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 lifecycle). This doc holds the *why* — the workflow, the guardrails, the
index-vs-file boundary, and the forks that need a Daniel decision. 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 Status: framed by product-designer (2026-07-23); **all five forks settled by Daniel
prune forks R-C/R-D/R-E are surfaced below; **none are settled** — they need a (2026-07-24)** — R-A this-bank-primary, R-B batched REAPER undo points
Daniel call before B5 / Phase R implementation. (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) 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 and need not confirm. This makes the confirmation *earned* by actual risk rather
than fired on every remove. than fired on every remove.
- **Undo.** REAPER's own undo stack does not know about ext-state index mutations. - **Undo.** REAPER's own undo stack does not natively cover ext-state index
Whether remove is undoable — and how — is a genuine question shared with the mutations, so this is a Phase-B-wide decision (fork R-B, **settled**): bank/index
rest of Phase B (see fork R-B); it is not remove-specific, but remove is where mutations integrate into REAPER's undo system as **batched undo points**
it bites first because remove is the first verb whose *only* effect is (`Undo_BeginBlock` / `Undo_EndBlock`), so one bank operation is one Ctrl-Z. Remove
destruction of an index entry with no relocation. 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 ### 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. 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" (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.) 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 - **Recoverability via the OS trash (fork R-C, settled: trash-preferred).** Prune
route deletions to the platform recycle bin / trash instead of unlinking, an routes deletions to the platform recycle bin / trash where a portable move-to-trash
accidental prune is recoverable outside the app. Whether SWELL/the platform is available, so an accidental prune is recoverable outside the app; it falls back
layer gives us a portable "move to trash" is a to-verify — but the *default* to unlink (behind the dry-run + confirm guardrail) only where the platform affords
should be the safest deletion 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 ### 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 ### Sample-remove
**Fork R-A — remove scope: from-this-bank vs. from-everywhere vs. both.** **Fork R-A — remove scope. SETTLED: THIS BANK (this-bank-primary).**
Does "remove sample" drop the entry from the bank in view (leaving copies in other Remove drops the entry from the bank in view only, leaving copies in other banks
banks), purge every index entry across the whole book, or offer both as distinct untouched. This is the core (and shipped) verb.
verbs? - *from-this-bank* is the composable primitive (it is literally the
- *from-this-bank* is the conservative, composable primitive (it is literally the
`BankIndex::remove` the model already has); "from everywhere" is then "remove `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 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 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. remove-from-this-bank usually *is* remove-from-everywhere.
- *from-everywhere* as a distinct verb only earns its keep once copy has scattered - **Decision:** ship **from-this-bank** as B5's core verb and the only surfaced
a sample across several banks; then "purge this everywhere" is a real affordance. Keep the `scope: this-bank | all-banks` seam in the action signature
convenience. as designed, but **this-bank is the settled default and the only shipped verb**;
- **Product-designer lean:** ship **from-this-bank** as B5's core verb (it is the all-banks stays a *latent parameter*, not a surfaced convenience — it can be
model primitive, it composes, it matches the partition model), and treat promoted later behind that seam without a rewrite if the copy workflow proves to
*from-everywhere* as an optional convenience layered on top *only if* the copy scatter samples in practice. (Settled 2026-07-24, confirming the product-designer
workflow proves to scatter samples in practice. Design the seam (the action can lean; from-everywhere is explicitly *not* elevated to a co-equal verb now.)
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.
**Fork R-B — undo model for index mutations (shared across Phase B, surfaced by **Fork R-B — undo model for index mutations (Phase-B-wide, surfaced by remove).
remove).** REAPER's undo stack does not cover `"reasampler"` ext-state index SETTLED: BATCH UNDO POINTS (option (iii) — REAPER-integrated, batched).**
mutations. So move/copy/evacuate/delete-bank/**remove** are, as specced, not on REAPER's undo stack does not natively cover `"reasampler"` ext-state index
REAPER's Ctrl-Z. Options: mutations, so move/copy/evacuate/delete-bank/remove needed an undo story. Daniel
- *(i)* Accept it: index mutations are not undoable via REAPER undo; the safety net chose to integrate bank/index mutations into **REAPER's own undo system as batched
is the confirmations + the fact that files survive (nothing is lost but the undo points** — the `Undo_BeginBlock` / `Undo_EndBlock` direction — batching the
entry, which the user can re-create by re-capture/re-move). Cheapest. related index mutations of one bank operation into a single undo point, so a bank
- *(ii)* A ReaSampler-internal one-step undo for the last bank/index mutation (a operation is one Ctrl-Z. The considered alternatives:
single snapshot of the book restored on a "ReaSampler: undo last bank change" - *(i)* Accept no undo (rely on confirmations + files surviving) — **rejected**, too
action). Bounded, cheap-ish, covers the "oops" case for all Phase B verbs at weak once remove destroys an index entry with no relocation.
once. - *(ii)* A ReaSampler-internal single-snapshot "undo last bank change" — **rejected**
- *(iii)* Hook into REAPER's undo system properly (`Undo_BeginBlock` / in favour of the more integrated (iii); the earlier product-designer lean toward
`Undo_EndBlock` with ext-state as undo state) — most integrated, most work, needs (ii) was overridden.
SDK verification that ext-state participates in undo blocks. - *(iii)* **CHOSEN** — hook REAPER's undo system properly, batching related index
- **Product-designer lean:** this is bigger than remove and should be decided for mutations into single undo points.
**all of Phase B at once**, not per-verb. Lean *(ii)* — a single-snapshot - **Scope — Phase-B-wide.** This is decided for **all of Phase B at once**, and it
internal undo of the last bank/index change — as the pragmatic middle: it turns **retro-touches B1B4**, not just B5: every index verb (create/rename/reorder/
every index verb's "oops" into a one-key recovery without wading into REAPER's delete-bank, move, copy, evacuate, remove) wraps its mutation in an undo block.
undo internals. **Needs Daniel** (and it may retro-touch B1B4, so surfacing it Surfaced with B1's open questions, not only at B5.
now rather than at B5 is deliberate — the seam is cheapest before B1 lands). - **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 ### Prune
**Fork R-C — deletion mechanism: unlink vs. OS trash.** Does prune `remove()` the **Fork R-C — deletion mechanism: unlink vs. OS trash. SETTLED: TRASH-PREFERRED,
file (irreversible) or route to the platform recycle bin/trash (recoverable UNLINK FALLBACK.** Prune routes deletions to the platform recycle bin / trash
outside the app)? Trash is the safer default; the question is whether SWELL / the (recoverable outside the app) **wherever the platform affords a portable
platform layer offers a portable move-to-trash, or whether we'd hand-roll it per move-to-trash**, and falls back to unlink — behind the dry-run + confirm guardrail —
platform (Win `SHFileOperation`/`IFileOperation`, macOS `NSFileManager only where it does not. Trash is the settled default; "delete where possible" reads
trashItemAtURL`, Linux XDG trash spec). **Needs Daniel + a to-verify** on platform as *recoverable-trash-preferred*, never plain unlink-by-default.
support. Lean: **trash if portably available, unlink with a strong dry-run+confirm - **To-verify (carried, per platform):** whether a portable move-to-trash exists via
guardrail if not.** 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. **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 folder-sweep. SETTLED: OWNED-FILE MANIFEST — and the seam lands EARLY (Phase B /
orphan* (a bank file no index references) rather than *a file that was never capture).** The book tracks the set of files it has created; prune reclaims
ours* (a user dropped it in by hand)? `(owned ∩ on-disk) referenced`. The considered alternatives:
- *(i) folder-sweep*treat every file in the bank folder not referenced by the - *(i) folder-sweep* — reclaim every unreferenced file in the folder. **Rejected**
book as an orphan and reclaim it. Simplest; **dangerous** — it will delete a it would delete a user's hand-placed file, violating "only reclaim our own
user's hand-placed file, violating "only reclaim our own leavings." leavings."
- *(ii) index-diff only*never sweep the folder; instead, capture the file's - *(ii) index-diff only*record a file's identity when its *last* index reference
identity at the moment its *last* index reference is dropped (remove/delete-bank) drops and prune only that set. Safe but partial (misses files orphaned outside a
and prune only that recorded set. Safe (only ever touches files we know we tracked drop path). Not chosen.
orphaned) but requires recording orphans-at-drop-time. - *(iii) owned-file manifest***CHOSEN.** Safest and most general: distinguishes
- *(iii) owned-file manifest* — the book tracks the set of files it has ever "our orphan" from "user's file" and from "already-gone."
created (capture writes to it); prune reclaims `(owned ∩ on-disk) referenced`. - **Seam lands early (accepted design-the-seam-now call).** The manifest is cheap to
Safest and most general (distinguishes "our orphan" from "user's file" and from maintain from capture onward but a **backfill cliff** to reconstruct later — you
"already-gone"); costs a manifest that rides the ext-state. cannot tell, after the fact, which folder files were ever ours. Daniel accepted the
- **Product-designer lean:** *(iii) owned-file manifest* — it is the honest recommendation to **start the owned-file manifest at capture time NOW, in Phase B,
encoding of "prune reclaims only the bank system's own leavings," and it is a even though prune (which consumes it) ships in Phase R.** So: **capture writes each
**design-the-seam-now** call: the manifest is cheap to maintain from capture file it creates into an owned-file manifest persisted in the `"reasampler"`
onward but expensive to reconstruct retroactively (you cannot tell, later, which ext-state**, and Phase R's R1/R2 *consume* that manifest. The exact persistence
folder files were ever ours). If we defer prune but *don't* start tracking owned shape — a sibling ext-state key vs. folded into the `banks` blob — is a small
files now, we build a backfill cliff. **Strong recommendation: start the owned- residual to settle at build; the **manifest-now decision is firm**. (Settled
file manifest as part of Phase B / capture even though prune ships in Phase R** — 2026-07-24; the up-front point is added to Phase B / the capture path in PLAN.md.)
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.
**Fork R-E — prune trigger: manual-only vs. offer-on-orphaning vs. periodic.** **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"), SETTLED: MANUAL ACTION + PANEL BUTTON.** Prune runs via a bindable manual action
prompted opportunistically (after a non-empty delete-bank: "…evacuate, or delete (dry-run-first, confirm-to-delete) **and** a button in the `bank_panel` that fires
and prune now?"), or a background/periodic sweep? Lean: **manual-only as the core that same action. No background sweep. The earlier optional "…and prune now at the
(an explicit, dry-run-first action the user invokes deliberately)**; an *optional* delete-bank confirmation" convenience was **not** selected — it is dropped from the
"…and prune now" affordance offered at the delete-bank confirmation as a settled spec (explicitly out of scope). A periodic/background sweep remains rejected
convenience, never a background sweep (silent irreversible file-deletion is exactly (silent irreversible file-deletion violates the guardrails). So R3 gains a
what the guardrails forbid). **Needs Daniel** to confirm manual-primary and `bank_panel` button affordance alongside the action registration. (Settled
whether the delete-time "prune now" convenience is wanted. 2026-07-24.)
--- ---
@@ -350,5 +360,5 @@ whether the delete-time "prune now" convenience is wanted.
| Side of the line | index | index | **file** | | Side of the line | index | index | **file** |
| Deletes bytes? | no | no | **yes (only op that does)** | | Deletes bytes? | no | no | **yes (only op that does)** |
| Produces orphans? | yes (last-ref) | yes (non-empty) | — (it *reclaims* them) | | 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 | | Guardrail | confirm on last-ref | confirm on non-empty | dry-run + manifest confirm |