Scope sample-remove (B5) and prune (Phase R) into the plan
Adds the two missing removal capabilities: sample-level remove (Phase B point B5, index-only) and the prune file-lifecycle path (new Phase R, the sole file-deleting op), resolving the dangling capture/prune reference. Adds CONTEXT.md spec, PLAN.md points, and a product note; surfaces forks R-A..R-E for decision.
This commit is contained in:
+274
@@ -731,3 +731,277 @@ 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, 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.
|
||||
|
||||
## 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, 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.
|
||||
|
||||
## 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).
|
||||
|
||||
## Open questions
|
||||
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
# 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. How prune attributes ownership is fork R-D
|
||||
(below). Prune is a reclaimer of ReaSampler's own orphans, not a general folder
|
||||
cleaner.
|
||||
|
||||
## 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, 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
|
||||
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`.
|
||||
|
||||
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.
|
||||
- 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.
|
||||
|
||||
## 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)** — 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.
|
||||
- **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.
|
||||
|
||||
## Open questions to resolve during build
|
||||
|
||||
- **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.
|
||||
|
||||
Reference in New Issue
Block a user