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
pool would be a one-way trap. Remove-from-pool is the pool's own "drop this
sample" verb and is allowed.
- **Remove scope (fork R-A, OPEN — needs Daniel).** Whether "remove" drops the
entry from *this* bank only (leaving copies elsewhere), purges the sample from
*every* bank, or offers both. Spec-of-record until settled: **remove-from-this-
bank** is the core verb (it is the model primitive and composes into
from-everywhere); design the action to carry a `scope: this-bank | all-banks`
parameter so from-everywhere is a later flag, not a rewrite. See product notes
§Fork R-A.
- **Remove scope (fork R-A, SETTLED 2026-07-24 — this-bank).** Remove drops the
entry from *this* bank only, leaving copies in other banks untouched — the core
and only shipped verb. The action carries a `scope: this-bank | all-banks` seam,
but **this-bank is the settled default and the only surfaced affordance**;
all-banks stays a latent parameter (promotable later behind the seam without a
rewrite), never a surfaced verb now. See product notes §Fork R-A.
## Precision / invariant implications
@@ -796,12 +795,18 @@ model capability.
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
actual orphan risk*, not fired on every remove.
- **Undo (fork R-B, OPEN — shared across all of Phase B).** REAPER's undo stack
does not cover `"reasampler"` ext-state mutations, so remove — like
move/copy/evacuate/delete-bank — is not on REAPER's Ctrl-Z as specced. The undo
model is a Phase-B-wide decision, not a remove-local one; see product notes
§Fork R-B. Surfaced here because remove is the first verb whose *only* effect is
index-entry destruction with no relocation, so it is where the gap first bites.
- **Undo (fork R-B, SETTLED 2026-07-24 — batched REAPER undo points, Phase-B-wide).**
Bank/index mutations integrate into REAPER's undo system as **batched undo points**
(`Undo_BeginBlock` / `Undo_EndBlock`): the related index mutations of one bank
operation are batched into a single undo point, so one bank operation is one
Ctrl-Z. This is a **Phase-B-wide** decision — it applies to
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)
@@ -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
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.
Spec-of-record: this-bank primary with a scope-parameter seam. Product notes
§Fork R-A.
- **Fork R-B — undo model for index mutations.** Open; a Phase-B-wide decision
surfaced by remove. Product notes §Fork R-B.
- **Fork R-A — remove scope.** Settled: **this-bank** (this-bank-primary, all-banks
a latent seam-only parameter). Folded into Settled decisions above.
- **Fork R-B — undo model for index mutations.** Settled: **batched REAPER undo
points** (`Undo_BeginBlock`/`Undo_EndBlock`), Phase-B-wide (retro-touches B1B4),
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.
- **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
user hand-dropped into the folder. How prune attributes ownership is fork R-D
(below). Prune is a reclaimer of ReaSampler's own orphans, not a general folder
cleaner.
user hand-dropped into the folder. Prune is a reclaimer of ReaSampler's own
orphans, not a general folder 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
@@ -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
rule protects referenced files; the ownership-attribution rule (fork R-D)
protects hand-dropped files.
- **Safest platform deletion available (fork R-C, OPEN — needs Daniel + verify).**
Route deletions to the platform recycle bin / trash if a portable move-to-trash
is available (recoverable outside the app); fall back to unlink with the dry-run +
confirm guardrail if not. Default to the safest deletion the platform affords.
- **Manual, explicit trigger (fork R-E, OPEN).** Prune is a deliberate action the
user invokes, dry-run-first — never a silent background sweep. An optional "…and
prune now" convenience may be offered at the delete-bank confirmation; a periodic
background sweep is rejected (silent irreversible file-deletion violates the
- **Safest platform deletion available (fork R-C, SETTLED 2026-07-24 — trash-
preferred, unlink fallback).** Route deletions to the platform recycle bin / trash
wherever a portable move-to-trash is available (recoverable outside the app); fall
back to unlink — behind the dry-run + confirm guardrail — only where the platform
affords no portable trash. "Delete where possible" means recoverable-trash-
preferred, never plain unlink-by-default. The move-to-trash surface is an explicit
per-platform **to-verify** (see REAPER/platform API surface).
- **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).
## Module architecture (preserve the pure/shell split)
Pure (no REAPER types, unit-tested — the mirror of `reconcile`):
- **Prune-reconcile core** — given `{ files present in the bank folder }` and
`{ files referenced by the book }` (and, per fork R-D, `{ files the book owns }`),
compute the orphan set. REAPER-free, filesystem-free, unit-tested hard (the prune
null test lives here). The referenced-set is unioned across all banks by asking
the `bank_book`.
- **Prune-reconcile core** — given `{ files present in the bank folder }`,
`{ files referenced by the book }`, and `{ files the book owns }` (the owned-file
manifest, R-D), compute the orphan set `(owned ∩ present) referenced`.
REAPER-free, filesystem-free, unit-tested hard (the prune null test lives here).
The referenced-set is unioned across all banks by asking the `bank_book`.
REAPER-facing / filesystem-facing (thin):
- `persist` / session — supplies the referenced-set (union across the book) and,
per fork R-D, the owned-file manifest; resolves the current project bank folder
via the M4 project-relative machinery.
- `persist` / session — supplies the referenced-set (union across the book) and the
owned-file manifest (R-D, written from capture onward in Phase B); resolves the
current project bank folder via the M4 project-relative machinery.
- 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
set (via trash if available — fork R-C — else unlink). Filesystem I/O only; the
decision stays in the pure core.
- `actions` (entry) — "Prune bank folder" (dry-run-first, confirm-to-delete);
registered with the `command_id`/`gaccel`/`hookcommand` contract.
set (via OS trash where portably available — fork R-C — else unlink). Filesystem
I/O only; the decision stays in the pure core.
- `actions` (entry) — "Prune bank folder" (dry-run-first, confirm-to-delete),
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)
@@ -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
file-handling in `persist` / `capture` (which already resolve and write files);
prefer reusing whatever path/file machinery M4 established.
- **Move-to-trash (fork R-C)** — if adopted, verify a portable move-to-trash
exists (SWELL, or per-platform: Win `IFileOperation`/`SHFileOperation`, macOS
`NSFileManager trashItemAtURL:`, Linux XDG trash spec). This is a **to-verify**,
not an assumed capability.
- **Persistence (fork R-D, if owned-file manifest adopted)** — a new tracked set in
the `"reasampler"` ext-state (or folded into the `banks` blob); shared M4 blob
machinery, new data only.
- **Move-to-trash (fork R-C, settled trash-preferred)** — verify a portable
move-to-trash exists (SWELL, or per-platform: Win `IFileOperation`/
`SHFileOperation`, macOS `NSFileManager trashItemAtURL:`, Linux XDG trash spec).
This is a **must-verify per platform** before use, not an assumed capability;
where it is unavailable, fall back to unlink behind the dry-run/confirm guardrail.
- **Owned-file manifest persistence (fork R-D, settled)** — a new tracked set in
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`
(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
`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
platform to-verify. Lean: trash if portably available, else unlink with strong
dry-run/confirm. Product notes §Fork R-C.
- **Fork R-D — orphan attribution: owned-file manifest vs. index-diff vs.
folder-sweep.** Open; needs Daniel. Lean: **owned-file manifest**, and — because
the manifest is cheap to maintain from capture onward but a backfill cliff to
reconstruct later — **start tracking owned files as part of capture / Phase B
even though prune ships in Phase R** (defer the feature, design the seam).
Folder-sweep is rejected as unsafe (deletes hand-dropped files). Product notes
§Fork R-D.
- **Fork R-E — trigger: manual-only vs. offer-on-orphaning vs. periodic.** Open.
Lean: manual-primary (dry-run-first), an optional "…and prune now" at the
delete-bank confirmation, no background sweep. Product notes §Fork R-E.
- **Fork R-C — deletion mechanism.** Settled: **trash-preferred, unlink fallback.**
Route to the OS trash where a portable move-to-trash is available (recoverable),
else unlink behind the dry-run/confirm guardrail. Per-platform trash surface is a
must-verify. Folded into Settled decisions + Guardrails + API surface above.
Product notes §Fork R-C.
- **Fork R-D — orphan attribution.** Settled: **owned-file manifest**, `(owned ∩
present) referenced`; folder-sweep rejected as unsafe. The **seam lands early** —
capture writes each created file to the manifest starting in Phase B, prune
consumes it in Phase R. Persistence shape (sibling key vs. `banks` blob) is a
build-time residual. Folded into Settled decisions + Module architecture + API
surface above, and added as an up-front Phase B / capture plan point. Product
notes §Fork R-D.
- **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
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)
**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
@@ -282,8 +311,9 @@ timeline item).
- [ ] Surface `BankIndex::remove` through `bank_book`: remove a `Sample` from a
bank's index; pool contents removable, pool-container privileges unchanged.
- [ ] "Remove selected sample(s)" action (`command_id`/`gaccel`/`hookcommand`),
MIDI-bindable; carries a `scope: this-bank | all-banks` seam (fork R-A — default
this-bank until Daniel settles).
MIDI-bindable; carries a `scope: this-bank | all-banks` seam (fork R-A, settled
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
model, as move/copy do).
- [ ] 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.
(touches B4)
- **B5 sample-remove forks (NEW, unsettled — need Daniel):** **R-A** — remove scope
(this-bank | all-banks | both); spec-of-record is this-bank-primary with a
scope-parameter seam. **R-B** — undo model for `"reasampler"` ext-state index
mutations (affects *all* of Phase B, surfaced by remove; move/copy/evacuate/
delete-bank/remove are not on REAPER Ctrl-Z as specced). Lean: a single-snapshot
ReaSampler-internal "undo last bank change." Both in `docs/product/removal-and-prune.md`.
**B5 sample-remove forks — settled 2026-07-24:**
- **R-A — remove scope.** Settled: **this-bank**. Removes the entry from the bank in
view only; the `scope: this-bank | all-banks` seam stays in the action signature
but all-banks is a latent parameter, not a surfaced verb. Folded into the B5 action
point above.
- **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.
## R1 — prune-reconcile core (pure)
**Goal:** REAPER-free, filesystem-free reconciler — given the set of files present
in the bank folder and the set of files referenced by the book (unioned across all
banks, pool included), compute the orphan set. The mirror of
**Goal:** REAPER-free, filesystem-free reconciler — given the files present in the
bank folder, the files referenced by the book (unioned across all banks, pool
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).
CONTEXT.md §Prune (Module architecture — pure).
**Verify:** CTest green. **Prune null test:** a folder whose every file is
referenced deletes nothing; prune returns exactly `present referenced` and
nothing else. Referenced-set unioned across every bank (a file referenced by any
bank — including via a copy — is never an orphan).
referenced deletes nothing; prune returns exactly `(owned ∩ present) referenced`
and nothing else. Referenced-set unioned across every bank (a file referenced by any
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
unioned across the whole book (copies keep a file alive).
- [ ] Tests: prune null test (all-referenced → empty); orphan = presentreferenced;
a copied file referenced by a second bank survives; empty folder / empty book edge
cases.
- [ ] Prune-reconcile pure function: `(present, referenced, owned) → orphans`,
computing `(owned ∩ present) referenced`; referenced unioned across the whole
book (copies keep a file alive).
- [ ] Tests: prune null test (all-referenced → empty); orphan = (owned∩present)
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)
**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,
feed the pure core, and produce a dry-run manifest. No deletion in this wave — the
report path only. CONTEXT.md §Prune (persist / prune shell).
supply the referenced-set and the **owned-file manifest** (from B-cap) from the
session, feed the pure core, and produce a dry-run manifest. No deletion in this
wave — the report path only. CONTEXT.md §Prune (persist / prune shell).
**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
way the index does (survives a Save-As relocation); deletes nothing.
**Depends on:** R1, B1, B2.
- [ ] Prune shell: enumerate the resolved current bank folder; feed the pure core.
- [ ] Session supplies the referenced-set (union across the book); resolve the bank
folder via the M4 project-relative machinery.
- [ ] Session supplies the referenced-set (union across the book) **and the
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);
**no deletion in this wave.**
## R3 — deletion + action (the destructive step, guarded)
**Goal:** The confirmed deletion step and the bindable "Prune bank folder" action:
dry-run-first, confirm-with-manifest, then reclaim the orphan set — via OS trash if
portably available (fork R-C), else unlink. CONTEXT.md §Prune (guardrails, API).
**Verify (in DAW):** "Prune bank folder" reports first, deletes only on explicit
confirm, and reclaims exactly the orphan set — never a referenced file, never a
hand-dropped non-bank file; the referenced/owned-set safety holds; non-bank and
capture invariants untouched.
**Depends on:** R2. **Forks R-C / R-D / R-E must be settled before this wave.**
**Goal:** The confirmed deletion step, the bindable "Prune bank folder" action, and
a `bank_panel` prune button: dry-run-first, confirm-with-manifest, then reclaim the
orphan set — via OS trash where portably available (fork R-C), else unlink.
CONTEXT.md §Prune (guardrails, API).
**Verify (in DAW):** "Prune bank folder" (action or panel button) reports first,
deletes only on explicit confirm, and reclaims exactly the orphan set — never a
referenced file, never a hand-dropped non-bank file; the referenced/owned-set safety
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`),
dry-run-first, confirm-to-delete.
- [ ] Deletion mechanism (fork R-C): OS trash if a portable move-to-trash is
verified available, else unlink with the dry-run/confirm guardrail. **Verify the
platform move-to-trash surface before use.**
- [ ] Orphan attribution (fork R-D): reclaim only the bank system's own leavings,
not hand-dropped folder files (owned-file manifest per the lean — see below).
- [ ] Trigger (fork R-E): manual-primary; optional "…and prune now" offered at the
delete-bank confirmation; **no** background sweep.
- [ ] `bank_panel` prune button (fork R-E) that fires the "Prune bank folder"
action through the existing command-id contract — the panel affordance alongside
the bindable action; split: button hit-test/layout is pure (mirror of
`mode_switch`/`bank_grid`), draw + dispatch is bank_panel shell.
- [ ] Deletion mechanism (fork R-C, settled trash-preferred): route to OS trash
where a portable move-to-trash is verified available, else unlink behind the
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)
- **Fork R-C — deletion mechanism (OS trash vs. unlink).** Lean: trash if portably
available (recoverable), else unlink with strong dry-run/confirm. Needs Daniel +
a platform to-verify (SWELL / per-platform trash APIs).
- **Fork R-D — orphan attribution (owned-file manifest vs. index-diff vs.
folder-sweep).** Lean: **owned-file manifest**. **Design-the-seam call:** the
manifest is cheap to maintain from capture onward but a backfill cliff to
reconstruct later — so **start tracking owned files as part of capture / Phase B
now, even though prune ships in Phase R.** Folder-sweep rejected as unsafe (deletes
hand-dropped files). Needs Daniel (touches the persisted shape + ideally lands
earlier than R3).
- **Fork R-E — trigger (manual-only vs. offer-on-orphaning vs. periodic).** Lean:
manual-primary + optional delete-time "prune now"; no background sweep. Needs Daniel.
## Phase R forks — settled 2026-07-24
- **Fork R-C — deletion mechanism.** Settled: **trash-preferred, unlink fallback.**
Route to OS trash where a portable move-to-trash is available (recoverable), else
unlink behind strong dry-run/confirm. Per-platform trash surface (SWELL / Win
`SHFileOperation`·`IFileOperation` / macOS `trashItemAtURL:` / Linux XDG) is a
**must-verify before use**. Folded into R3.
- **Fork R-D — orphan attribution.** Settled: **owned-file manifest**,
`(owned ∩ present) referenced`; folder-sweep rejected as unsafe. **Seam lands
early** — the manifest is written from capture onward (new **B-cap** point in
Phase B), not reconstructed at prune time; R1/R2 consume it. Persistence shape
(sibling `"reasampler"` key vs. `banks` blob) is a small build-time residual.
- **Fork R-E — trigger.** Settled: **manual action + `bank_panel` button**,
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
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 |