Merge dev into phase-b-multibank (integrate parallel M7/8 + Phase D work before dev promotion)
# Conflicts: # CLAUDE.md # CMakeLists.txt # src/actions.cpp # src/bank_panel.cpp # src/persist.h
This commit is contained in:
+304
@@ -739,3 +739,307 @@ and the settled-decision prose above). One panel-polish detail remains open.
|
||||
- **Active-bank indicator placement (B4 polish)** — per-region headers vs. a single
|
||||
header readout vs. lit-tab treatment. The "visually unmistakable" requirement is
|
||||
settled (fork 4); only the placement is open. Panel-polish detail.
|
||||
|
||||
---
|
||||
|
||||
# Sample removal — additive spec (Phase B, point B5)
|
||||
|
||||
> **Additive section, part of the Multi-bank pillar.** The sample-level companion
|
||||
> to move/copy/evacuate/delete-bank: a verb that **drops a `Sample`'s index entry**
|
||||
> from a bank (or the pool). Index-only, non-destructive to the file — it sits on
|
||||
> the same side of the index/file line as every other Phase B op. Product framing:
|
||||
> `docs/product/removal-and-prune.md` §Sample-remove. Same verify discipline:
|
||||
> **verify every REAPER API name/signature against the SDK header before use.**
|
||||
|
||||
## What it is
|
||||
|
||||
Move, copy, and evacuate all keep a sample *somewhere*; there was no verb to drop
|
||||
a sample outright. **Sample-remove** is that verb: it removes one `Sample` entry
|
||||
from one `BankIndex`. It exposes the `remove` primitive `bank_model`'s `BankIndex`
|
||||
**already has** — B5 wires it to an action + a panel affordance, it does not add a
|
||||
model capability.
|
||||
|
||||
## Settled decisions (spec-level)
|
||||
|
||||
- **Remove is index-only.** It removes the `Sample` from a `BankIndex` and mutates
|
||||
only index + ext-state. No file is written, moved, or deleted; no timeline item
|
||||
is touched. Identical non-destructive posture to move/copy/evacuate/delete-bank.
|
||||
- **Remove can orphan a file — the same designed orphaned-until-prune state a
|
||||
non-empty delete-bank produces.** When remove drops the *last* index reference to
|
||||
a file (no other bank holds its hash), that file becomes an orphan on disk,
|
||||
referenced by no bank, reclaimed later by **prune** (Phase R) — never by remove.
|
||||
This is not a new hazard class; it is the existing "files persist until prune"
|
||||
window, reached by a sample-level verb instead of a bank-level one.
|
||||
- **Collapse-by-hash is unaffected.** Remove targets a specific entry in a specific
|
||||
bank. Because cross-bank dedup is deliberately not enforced, removing a sample
|
||||
from one bank leaves any same-hash entry in another bank intact — the same
|
||||
coexistence copy relies on.
|
||||
- **The pool's *contents* are removable; the pool *container* is not.** Pool
|
||||
privileges (un-deletable, un-renamable, un-evacuable) govern the pool as a
|
||||
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, 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
|
||||
|
||||
- **Non-destructive** extends to remove verbatim: index + ext-state only, no file
|
||||
touched, no timeline item touched.
|
||||
- **Relative-paths-only** is unaffected — remove deletes an entry, it adds no path
|
||||
handling.
|
||||
- **Determinism / bit-identical / null-test (capture)** untouched — remove sits
|
||||
above the file, same as all of multi-bank.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Confirm on last-reference remove; don't confirm otherwise.** A remove that
|
||||
drops the *last* index reference to a file orphans it (until prune) — confirm
|
||||
that case, naming the consequence ("…its file remains on disk until pruned"). A
|
||||
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, 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 B1–B4, 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)
|
||||
|
||||
- `bank_book` / `BankIndex` (pure) — expose remove of a `Sample` from a bank's
|
||||
index (the existing `BankIndex::remove` primitive, surfaced through the book);
|
||||
pool contents removable, pool-container privileges unchanged.
|
||||
- `actions` (entry) — "remove selected sample(s) from bank" (and, under fork R-A,
|
||||
a scope parameter); registered with the `command_id`/`gaccel`/`hookcommand`
|
||||
contract; MIDI-bindable to suit the capture-heavy workflow.
|
||||
- `bank_panel` (affordance) — remove on the current selection (menu entry / key),
|
||||
reusing the M5 selection model exactly as move/copy do; confirm-on-last-reference
|
||||
at this layer.
|
||||
|
||||
## REAPER API surface
|
||||
|
||||
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).
|
||||
|
||||
## Settled forks (Daniel, 2026-07-24)
|
||||
|
||||
- **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 B1–B4),
|
||||
with the ext-state-participation SDK check as a must-verify-before-build. Folded
|
||||
into Guardrails above and the Phase B / B1 plan points.
|
||||
|
||||
---
|
||||
|
||||
# Prune — file-lifecycle spec (Phase R — Reclaim)
|
||||
|
||||
> **New pillar, its own lettered phase.** Prune is the file-lifecycle path the
|
||||
> capture and multi-bank specs forward-reference throughout ("files persist on disk
|
||||
> until prune", "the capture/prune path reclaims it") but that had no phase, module,
|
||||
> or point until now. It is the **only** operation in ReaSampler that deletes bytes
|
||||
> off disk. Namespaced **`R` (Reclaim)** alongside `M` (capture), `D` (Design View),
|
||||
> `B` (Banks) — it is a distinct pillar, not a Multi-bank sub-step, because it
|
||||
> serves *every* orphan-producing path (delete-bank, sample-remove, re-capture) and
|
||||
> carries a new risk class (file deletion) with its own invariants. Product framing
|
||||
> and the phase-placement justification: `docs/product/removal-and-prune.md` §Prune.
|
||||
> Same discipline: **verify every REAPER/SWELL/filesystem API name/signature against
|
||||
> the SDK/SWELL headers before use.**
|
||||
|
||||
## What it is
|
||||
|
||||
Over a project's life, delete-bank and sample-remove (and, potentially, M10
|
||||
re-capture superseding an old file) leave `.wav` files on disk that no bank index
|
||||
references — the "orphaned-until-prune" state the specs design in on purpose.
|
||||
**Prune is the reclaim pass**: reconcile the physical bank folder against the union
|
||||
of every bank's index, and reclaim the files nothing references. It makes good on
|
||||
the promise the rest of the spec keeps making.
|
||||
|
||||
## The load-bearing rule
|
||||
|
||||
> **Remove creates orphans; prune reclaims them.** Sample-remove and delete-bank
|
||||
> drop index entries and may leave a file referenced by nothing. Prune is the
|
||||
> single path that turns such an orphan back into free disk space. **No other
|
||||
> operation deletes a file; prune deletes *only* files that no index references.**
|
||||
> A bank op that deletes a file is still a bug — prune is not a bank op, it is the
|
||||
> file-lifecycle op.
|
||||
|
||||
This asymmetry is deliberate and must be stated loudly: every *other* invariant
|
||||
says "no operation deletes a file." Prune is the sole, explicit exception, and its
|
||||
entire job is deletion — so it must be the *only* file-deleting authority in the
|
||||
system, with the strongest guardrails.
|
||||
|
||||
## Mirror of `reconcile` — the pure pattern one level down
|
||||
|
||||
Prune reuses the shape Design View already shipped. `view_mode_model`'s
|
||||
`ViewModeModel::reconcile(liveGuids)` reconciles *membership entries* against *live
|
||||
tracks* and returns the residuals to drop. **Prune reconciles *files on disk*
|
||||
against *referenced files*** (the union of every bank's index) and returns the
|
||||
orphan set to delete. Same pure pattern, one level down (files instead of GUIDs).
|
||||
|
||||
The **decision is pure and unit-tested**: given the set of files present in the
|
||||
bank folder and the set of files referenced by the book, compute the orphan set.
|
||||
Only the two ends touch the shell — *enumerating* the bank folder and *deleting*
|
||||
the orphans are filesystem I/O. Keep the "which files are orphans" core REAPER-free
|
||||
and hard-tested (this is the safety-critical part); keep the I/O thin. Same
|
||||
pure/shell split as `bank_model` / `view_mode_model` / `bank_book`.
|
||||
|
||||
## Settled decisions (spec-level)
|
||||
|
||||
- **Referenced-set is the union across ALL banks, pool included.** A file is an
|
||||
orphan iff **no** bank in the book references it. Because copy lets one file be
|
||||
referenced by several banks, prune must union references across the whole book
|
||||
before deciding. This is the safety-critical computation — the **prune null
|
||||
test** is *prune never deletes a file that any index references.*
|
||||
- **Project-relative resolution, current folder.** Prune enumerates and deletes
|
||||
within the project bank folder using the **same M4 project-relative path
|
||||
resolution** the index uses, against the *resolved current* folder — never a
|
||||
stale absolute path — so a Save-As relocation cannot cause it to mis-identify or
|
||||
mis-target orphans.
|
||||
- **Dry-run first, always.** Prune reports before it deletes: the orphan count,
|
||||
reclaimed size, and (for a small set) the files. The dry-run — compute-and-report,
|
||||
the pure core with no deletion — is the primary surface; actual deletion is the
|
||||
confirmed second step. A prune that silently sweeps is unacceptable for an
|
||||
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. 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
|
||||
|
||||
- **The single intentional exception to "no operation deletes files."** Stated
|
||||
above; called out again here so the invariant table is honest: prune is
|
||||
destructive-to-files *by design and by exclusive authority*.
|
||||
- **Relative-paths-only / Save-As machinery reused** — prune resolves paths the
|
||||
same way the index does (M4), so it inherits relative-path correctness and
|
||||
Save-As survival; it introduces no new path handling.
|
||||
- **Determinism / bit-identical / null-test (capture)** untouched — prune sits
|
||||
below the capture path entirely.
|
||||
- **Prune null test (new invariant):** a prune of a folder whose every file is
|
||||
referenced by some bank deletes nothing; a prune deletes exactly the
|
||||
`present − referenced` orphan set and nothing else. Ship as a tested property of
|
||||
the pure core.
|
||||
|
||||
## Guardrails — the genuinely destructive act
|
||||
|
||||
- **Dry-run + confirm-with-manifest** (above): the user approves a *specific*
|
||||
deletion (count + size + files), never an abstract "clean up."
|
||||
- **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, 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 }`,
|
||||
`{ 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 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 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)
|
||||
|
||||
No new REAPER *audio* API. New surfaces to verify before use:
|
||||
- **Filesystem enumeration + delete** — directory listing and file removal for the
|
||||
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, 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.
|
||||
|
||||
## Non-goals / guardrails
|
||||
|
||||
- **Prune is the ONLY file-deletion authority.** No bank op, no capture op, no
|
||||
Design View op deletes a file. If any path other than prune deletes a bank file,
|
||||
reject it in review.
|
||||
- **No general folder cleaning.** Prune reclaims the bank system's own unreferenced
|
||||
leavings, not arbitrary files a user placed in the folder (fork R-D governs the
|
||||
attribution).
|
||||
- **No silent deletion.** Dry-run + explicit confirm always; no background sweep.
|
||||
- **No file deleted while any index references it.** The referenced-set union
|
||||
across all banks is the safety-critical invariant — enforce and test it in the
|
||||
pure core, not just the UI.
|
||||
- **Additive only.** Prune reads the book and the folder; it does not modify
|
||||
`BankIndex`, `bank_book`, the capture roadmap, or Design View semantics.
|
||||
|
||||
## Settled forks (Daniel, 2026-07-24)
|
||||
|
||||
- **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).
|
||||
|
||||
Reference in New Issue
Block a user