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.
|
||||
|
||||
@@ -118,23 +118,34 @@ landed milestone.
|
||||
> spec. Product framing: `docs/product/design-view.md` §Two-canvas direction. When a
|
||||
> point lands, doc-keeper moves it to `COMPLETED.md`.
|
||||
>
|
||||
> **D2-W1 (pure lane extension) and D2-W2 (shell: lane application + new-content detection) have landed** — see `COMPLETED.md`.
|
||||
> **D2-W1 (pure lane extension), D2-W2 (shell: lane application + new-content
|
||||
> detection), and D2-W3-A (lane minting + item→lane assignment + persist
|
||||
> round-trip) have landed** — see `COMPLETED.md`.
|
||||
|
||||
## D2-W3 — actions, persist wiring, panel UI
|
||||
**Goal:** Any new lane/mode-management actions, the persist slice serializing the
|
||||
lane-ownership index alongside the membership index, and any panel UI indicator.
|
||||
## D2-W3-B — two-canvas actions + panel UI indicators
|
||||
**Goal:** Any new lane/mode-management actions (bindable in the Actions list), and
|
||||
any panel UI indicator for lane/mode state. The persist slice and the
|
||||
lane-ownership index round-trip were completed in D2-W3-A; this wave closes the
|
||||
remaining UI and action surface.
|
||||
See CONTEXT.md §Two-canvas sub-phase (Module architecture — persistence).
|
||||
**Verify (in DAW):** The lane-ownership index survives Save / Save As / reopen
|
||||
(rides in the `"reasampler"` `view_state` alongside the membership index);
|
||||
lane/mode-management actions are registered and bindable.
|
||||
**Depends on:** D2-W2.
|
||||
**Verify (in DAW):** Lane/mode-management actions are registered and bindable in
|
||||
the Actions list; the panel UI indicator reflects the current lane/mode state.
|
||||
**Depends on:** D2-W3-A.
|
||||
|
||||
- [ ] Any new lane/mode-management actions (`command_id`/`gaccel`/`hookcommand`);
|
||||
bindable in the Actions list.
|
||||
- [ ] Persist slice: serialize/deserialize the lane-ownership index in the
|
||||
`"reasampler"` `view_state` section, alongside the membership index.
|
||||
- [ ] Any panel UI indicator for lane/mode state.
|
||||
|
||||
**D2-W3-A code-review polish (fold into this wave):**
|
||||
- [ ] Simplify the redundant per-mint `I_NUMFIXEDLANES` re-read in `view.cpp`
|
||||
(`applyMintPlan`): a single grow-and-track pass removes the second
|
||||
`GetMediaTrackInfo_Value` call inside the mint loop.
|
||||
- [ ] Optional shared item-read helper to remove duplicated `itemGuid`/`itemLaneName`
|
||||
read logic between `view.cpp` and `bank_panel.cpp`.
|
||||
- [ ] Optional defensive note/check in `reconcileManagedLanes` for the edge case
|
||||
where a managed lane name encodes an unregistered mode id (log and skip rather
|
||||
than silently record an orphaned ownership entry).
|
||||
|
||||
---
|
||||
|
||||
# Phase B — Multi-bank (parallel to the M0–M11 capture roadmap and Phase D)
|
||||
@@ -254,6 +265,34 @@ and product notes → *Fork 5 — settled*.)
|
||||
index-only and reversible). **Verify the drag hit-test doesn't collide with the M5
|
||||
grid's multi-select drag.**
|
||||
|
||||
## B5 — sample-remove (the missing sample-level verb)
|
||||
**Goal:** Drop an individual `Sample`'s index entry from a bank or the pool —
|
||||
the sample-level companion to move/copy/evacuate/delete-bank. Index-only,
|
||||
non-destructive to the file; exposes the `BankIndex::remove` primitive that
|
||||
`bank_model` already has (wires it, does not add it). CONTEXT.md §Sample removal.
|
||||
Product framing + open forks: `docs/product/removal-and-prune.md` §Sample-remove.
|
||||
**Verify (in DAW):** Remove drops the selected sample's entry from the target
|
||||
bank; a same-hash entry in another bank is untouched (no cross-bank dedup);
|
||||
pool *contents* are removable while pool-container privileges hold; removing the
|
||||
last index reference to a file leaves that file on disk (orphaned until prune —
|
||||
never deleted by remove); non-destructive (index + ext-state only, no file, no
|
||||
timeline item).
|
||||
**Depends on:** B1, B2, B3 (action set), B4 (panel affordance).
|
||||
|
||||
- [ ] 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).
|
||||
- [ ] `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
|
||||
bank references it) confirms, naming the orphaned-until-prune consequence;
|
||||
remove of a still-referenced sample does not confirm.
|
||||
- [ ] Tests: remove drops the target entry; same-hash entry in another bank
|
||||
survives; remove-from-pool allowed; last-reference remove leaves an orphan (file
|
||||
untouched); non-destructive (no file/timeline mutation).
|
||||
|
||||
## Phase B open questions
|
||||
All five forks settled by Daniel (2026-07-23): persistence key = fold pool into `banks`,
|
||||
retire legacy key (1a); delete drops members + add evacuate verb (2); move is the
|
||||
@@ -265,3 +304,102 @@ ready to scope into implementation waves. One polish detail remains:
|
||||
- **Active-bank indicator placement** — per-region headers vs. single header readout
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
# Phase R — Reclaim (file lifecycle: the prune path)
|
||||
|
||||
> **New pillar, own lettered namespace.** Prune is the file-lifecycle path the
|
||||
> capture and multi-bank specs forward-reference throughout ("files persist on disk
|
||||
> until prune") but that had no phase, module, or point. It is the **only** operation
|
||||
> in ReaSampler that deletes bytes off disk. Namespaced **`R` (Reclaim)** alongside
|
||||
> `M`/`D`/`B` because it is a distinct pillar — it serves *every* orphan-producing
|
||||
> path (delete-bank, sample-remove B5, potentially M10 re-capture), not just
|
||||
> Multi-bank, and it carries a new risk class (file deletion) with its own
|
||||
> invariants. Authoritative spec: **CONTEXT.md §Prune — file-lifecycle spec**.
|
||||
> Product framing + phase-placement justification + forks:
|
||||
> `docs/product/removal-and-prune.md` §Prune. When a point lands, doc-keeper moves it
|
||||
> to `COMPLETED.md`.
|
||||
>
|
||||
> **Boundary (load-bearing):** *remove creates orphans; prune reclaims them.* No
|
||||
> operation other than prune deletes a file; prune deletes only files no index
|
||||
> references. A bank op that deletes a file is still a bug.
|
||||
>
|
||||
> **Depends on:** B1, B2 (needs the multi-bank book to union the referenced-set
|
||||
> across all banks) and B5 conceptually (sample-remove is a primary orphan-producer,
|
||||
> so remove-then-prune is the coherent pair — mirror of evacuate-then-delete). Does
|
||||
> **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
|
||||
`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).
|
||||
|
||||
- [ ] 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 = present−referenced;
|
||||
a copied file referenced by a second bank survives; empty folder / empty book 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).
|
||||
**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.
|
||||
- [ ] 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.**
|
||||
|
||||
- [ ] "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.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
# Removal & prune — product notes
|
||||
|
||||
Framing, rationale, and open forks behind the two missing removal capabilities:
|
||||
**sample-remove** (a sample-level index verb) and **prune** (the file-lifecycle
|
||||
path CONTEXT.md keeps forward-referencing but never scoped). The tickable spec
|
||||
lives in `PLAN.md` (Phase B point B5 for remove; **Phase R** for prune) and the
|
||||
authoritative technical detail in `CONTEXT.md` (§Sample removal, §Prune — file
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## The one boundary that governs everything: index vs. file
|
||||
|
||||
ReaSampler already draws a hard line, stated repeatedly in CONTEXT.md: **a bank
|
||||
operation touches the *index*, never the *file*.** Move, copy, evacuate, and
|
||||
delete-bank are all index-only; files persist on disk "until prune." Every
|
||||
removal capability below sits on exactly one side of that line, and keeping the
|
||||
two verbs on opposite sides is the whole design.
|
||||
|
||||
- **Sample-remove drops an index entry.** It is the sample-level sibling of the
|
||||
bank verbs — move/copy/evacuate all keep the sample *somewhere*; remove is
|
||||
"drop this entry outright." It is **index-only, non-destructive to the file**,
|
||||
and — exactly like a plain delete-bank of a non-empty bank — it can *create*
|
||||
an orphan when it removes the last index reference to a file. It sits on the
|
||||
**same side of the line as every existing Phase B op.**
|
||||
|
||||
- **Prune deletes files off disk.** It is the *only* operation in the entire
|
||||
system that removes bytes. It reconciles the physical bank folder against the
|
||||
union of all bank indices and reclaims files referenced by no bank. It sits on
|
||||
the **file side of the line, alone.**
|
||||
|
||||
So the crisp statement, worth putting in the spec verbatim:
|
||||
|
||||
> **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 no index references.
|
||||
|
||||
This is why they are two different scope objects (Phase B vs. Phase R), even
|
||||
though a naive reading ("both are 'delete' verbs") would lump them together.
|
||||
|
||||
---
|
||||
|
||||
## Sample-remove — the missing sample-level verb
|
||||
|
||||
### What the user is doing
|
||||
|
||||
The user has a sample they no longer want in a bank (or in the pool): a bad take,
|
||||
a duplicate they don't want collapsed, a sample they filed into "Drums" by
|
||||
mistake. Today they can *relocate* it (move/copy/evacuate) but they cannot drop
|
||||
it. Remove is the "get this out of here" verb. Two intents hide inside it, and
|
||||
the distinction is a fork (R-A below):
|
||||
|
||||
1. **Remove from *this* bank** — drop the entry from the bank the user is looking
|
||||
at, leaving any copies in other banks untouched. (If the sample was copied
|
||||
into "Drums" and also lives in the pool, remove-from-Drums leaves the pool copy
|
||||
alone.)
|
||||
2. **Remove from *everywhere*** — drop every index entry for this sample across
|
||||
all banks in one act ("purge this sample from the library").
|
||||
|
||||
### Reconciling with existing invariants
|
||||
|
||||
- **Collapse-by-hash:** unaffected. Remove operates on a specific `Sample` entry
|
||||
in a specific `BankIndex` (the `remove` primitive `bank_model` already has, per
|
||||
CLAUDE.md — B5 exposes it, it does not add it). Because dedup is per-bank and
|
||||
cross-bank dedup is deliberately *not* enforced, "remove from this bank" and
|
||||
"the same hash still lives in another bank" coexist cleanly — that is the same
|
||||
coexistence copy already relies on.
|
||||
- **Files-are-never-deleted-by-a-bank-op:** upheld. Remove is index-only, exactly
|
||||
like move/copy/evacuate/delete-bank. When remove drops the *last* reference to a
|
||||
file, it produces the **same orphaned-until-prune state** a non-empty
|
||||
delete-bank already produces — a designed state, not a new hazard class. The
|
||||
file is reclaimed later by prune, never by remove.
|
||||
- **Pool privileges:** the pool cannot be *deleted, renamed, or evacuated*, but
|
||||
individual samples **can** be removed from the pool — otherwise the pool would
|
||||
become a roach-motel (samples check in, never leave except by moving to a named
|
||||
bank). Remove-from-pool is allowed; it is the pool's own "drop this sample"
|
||||
verb. (The pool-as-container privilege is untouched; only its *contents* are
|
||||
removable.)
|
||||
- **Non-destructive:** remove mutates only index + ext-state, touches no file and
|
||||
no timeline item — the Phase B non-destructive guarantee extends to it verbatim.
|
||||
|
||||
### Guardrails
|
||||
|
||||
Remove is *less* dangerous than it first looks, because it never deletes a file —
|
||||
the bytes survive on disk until an explicit prune. So the recovery story is: an
|
||||
accidental remove loses the *index entry*, not the audio. But there are two
|
||||
sharpnesses to guard:
|
||||
|
||||
- **Last-reference remove is the orphan-maker.** Removing a sample that exists in
|
||||
only one bank orphans its file (until prune). This is the same footgun as
|
||||
non-empty delete-bank, and it deserves the same treatment: **confirm when the
|
||||
remove drops the last index reference** ("Remove 'kick_03'? It is in no other
|
||||
bank — its file will remain on disk until pruned."). A remove of a sample that
|
||||
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.
|
||||
|
||||
### Where it lives
|
||||
|
||||
Remove is a `bank_book`/`BankIndex` verb (pure), a bindable `actions` entry, and a
|
||||
`bank_panel` affordance on the current selection — the exact three-layer shape
|
||||
every Phase B verb already takes. That is why it belongs **in Phase B as B5**, not
|
||||
in a phase of its own: same modules, same pattern, same side of the index/file
|
||||
line. It is the verb Phase B forgot, not a new pillar.
|
||||
|
||||
---
|
||||
|
||||
## Prune — the file-lifecycle path CONTEXT.md kept promising
|
||||
|
||||
### What the user is doing
|
||||
|
||||
The user has been working for a while: capturing, re-capturing (M10), deleting
|
||||
banks, removing samples. Each of those left files on disk that no index references
|
||||
any more — the "orphaned-until-prune" state the multi-bank spec designs in on
|
||||
purpose. Over a long project the bank folder accumulates dead `.wav` files that
|
||||
cost disk and clutter. **Prune is the reclaim pass**: "sweep the bank folder,
|
||||
delete the files nothing references, tell me what you reclaimed."
|
||||
|
||||
This is the path CONTEXT.md forward-references in at least four places ("files
|
||||
persist on disk until prune," "the capture/prune path reclaims it") but never
|
||||
scopes. It is a real, promised capability with **no phase, no module, no point**
|
||||
— a dangling reference the plan has to make good on.
|
||||
|
||||
### The load-bearing precedent: prune-on-reconcile already exists for modes
|
||||
|
||||
ReaSampler already shipped this exact shape once. Design View's `view_mode_model`
|
||||
has **`ViewModeModel::reconcile(liveGuids)`** — a pure function fed the live set
|
||||
(the tracks that still exist), returning the residual membership entries to drop
|
||||
(CONTEXT.md §Design View: "prunes orphaned snapshots on every toggle/load;
|
||||
tolerates unknown/stale GUIDs (prune on reconcile)"). Prune is the **file-pool
|
||||
mirror of that pure pattern**:
|
||||
|
||||
> `reconcile(liveGuids)` reconciles *membership entries* against *live tracks*.
|
||||
> Prune reconciles *files on disk* against *referenced files* (the union of every
|
||||
> bank's index). Same shape — feed the pure core the live set, get back the
|
||||
> residuals — one level down (files instead of GUIDs).
|
||||
|
||||
This is the "mirror the existing pure pattern" move the whole codebase is built
|
||||
on (`bank_model`, `view_mode_model`, `bank_book` are all the same pure-registry
|
||||
shape). The **pure part of prune** — "given the set of files on disk and the set
|
||||
of files referenced by the book, compute the orphan set" — is a REAPER-free,
|
||||
unit-testable function that belongs with the pure cores. Only the two ends are
|
||||
shell work: *enumerating* the bank folder (filesystem I/O) and *deleting* the
|
||||
orphans (filesystem I/O). Keep the decision (which files are orphans) pure and
|
||||
tested; keep the I/O thin. This is the same pure/shell split as everything else.
|
||||
|
||||
### Reconciling with existing invariants
|
||||
|
||||
- **This is the one place the "files are never deleted" rule is *intentionally*
|
||||
broken — and it must be the *only* one.** Every other invariant says "no bank op
|
||||
deletes a file." Prune is explicitly not a bank op; it is the file-lifecycle op,
|
||||
and its entire job is deletion. The spec must state this asymmetry loudly so a
|
||||
reviewer never reads prune as violating the bank-op rule: **prune is the sole
|
||||
file-deletion authority; a bank op that deletes a file is still a bug.**
|
||||
- **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 means one file can
|
||||
be referenced by several banks, prune must union references across the whole
|
||||
book before deciding — deleting a file still referenced by "Drums" because it
|
||||
left the pool would be catastrophic. The referenced-set computation is the
|
||||
safety-critical core and the thing to test hardest (the null test of prune:
|
||||
*prune never deletes a file that any index references*).
|
||||
- **Relative-paths-only / project-relative resolution:** prune enumerates and
|
||||
deletes within the project bank folder using the same M4 project-relative path
|
||||
resolution the index uses. It must resolve the *same* way the index does, or it
|
||||
could mis-identify orphans across a Save-As relocation. Prune runs against the
|
||||
*resolved current* bank folder, never a stale absolute path.
|
||||
- **Collapse-by-hash:** irrelevant to prune's decision (prune works on files and
|
||||
references, not hashes) but worth noting: two index entries that collapsed onto
|
||||
one file mean one file, multiple references — prune's union handles this for
|
||||
free (the file is referenced, so it survives).
|
||||
- **Determinism / bit-identical / null-test (capture):** untouched — prune sits
|
||||
below the capture path entirely, same as multi-bank.
|
||||
|
||||
### Guardrails — this is the genuinely destructive act
|
||||
|
||||
Prune deletes real bytes irreversibly (a deleted `.wav` is gone unless it went to
|
||||
an OS trash — see fork R-C). It earns the strongest guardrails in the product:
|
||||
|
||||
- **Dry-run first, always.** Prune should *report before it deletes*: "12 files
|
||||
(34 MB) are referenced by no bank. Delete them?" A prune that silently sweeps is
|
||||
unacceptable for an irreversible file-delete. The dry-run (compute-and-report,
|
||||
the pure core with no deletion) is arguably the *primary* surface, and the
|
||||
actual deletion is the confirmed second step. This mirrors how every safe
|
||||
garbage-collector / disk-cleaner works (npm prune --dry-run, git gc reporting,
|
||||
Lightroom's "delete rejected photos" confirmation).
|
||||
- **Confirm with a manifest.** The confirmation names the count and the reclaimed
|
||||
size and — for a small set — the files. The user approves a *specific* deletion,
|
||||
not an abstract "clean up."
|
||||
- **Never touch a referenced file, and never touch a non-bank file.** Prune's
|
||||
scope is *files in the bank folder that the book once owned and no longer
|
||||
references*. A file that was never a bank file (a user dropped something into the
|
||||
folder by hand) is out of scope — prune should only reclaim files it can
|
||||
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.
|
||||
|
||||
### Where it lives — and why it is its own phase, not a Phase B point
|
||||
|
||||
Prune is **not** a Phase B point. Three reasons it earns its own lettered phase
|
||||
(proposed **Phase R — Reclaim / file lifecycle**):
|
||||
|
||||
1. **It is a different pillar.** Phase B is the *bank container* pillar
|
||||
(index-only, non-destructive, above the file). Prune is the *file lifecycle*
|
||||
pillar (the one path that deletes files). CONTEXT.md already names it as a
|
||||
separate concern every time it says "the capture/**prune** path" — file
|
||||
lifecycle is spoken of as its own thing, owned by neither the capture nor the
|
||||
bank layer. Giving it its own phase matches how the spec already talks about it.
|
||||
2. **It serves more than Phase B.** Orphans are produced by delete-bank *and*
|
||||
sample-remove (B5) *and*, arguably, by M10 re-capture superseding an old file,
|
||||
*and* by Design View's deleted-track residual files if any exist. Prune is the
|
||||
downstream reclaim for *all* file-orphaning paths, not a Phase-B-internal
|
||||
cleanup. A capability multiple pillars forward-reference should not be nested
|
||||
inside one of them.
|
||||
3. **It carries a new risk class and new invariants.** Every phase so far has been
|
||||
non-destructive-to-files by construction. Prune is the first phase that deletes
|
||||
files, so it needs its own invariant section (the prune null test, the
|
||||
referenced-set union, the dry-run guarantee, trash-routing). Burying that under
|
||||
a Phase B checkbox would hide the one genuinely destructive capability in the
|
||||
product inside a phase whose headline invariant is "non-destructive." The
|
||||
dissonance alone argues for separation.
|
||||
|
||||
Lettered, per the established convention (`M` = capture pillar, `D` = Design View,
|
||||
`B` = Banks): **`R` = Reclaim.** It reads correctly — Phase R is "the file
|
||||
lifecycle pillar," not "a bank sub-step."
|
||||
|
||||
**Sequencing:** Phase R depends on B1/B2 (it needs the multi-bank book to compute
|
||||
the referenced-set union across all banks) and on B5 conceptually (sample-remove
|
||||
is a primary orphan-producer, so prune is most useful once remove exists), but it
|
||||
does not depend on the B3/B4 *UI*. It can land any time after the book exists;
|
||||
practically it should follow B5 so the two removal verbs ship as a coherent pair
|
||||
(remove-then-prune is the workflow, mirroring evacuate-then-delete).
|
||||
|
||||
---
|
||||
|
||||
## Open forks — need a Daniel decision
|
||||
|
||||
### 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
|
||||
`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.
|
||||
|
||||
**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 B1–B4, so surfacing it
|
||||
now rather than at B5 is deliberate — the seam is cheapest before B1 lands).
|
||||
|
||||
### 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-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.
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## Summary of the boundary (for the spec)
|
||||
|
||||
| | Sample-remove (B5) | Delete-bank (B1/B3, shipped-spec) | Prune (Phase R) |
|
||||
|---|---|---|---|
|
||||
| Object | one `Sample` entry | one bank + its member entries | files on disk |
|
||||
| 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) |
|
||||
| Guardrail | confirm on last-ref | confirm on non-empty | dry-run + manifest confirm |
|
||||
Reference in New Issue
Block a user