diff --git a/CLAUDE.md b/CLAUDE.md index a855a1b..44c0851 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repo identity and current state -The CMake project and binary are now named `reaper_reasampler`. This is **ReaSampler** — a per-project audio sample-bank capture tool. The MPE modules (`mpe_model`, `mpe_view`) have been removed. M0–M8 are complete (bank_model, peaks, capture offline+realtime, persist, bank_panel, insert, capture action family, RealtimeRecordBackend, tail T1+T2+T1-followons). Phase B multi-bank (B1–B5, B-cap), Phase D1/D2 Design View (D1–D5, D2-W1–W3-B), Phase V versioning/beta-channel (V1/V3, V4), and M10 provenance + re-capture from source have all landed. M9 slots, M11 polish, and Phase R prune remain. The *discipline* — pure REAPER-free testable core split from REAPER-facing shells — is **preserved** throughout. +The CMake project and binary are now named `reaper_reasampler`. This is **ReaSampler** — a per-project audio sample-bank capture tool. The MPE modules (`mpe_model`, `mpe_view`) have been removed. M0–M8 are complete (bank_model, peaks, capture offline+realtime, persist, bank_panel, insert, capture action family, RealtimeRecordBackend, tail T1+T2+T1-followons). Phase B multi-bank (B1–B5, B-cap), Phase D1/D2 Design View (D1–D5, D2-W1–W3-B), Phase V versioning/beta-channel (V1/V3, V4), M10 provenance + re-capture from source, and Phase R Reclaim (R1–R3: prune-reconcile core, dry-run shell, guarded deletion + action + panel button) have all landed. M9 slots and M11 polish remain. The *discipline* — pure REAPER-free testable core split from REAPER-facing shells — is **preserved** throughout. CONTEXT.md is the authoritative spec and build roadmap. Read it first for any non-trivial task. Every REAPER API name cited there is correct-by-intent; verify argument order, types, and flag values against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use. @@ -37,6 +37,8 @@ Key targets (see CMakeLists.txt for the full list): | `owned_manifest_tests` | executable | Pure unit tests for `owned_manifest` — no REAPER, no DAW. | | `app_version_tests` | executable | Pure unit tests for `app_version` — no REAPER, no DAW. | | `provenance_tests` | executable | Pure unit tests for `provenance` — no REAPER, no DAW. | +| `prune_reconcile_tests` | executable | Pure unit tests for `prune_reconcile` — no REAPER, no DAW. | +| `prune_button_tests` | executable | Pure unit tests for `prune_button` — no REAPER, no DAW. | | `reaper_reasampler` | loadable module | The actual extension binary (`.dll` / `.dylib` / `.so`). | ### Beta channel build (Phase V, V4) @@ -73,16 +75,18 @@ There is no hot-reload. Copy the built binary into REAPER's `UserPlugins/` folde - `app_version` — REAPER-free version/channel identity (Phase V, V1+V4): CMake-sourced semver constant (`appVersion()`), ext-state stamp value (`stampVersion()` — numeric triple only, no channel suffix), `parseVersion`/`versionLess`/`classifyWritingVersion`, and the full set of channel-derived identity accessors (`extStateNamespace()`, `commandIdPrefix()`, `actionDisplayPrefix()`, `binaryName()`, `dockTitle()`, `dockIdent()`, `channelCommandId()`, `channelActionName()`). All channel strings derive from the one `REASAMPLER_CHANNEL_IS_BETA` bit threaded in via `configure_file` → `version_generated.h`; no scattered `#ifdef`s in the shells. - `wav_trim` — 32-bit-float WAV parse + header-aware truncate plan (RIFF/data size rewrite) for the realtime tail's PCM decay-scan trim (T2). Rejects WAVE_FORMAT_EXTENSIBLE with non-float SubFormat GUID. Depends on `peaks` for the `AudioSample` float alias. - `provenance` — capture-recipe fingerprint (M10): build/encode/compare a `rsprov1` length-prefixed fingerprint of scope, exact range, tail, rate/channels, track GUIDs, and order-sensitive FX-chain identity; parse/compare for drift detection on re-capture. A thin reproducibility fingerprint — NOT a serialized chain to restore. Drives `BankIndex::updateInPlace` / `BankBook::updateSampleInPlace` (order-preserving, id-stable) on re-capture. +- `prune_reconcile` — Phase R pure prune core: `pruneOrphans(present, referenced, owned)` computes `(owned ∩ present) − referenced` (exact-string path match); `buildPruneReport` tallies count/bytes/display-capped file list; `pruneDeletePlan` produces the confirm-time staleness intersection (`confirmed ∩ freshOrphans`). REAPER-free, filesystem-free. The safety-critical "which files are orphans" decision — hard-tested here before any I/O exists. Also: `BankBook::referencedPaths()` additive const union query (all banks incl. pool, de-duped) added to `bank_book`. +- `prune_button` — Phase R pure layout/hit-test for the `bank_panel` footer Prune button: `computePruneButton` (right-anchored, suppressed gracefully when footer is too narrow) + `hitTestPruneButton`. Mirror of `mode_switch` / `tab_strip`. **REAPER-facing shells:** - `capture` — `ICaptureBackend` interface; `OfflineRenderBackend` (deterministic default) and `RealtimeRecordBackend`. Input: `CaptureRequest`. Output: finished file + populated `Sample` handed to `bank_model`. - `insert` — placement via `InsertMedia`; conform-to-project-tempo is an explicit opt-in flag, never silent stretching. - `bank_panel` — docked LICE-drawn grid: thumbnails, audition, multi-select, keyboard navigation. -- `persist` — project ext state (`SetProjExtState` / `GetProjExtState`, namespace `"reasampler"`) ↔ `BankBook` JSON (`"banks"` key) + `ViewModeModel` JSON (`"view_state"` key) + `TailSetting` JSON (`"tail_setting"` key) + `OwnedManifest` JSON (`"owned_files"` key) + writing-version stamp (`"version"` key, written via `stampVersion()` on every `saveToActiveProject()`); project-relative path resolution. A `projectconfig` hook (`BeginLoadProjectState(isUndo)`) triggers a deferred session reload on undo/redo so Ctrl-Z/redo visibly restores book/view/tail/manifest in-session. +- `persist` — project ext state (`SetProjExtState` / `GetProjExtState`, namespace `"reasampler"`) ↔ `BankBook` JSON (`"banks"` key) + `ViewModeModel` JSON (`"view_state"` key) + `TailSetting` JSON (`"tail_setting"` key) + `OwnedManifest` JSON (`"owned_files"` key) + writing-version stamp (`"version"` key, written via `stampVersion()` on every `saveToActiveProject()`); project-relative path resolution. A `projectconfig` hook (`BeginLoadProjectState(isUndo)`) triggers a deferred session reload on undo/redo so Ctrl-Z/redo visibly restores book/view/tail/manifest in-session. Hosts `ReaSamplerSession::pruneDryRun()` (read-only orphan enumeration via M4 project-relative resolution) and `pruneOrphanSet()` (full-set query for the R3 delete path); supplies `referencedPaths()` + `owned().paths()` to the `prune_reconcile` pure core. - `view` — Design View shell: reads the folder tree via `view_tree`, snapshots flag values before parking, drives hide + CPU-park on inactive-mode leaves (`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline) and derived visibility on parents; restores from snapshot. Never touches master or `B_MUTE`/`I_SOLO`. - `track_guid` — shared `MediaTrack*` → canonical GUID-string formatter; single source of truth for membership keys used by both the view shell and the actions layer. - `provenance_shell` — FX-chain identity queries via `TrackFX_*` / `TakeFX_*` APIs; collects source-item paths and parent-detection inputs to feed the pure `provenance` fingerprint builder. Stamps `Sample.provenance` on capture when every resolving source item maps by exact normalized path (case-folded on Windows) to exactly one bank sample; ambiguous/mixed cases record nothing conservatively. -- `actions` — registers the capture/placement/slot action family, the Design View action family (toggle active mode, activate Arrange/Design, tag/untag selected tracks, show-both), and the multi-bank action family (create/rename/reorder/delete bank, evacuate, activate, move/copy/remove selected samples); routes each to the modules above via the `command_id`/`gaccel`/`hookcommand` contract. Every bank index verb wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is one Ctrl-Z. +- `actions` — registers the capture/placement/slot action family, the Design View action family (toggle active mode, activate Arrange/Design, tag/untag selected tracks, show-both), the multi-bank action family (create/rename/reorder/delete bank, evacuate, activate, move/copy/remove selected samples), and the Phase R prune action (`BANK_PRUNE_FOLDER` — dry-run-first, confirm-with-manifest, then `pruneDeletePlan`-guarded deletion; **the ONLY file-deletion authority in the system**); routes each to the modules above via the `command_id`/`gaccel`/`hookcommand` contract. Every bank index verb wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is one Ctrl-Z. The prune action writes no ext state and opens no undo point (file deletion is not REAPER-undoable). ## REAPER extension contract (src/main.cpp) diff --git a/COMPLETED.md b/COMPLETED.md index 12bcfbb..1b8bb5a 100644 --- a/COMPLETED.md +++ b/COMPLETED.md @@ -1034,3 +1034,152 @@ LICE-drawn tabs + overflow, and both move affordances with drop-highlighting (5) Active-bank indicator placement (the one residual polish detail) was resolved at build time. B5 forks R-A and R-B settled 2026-07-24 (see B5 and B1 notes above). Both in `docs/product/removal-and-prune.md` §Fork R-A / §Fork R-B. + +--- + +# 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. +> +> **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 files present in the +bank folder, the files referenced by the book (unioned across all banks, pool +included), and the **owned-file manifest** (fork R-D, written from capture onward by +B-cap), compute the orphan set `(owned ∩ present) − referenced`. The mirror of +`ViewModeModel::reconcile(liveGuids)`, one level down (files instead of GUIDs). +CONTEXT.md §Prune (Module architecture — pure). +**Verify:** CTest green. **Prune null test:** a folder whose every file is +referenced deletes nothing; prune returns exactly `(owned ∩ present) − referenced` +and nothing else. Referenced-set unioned across every bank (a file referenced by any +bank — including via a copy — is never an orphan); a present-but-not-owned file (a +hand-dropped file) is never an orphan. + +- [x] Prune-reconcile pure function: `(present, referenced, owned) → orphans`, + computing `(owned ∩ present) − referenced`; referenced unioned across the whole + book (copies keep a file alive). +- [x] Tests: prune null test (all-referenced → empty); orphan = (owned∩present)− + referenced; a copied file referenced by a second bank survives; a present-but- + unowned (hand-dropped) file is never reclaimed; empty folder / empty book / empty + manifest edge cases. + +**Notes/decisions:** +- New pure module `src/prune_reconcile.{h,cpp}`: exports `pruneOrphans(present, + referenced, owned)` (the safety-critical set algebra), `buildPruneReport` + (count/bytes/display-capped list, unit-testable), and `pruneDeletePlan` (the R3 + confirm-time staleness intersection — `confirmed ∩ freshOrphans` in confirm order). + Exact-string path match throughout (no case-folding, no separator normalization). + `BankBook::referencedPaths()` additive const union query (all banks incl. pool, + de-duped) added to `bank_book`. New `prune_reconcile_tests` CTest target. + +--- + +## 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 the **owned-file manifest** (from B-cap) from the +session, feed the pure core, and produce a dry-run manifest. No deletion in this +wave — the report path only. CONTEXT.md §Prune (persist / prune shell). +**Verify (in DAW):** Dry-run reports the orphan count + reclaimed size (+ file list +for a small set) against the resolved current bank folder; resolves paths the same +way the index does (survives a Save-As relocation); deletes nothing. +**Depends on:** R1, B1, B2. + +- [x] Prune shell: enumerate the resolved current bank folder; feed the pure core. +- [x] Session supplies the referenced-set (union across the book) **and the + owned-file manifest** (written by B-cap); resolve the bank folder via the M4 + project-relative machinery. +- [x] Dry-run manifest: orphan count + reclaimed size (+ files for a small set); + **no deletion in this wave.** + +**Notes/decisions:** +- `ReaSamplerSession::pruneDryRun()` (read-only, non-throwing) enumerates the + resolved current bank folder, unioning `book().referencedPaths()` and + `owned().paths()`, feeds `pruneOrphans`, and calls `buildPruneReport` with a 64-file + display cap. Pure `bankRelativeForName` (`capture_paths`) normalizes the folder- + enumeration spelling to match the index convention so the pure core's exact-string + match lines up. Forever-stable `BANK_PRUNE_FOLDER` action registered (dry-run report + to console in R2; deletion wired in R3 behind the same action id). + +--- + +## R3 — deletion + action (the destructive step, guarded) +**Goal:** The confirmed deletion step, the bindable "Prune bank folder" action, and +a `bank_panel` prune button: dry-run-first, confirm-with-manifest, then reclaim the +orphan set — via OS trash where portably available (fork R-C), else unlink. +CONTEXT.md §Prune (guardrails, API). +**Verify (in DAW):** "Prune bank folder" (action or panel button) reports first, +deletes only on explicit confirm, and reclaims exactly the orphan set — never a +referenced file, never a hand-dropped non-bank file; the referenced/owned-set safety +holds; deletions route to OS trash where available; non-bank and capture invariants +untouched. +**Depends on:** R2 (and B-cap's owned-file manifest). All forks settled 2026-07-24. + +- [x] "Prune bank folder" action (`command_id`/`gaccel`/`hookcommand`), + dry-run-first, confirm-to-delete. +- [x] `bank_panel` prune button (fork R-E) that fires the "Prune bank folder" + action through the existing command-id contract — the panel affordance alongside + the bindable action; split: button hit-test/layout is pure (mirror of + `mode_switch`/`bank_grid`), draw + dispatch is bank_panel shell. +- [x] Deletion mechanism (fork R-C, settled trash-preferred): route to OS trash + where a portable move-to-trash is verified available, else unlink behind the + dry-run/confirm guardrail. +- [x] Orphan attribution (fork R-D, settled owned-file manifest): reclaim only + `(owned ∩ present) − referenced` — the bank system's own leavings, never a + hand-dropped folder file. (Manifest written by B-cap; consumed via R1/R2.) + +**Notes/decisions:** +- Deletion is guarded: dry-run → REAPER `ShowMessageBox` confirm (count+bytes+files) + → `pruneDeletePlan` staleness intersection (confirmed ∩ fresh pure-core output) → + delete exactly the plan. Zero ext-state writes, no undo point (file deletion is not + REAPER-undoable by design). +- **Windows:** routes to Recycle Bin via `SHFileOperationW` + `FOF_ALLOWUNDO` + (verified against SDK 10.0.26100). **macOS / Linux:** no portable SWELL trash + surface; falls back to `unlink` behind the dry-run/confirm guardrail. +- Manifest entries are deliberately NOT removed on deletion (the owned-file manifest + algebra self-cleans: a deleted file will drop from `present` on the next prune scan, + and `pruneOrphans` returns `(owned ∩ present) − referenced` — the absent file + contributes nothing regardless). +- New pure module `src/prune_button.{h,cpp}`: layout (`computePruneButton`) and + hit-test (`hitTestPruneButton`) for the footer prune button, right-anchored, + suppressed gracefully when the footer is too narrow. Mirror of `mode_switch` / + `tab_strip`. New `prune_button_tests` CTest target. +- `bank_panel` footer button dispatches `BANK_PRUNE_FOLDER` via `Main_OnCommand` + through the registered command id (the same action as the bindable menu entry — + no duplicate logic). + +## Phase R forks — settled 2026-07-24 +- **Fork R-C — deletion mechanism.** Settled: **trash-preferred, unlink fallback.** + Route to OS trash where a portable move-to-trash is available (recoverable), else + unlink behind strong dry-run/confirm. Per-platform trash surface verified at build: + Windows `SHFileOperationW` + `FOF_ALLOWUNDO` (SDK 10.0.26100); macOS/Linux no + portable SWELL trash surface → unlink fallback. Folded into R3. +- **Fork R-D — orphan attribution.** Settled: **owned-file manifest**, + `(owned ∩ present) − referenced`; folder-sweep rejected as unsafe. **Seam lands + early** — the manifest is written from capture onward (new **B-cap** point in + Phase B), not reconstructed at prune time; R1/R2 consume it. Persistence shape + (sibling `"reasampler"` key vs. `banks` blob) resolved at build time: sibling + `"owned_files"` key. +- **Fork R-E — trigger.** Settled: **manual action + `bank_panel` button**, + dry-run-first, confirm-to-delete. No background sweep. The earlier optional + delete-time "…and prune now" convenience was **not** selected — out of scope. + Folded into R3. + +Both docs of record: `docs/product/removal-and-prune.md` §Fork R-C/R-D/R-E and +CONTEXT.md §Prune (Settled forks). diff --git a/CONTEXT.md b/CONTEXT.md index 16a4690..ecd3d45 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -958,7 +958,9 @@ pure/shell split as `bank_model` / `view_mode_model` / `bank_book`. 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). + per-platform **to-verify** (see REAPER/platform API surface). (R3 verified: Windows + `SHFileOperationW` + `FOF_ALLOWUNDO` confirmed against SDK 10.0.26100; macOS/Linux + unlink fallback — no portable SWELL trash 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 @@ -1000,6 +1002,9 @@ No new REAPER *audio* API. New surfaces to verify before use: `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. + (R3 verified: Windows routes to Recycle Bin via `SHFileOperationW` + `FOF_ALLOWUNDO`, + verified against SDK 10.0.26100. macOS / Linux: no portable SWELL trash surface + found — fall back to `unlink` behind the dry-run/confirm guardrail, as specified.) - **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 diff --git a/PLAN.md b/PLAN.md index e402d67..a16f1dc 100644 --- a/PLAN.md +++ b/PLAN.md @@ -88,114 +88,6 @@ landed milestone. --- -# 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 files present in the -bank folder, the files referenced by the book (unioned across all banks, pool -included), and the **owned-file manifest** (fork R-D, written from capture onward by -B-cap), compute the orphan set `(owned ∩ present) − referenced`. The mirror of -`ViewModeModel::reconcile(liveGuids)`, one level down (files instead of GUIDs). -CONTEXT.md §Prune (Module architecture — pure). -**Verify:** CTest green. **Prune null test:** a folder whose every file is -referenced deletes nothing; prune returns exactly `(owned ∩ present) − referenced` -and nothing else. Referenced-set unioned across every bank (a file referenced by any -bank — including via a copy — is never an orphan); a present-but-not-owned file (a -hand-dropped file) is never an orphan. - -- [ ] Prune-reconcile pure function: `(present, referenced, owned) → orphans`, - computing `(owned ∩ present) − referenced`; referenced unioned across the whole - book (copies keep a file alive). -- [ ] Tests: prune null test (all-referenced → empty); orphan = (owned∩present)− - referenced; a copied file referenced by a second bank survives; a present-but- - unowned (hand-dropped) file is never reclaimed; empty folder / empty book / empty - manifest edge cases. - -## R2 — prune shell + persist wiring (filesystem I/O, thin) -**Goal:** Enumerate the current project bank folder (M4 project-relative resolution), -supply the referenced-set and the **owned-file manifest** (from B-cap) from the -session, feed the pure core, and produce a dry-run manifest. No deletion in this -wave — the report path only. CONTEXT.md §Prune (persist / prune shell). -**Verify (in DAW):** Dry-run reports the orphan count + reclaimed size (+ file list -for a small set) against the resolved current bank folder; resolves paths the same -way the index does (survives a Save-As relocation); deletes nothing. -**Depends on:** R1, B1, B2. - -- [ ] Prune shell: enumerate the resolved current bank folder; feed the pure core. -- [ ] Session supplies the referenced-set (union across the book) **and the - owned-file manifest** (written by B-cap); resolve the bank folder via the M4 - project-relative machinery. -- [ ] Dry-run manifest: orphan count + reclaimed size (+ files for a small set); - **no deletion in this wave.** - -## R3 — deletion + action (the destructive step, guarded) -**Goal:** The confirmed deletion step, the bindable "Prune bank folder" action, and -a `bank_panel` prune button: dry-run-first, confirm-with-manifest, then reclaim the -orphan set — via OS trash where portably available (fork R-C), else unlink. -CONTEXT.md §Prune (guardrails, API). -**Verify (in DAW):** "Prune bank folder" (action or panel button) reports first, -deletes only on explicit confirm, and reclaims exactly the orphan set — never a -referenced file, never a hand-dropped non-bank file; the referenced/owned-set safety -holds; deletions route to OS trash where available; non-bank and capture invariants -untouched. -**Depends on:** R2 (and B-cap's owned-file manifest). All forks settled 2026-07-24. - -- [ ] "Prune bank folder" action (`command_id`/`gaccel`/`hookcommand`), - dry-run-first, confirm-to-delete. -- [ ] `bank_panel` prune button (fork R-E) that fires the "Prune bank folder" - action through the existing command-id contract — the panel affordance alongside - the bindable action; split: button hit-test/layout is pure (mirror of - `mode_switch`/`bank_grid`), draw + dispatch is bank_panel shell. -- [ ] Deletion mechanism (fork R-C, settled trash-preferred): route to OS trash - where a portable move-to-trash is verified available, else unlink behind the - dry-run/confirm guardrail. **Verify the platform move-to-trash surface before use - (per platform).** -- [ ] Orphan attribution (fork R-D, settled owned-file manifest): reclaim only - `(owned ∩ present) − referenced` — the bank system's own leavings, never a - hand-dropped folder file. (Manifest written by B-cap; consumed via R1/R2.) - -## Phase R forks — settled 2026-07-24 -- **Fork R-C — deletion mechanism.** Settled: **trash-preferred, unlink fallback.** - Route to OS trash where a portable move-to-trash is available (recoverable), else - unlink behind strong dry-run/confirm. Per-platform trash surface (SWELL / Win - `SHFileOperation`·`IFileOperation` / macOS `trashItemAtURL:` / Linux XDG) is a - **must-verify before use**. Folded into R3. -- **Fork R-D — orphan attribution.** Settled: **owned-file manifest**, - `(owned ∩ present) − referenced`; folder-sweep rejected as unsafe. **Seam lands - early** — the manifest is written from capture onward (new **B-cap** point in - Phase B), not reconstructed at prune time; R1/R2 consume it. Persistence shape - (sibling `"reasampler"` key vs. `banks` blob) is a small build-time residual. -- **Fork R-E — trigger.** Settled: **manual action + `bank_panel` button**, - dry-run-first, confirm-to-delete. No background sweep. The earlier optional - delete-time "…and prune now" convenience was **not** selected — out of scope. - Folded into R3. - -Both docs of record: `docs/product/removal-and-prune.md` §Fork R-C/R-D/R-E and -CONTEXT.md §Prune (Settled forks). - ---- - # Phase S — MIDI-playback instrument (native VST3 sampler; a second build artifact) > **New pillar, own lettered namespace, and — uniquely — a second build artifact.**