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:
2026-07-25 23:30:44 -04:00
42 changed files with 5289 additions and 140 deletions
+304
View File
@@ -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 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)
- `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 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.
---
# 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).