docs: archive Phase B (B1-B5, B-cap, R-B) to COMPLETED; reconcile CLAUDE.md

This commit is contained in:
2026-07-26 15:51:13 -04:00
parent 5a3e6df4c3
commit c66f44ba1f
3 changed files with 234 additions and 203 deletions
+5 -3
View File
@@ -34,6 +34,7 @@ Key targets (see CMakeLists.txt for the full list):
| `mode_switch_tests` | executable | Pure unit tests for `mode_switch` — no REAPER, no DAW. |
| `bank_book_tests` | executable | Pure unit tests for `bank_book` — no REAPER, no DAW. |
| `wav_trim_tests` | executable | Pure unit tests for `wav_trim` — no REAPER, no DAW. |
| `owned_manifest_tests` | executable | Pure unit tests for `owned_manifest` — no REAPER, no DAW. |
| `reaper_reasampler` | loadable module | The actual extension binary (`.dll` / `.dylib` / `.so`). |
### macOS / Linux: SWELL dialog resources
@@ -56,17 +57,18 @@ There is no hot-reload. Copy the built binary into REAPER's `UserPlugins/` folde
- `view_mode_model` — Design View mode system: mode registry, GUID-keyed membership, folder-tree-aware visibility derivation, snapshot-based park/restore planner, JSON round-trip. Mirror of `bank_model` for the Design View phase.
- `view_tree` — pure `I_FOLDERDEPTH`→FolderTree helper for the Design View shell; no REAPER types at the boundary.
- `mode_switch` — REAPER-free segment layout + hit-test math for the bank_panel's Design View mode switch; divides a header rectangle into N equal segments and hit-tests a point to a segment. Mirror of `bank_grid`.
- `bank_book` — multi-bank registry (Phase B): an ordered set of banks (pool seeded as bank-zero + named banks), each wrapping a `BankIndex`. Owns create/rename/reorder/delete of named banks, pool privileges (un-deletable/un-renamable/un-evacuable, never zero banks) enforced in-model, active-bank id, index-only move/copy of a sample between banks, JSON round-trip + legacy-`bank_index`→pool migration. Wraps `BankIndex` (bank_model untouched; no `bankId` on `Sample`).
- `bank_book` — multi-bank registry (Phase B): an ordered set of banks (pool seeded as bank-zero + named banks), each wrapping a `BankIndex`. Owns create/rename/reorder/delete of named banks, pool privileges (un-deletable/un-renamable/un-evacuable, never zero banks) enforced in-model, active-bank id, index-only move/copy/remove of a sample between or from banks, `hashReferencedElsewhere` cross-bank reference query, JSON round-trip + legacy-`bank_index`→pool migration. Wraps `BankIndex` (bank_model untouched; no `bankId` on `Sample`).
- `owned_manifest` — owned-file manifest seam (Phase B B-cap): the set of project-relative files the capture path itself created, persisted under the `"owned_files"` ext-state key, so Phase R prune can distinguish the bank system's own orphans from hand-dropped files. Deliberately decoupled from `bank_book` — tracks files created, not index membership. Phase R (R1/R2) consumes it; no prune logic here.
- `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.
**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"`) ↔ `bank_model` JSON (`"bank_index"` key) + `ViewModeModel` JSON (`"view_state"` key) + `TailSetting` JSON (`"tail_setting"` key); project-relative path resolution.
- `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); 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.
- `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.
- `actions` — registers the capture/placement/slot action family and the Design View action family (toggle active mode, activate Arrange/Design, tag/untag selected tracks, show-both); routes each to the modules above via the `command_id`/`gaccel`/`hookcommand` contract.
- `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.
## REAPER extension contract (src/main.cpp)
+229
View File
@@ -654,3 +654,232 @@ items exempt; one undo block per action. ctest 14/14 green.
lane-split marker has no natural cheap home in the bank panel; the mode switch
already shows the active mode. Preserved as a deferred/backlog note in PLAN.md
Phase D2 — not silently dropped.
---
# Phase B — Multi-bank (parallel to the M0M11 capture roadmap and Phase D)
> **Separate phase namespace.** The M-numbers belong to the capture pillar
> (M0M11); the D-letters belong to Design View. Multi-bank is a third orthogonal
> pillar — generalizing the single bank into a pool + named banks — so it takes its
> own **lettered** namespace (B1, B2, …). "B" reads for **Banks** and, like Phase D,
> keeps the roadmaps from colliding on numbering: Phase B is not "the twelfth
> capture step," it is a different pillar. Authoritative spec: **CONTEXT.md
> §Multi-bank**. Product framing: `docs/product/multi-bank.md`.
## B1 — bank_book (pure)
**Goal:** REAPER-free bank registry wrapping N `BankIndex` instances: pool seeded +
privileged, create/rename/reorder/delete named banks, active-bank id, move/copy a
sample between banks, JSON round-trip + legacy-migration. The heart of the phase;
mirror of `bank_model` / `view_mode_model`; **`BankIndex` untouched (additive)**.
CONTEXT.md §Multi-bank (Module architecture — pure).
**Verify:** CTest green. Pool always present, un-deletable, un-renamable,
un-evacuable (rules rejected in-model). Active-bank defaults to pool. Move is
index-only (source loses entry, destination gains it) and observes destination
collapse-by-hash; copy leaves source intact. Delete drops member index entries.
Evacuate moves all members to the pool, leaving the bank empty. JSON round-trip
lossless across pool-as-bank-zero + named banks + per-bank indices + ordinals +
active id. Legacy `bank_index` JSON parses into `{ pool }` with zero named banks.
- [x] Bank registry: ordered `{ bank id, display name, ordinal, BankIndex }`; pool
seeded with fixed id + fixed name; create / rename / reorder / delete named banks
(delete drops the bank's member index entries).
- [x] Pool-privilege rules enforced in-model: reject delete-pool, reject
rename-pool, reject evacuate-pool, never allow zero banks.
- [x] Active-bank id (get/set; defaults to pool); resolve active bank's `BankIndex`.
- [x] Move sample between banks (index-only; destination collapse-by-hash observed;
source entry removed).
- [x] Copy sample between banks (index-only; source entry retained; destination
collapse-by-hash observed).
- [x] Evacuate bank: move every member to the pool (index-only; destination
collapse-by-hash observed), leaving the bank empty; pool cannot be evacuated.
- [x] JSON round-trip: pool-as-bank-zero inside the blob + named banks + per-bank
indices + ordinals + active id.
- [x] Legacy migration: a bare `bank_index` JSON promotes to the pool's index with
zero named banks (one-way, lossless; blob authoritative thereafter).
- [x] Tests: pool privileges (delete/rename/evacuate rejected); move
source-loses/dest-gains; copy source-retained; evacuate empties source into pool
with dest collapse; cross-bank same-hash coexistence; dest collapse on move into a
bank already holding the hash; JSON lossless; legacy migration.
> **Phase-B-wide undo (fork R-B, settled 2026-07-24 — batched REAPER undo points).**
> Every index verb across B1B5 (create/rename/reorder/delete-bank, move, copy,
> evacuate, remove) wraps its bank/index mutation in a **batched REAPER undo point**
> (`Undo_BeginBlock` / `Undo_EndBlock`), so one bank operation is one Ctrl-Z. This is
> a cross-cutting decision that retro-touches B1B4, not a B5-local one; the
> per-verb points above inherit it. **Must-verify before build:** confirm against
> `vendor/reaper-sdk` that `"reasampler"` ext-state mutations participate correctly
> in `Undo_BeginBlock`/`Undo_EndBlock` undo blocks — the whole approach depends on
> it. See CONTEXT.md §Sample removal (Guardrails) + product notes §Fork R-B.
**Notes/decisions (R-B — Phase-B-wide undo, landed):**
- Every bank index verb — bindable action AND panel gesture (menu/drag/Delete key) —
is one batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`,
`UNDO_STATE_MISCCFG`); ext-state participates in undo via `UNDO_STATE_MISCCFG`
("extensions!"), SDK-verified. A `projectconfig` hook
(`BeginLoadProjectState(isUndo)`) triggers a deferred session reload so Ctrl-Z/redo
visibly restores book/view/tail/manifest in-session. Rejected/no-op ops open no
undo point; unsaved-project ops discard the empty block.
---
## B-cap — owned-file manifest seam (capture writes; prune consumes in Phase R)
**Goal:** Capture writes each file it creates into an **owned-file manifest**
persisted in the `"reasampler"` ext-state, so Phase R prune can later distinguish
the bank system's own orphans from hand-dropped files. Consumed only in Phase R
(R1/R2) — landed early here because reconstructing the manifest retroactively is a
backfill cliff (fork R-D, settled 2026-07-24: *defer the feature, design the seam*).
CONTEXT.md §Prune (Settled decisions — orphan attribution) + product notes §Fork R-D.
**Verify:** every file the capture path creates is recorded in the owned-file
manifest; the manifest round-trips through the `"reasampler"` ext-state (Save / Save
As / reopen); relative-paths-only preserved. Prune's consumption of it is Phase R.
**Depends on:** the capture add-path (M7) + persist blob machinery (M4 / B2).
- [x] Capture records each created file into an owned-file manifest (the set of
files the book has created), persisted in the `"reasampler"` ext-state (sibling
`owned_files` key — persistence shape resolved at build time: sibling key, not
folded into the `banks` blob).
- [x] Manifest round-trips: survives Save / Save As / reopen via the M4 blob
machinery; relative-paths-only. (Consumed by Phase R R1/R2 — not consumed here.)
**Notes/decisions:**
- Pure `owned_manifest` module (`src/owned_manifest.{h,cpp}`): relative paths, dedup,
JSON round-trip. Deliberately decoupled from `bank_book` — tracks files created, not
index membership; sample-remove is not manifest-remove. Unit-tested via new
`owned_manifest_tests` CTest target. Persisted under the `"owned_files"` ext-state
key. Both capture commit paths (offline + realtime) record created files. Joins the
undo-reload set.
---
## B2 — persist slice (banks ↔ project ext state)
**Goal:** Serialize the book under the `banks` key in `"reasampler"` alongside the
existing sections, with the pool folded in as bank-zero; migrate a legacy
`bank_index` key into the pool on first load and retire the legacy key; reload-on-open
and Save-As survival via the existing M4 machinery. CONTEXT.md §Multi-bank (persist).
**Verify (in DAW):** Banks + named banks + active bank + all per-bank samples survive
Save / Save As / close+reopen; **relative paths only**; bank travels with the `.rpp`;
a project saved before this phase (legacy `bank_index` only) loads as pool + zero
named banks with no sample loss, and after save carries `banks` with no `bank_index`
written.
**Depends on:** B1. (Persistence-key fork settled — fork 1 (a): pool inside the
`banks` blob, legacy key retired after one-way migration.)
- [x] Serialize/deserialize the book under the `banks` key (pool-as-bank-zero inside
the blob; distinct section from `view_state`; no `bank_index` key written going
forward).
- [x] Legacy-migration path on load: absent `banks` + present `bank_index` → promote
into pool, mint the blob, treat blob as authoritative (legacy key retired).
- [x] Session exposes the book; the active bank's `BankIndex` is the capture add
target (route the M7 capture family through it — additive to M7, no M7 rewrite).
- [x] Confirm survival across Save / Save As; confirm legacy-project load path.
---
## B3 — actions
**Goal:** Bindable action set for the multi-bank workflow. CONTEXT.md §Multi-bank
(actions). **Verify (in DAW):** Each action registered (bindable in Actions list);
bank-activate + move/copy + evacuate MIDI-bindable; create/rename/delete/evacuate
drive the B1 model via the B2-persisted session.
**Depends on:** B1, B2.
- [x] Create bank / rename bank / delete bank (delete drops member index entries;
confirm-on-non-empty offered at the UI layer in B4).
- [x] Evacuate bank → pool (move all members back to the pool; refuses on the pool).
- [x] Activate bank (direct-by-id + cycle).
- [x] Move selected samples → bank / copy selected samples → bank (move is default).
- [x] Pool full-height / banks full-height toggles.
- [x] Register each (`command_id`/`gaccel`/`hookcommand`); bank-activate + move/copy
+ evacuate MIDI-bindable.
---
## B4 — bank_panel vertical split (UI)
**Goal:** The vertical-split bank window — pool on top, named-banks tab-page region
below, full-height toggles — extending the M5 docked grid. CONTEXT.md §Multi-bank
(bank_panel). **Verify (in DAW):** Pool grid renders on top; named-banks tab strip
below (empty when no named banks, one tab per named bank); active-bank **unmistakably**
indicated; both full-height toggles collapse the split correctly; sample move/copy
affordance works; non-empty delete confirms and offers evacuate; the Design View mode
switch in the header is unaffected.
**Depends on:** B1, B2, B3. (Tab rendering + move-affordance mechanics — fork 5 —
settled 2026-07-23: LICE-drawn tabs + both move affordances; see Phase B open questions
and product notes → *Fork 5 — settled*.)
- [x] Vertical split: pool grid region (top) + named-banks tab-page region (bottom).
- [x] Named-banks tab strip: **LICE-drawn** (matching the M5 grid + Design View
segmented switch, not SWELL-native — fork 5a); one tab per named bank; empty state
when none.
- [x] Tab-strip overflow/scroll affordance (fork 5a): scroll/chevron overflow shipped
with the strip.
- [x] Pool full-height / banks full-height toggle affordances wired to B3.
- [x] Active-bank indicator — **visually unmistakable** (settled constraint).
- [x] Create / rename / delete / activate / evacuate affordances driving B3 actions.
- [x] Delete confirms on a non-empty bank, naming the evacuate alternative.
- [x] Sample move affordance — **both** (fork 5b): a "move to bank" menu on the current
selection (bindable front-end for the B3 move action) **and** drag-between-regions.
Copy is the deliberate secondary act, offered on the menu.
- [x] Drag mis-drop mitigation (fork 5b): clear drop-target highlighting on the
destination region/tab during a drag.
**Notes/decisions:**
- New pure module `src/tab_strip.{h,cpp}`: named-banks tab-strip geometry (B4) —
strip rect + N tabs + scroll offset → per-tab rects (overflow-clipped), overflow
chevron reservation + maxScroll, and point → tab/chevron hit-test. Unit-tested via
`tab_strip_tests` CTest target. Mirror of `mode_switch`.
- Active-bank indicator placement (the open polish detail from the Phase B open
questions) was resolved at build time in the panel implementation.
- Post-landing: m11's console-chatter policy applied to Phase B messages (successes
silent, failures kept).
---
## 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).
- [x] Surface `BankIndex::remove` through `bank_book`: remove a `Sample` from a
bank's index; pool contents removable, pool-container privileges unchanged.
- [x] "Remove selected sample(s)" action (`command_id`/`gaccel`/`hookcommand`),
MIDI-bindable; carries a `scope: this-bank | all-banks` seam (fork R-A, settled
2026-07-24: **this-bank** is the default and only surfaced affordance; all-banks
stays a latent seam-only parameter, not shipped).
- [x] `bank_panel` remove affordance on the current selection (reuse M5 selection
model, as move/copy do).
- [x] 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.
- [x] 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).
**Notes/decisions (B5 forks — settled 2026-07-24):**
- **R-A — remove scope.** Settled: **this-bank**. Removes the entry from the bank in
view only; the `scope: this-bank | all-banks` seam stays in the action signature
but all-banks is a latent parameter, not a surfaced verb.
- `hashReferencedElsewhere` cross-bank reference query added to `bank_book` to
support the confirm-on-last-reference guardrail without coupling to the panel.
- Both `bank_panel` context menu ("Remove selected sample(s)") and Delete key
affordance wired; panel gesture is also one batched undo point (R-B applies).
---
## Phase B open questions — all resolved
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
default gesture (3); active-bank/shown-tab distinct with an unmistakable indicator (4);
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.
-200
View File
@@ -130,206 +130,6 @@ landed milestone.
---
# Phase B — Multi-bank (parallel to the M0M11 capture roadmap and Phase D)
> **Separate phase namespace.** The M-numbers belong to the capture pillar
> (M0M11); the D-letters belong to Design View. Multi-bank is a third orthogonal
> pillar — generalizing the single bank into a pool + named banks — so it takes its
> own **lettered** namespace (B1, B2, …). "B" reads for **Banks** and, like Phase D,
> keeps the roadmaps from colliding on numbering: Phase B is not "the twelfth
> capture step," it is a different pillar. Authoritative spec: **CONTEXT.md
> §Multi-bank**. Product framing: `docs/product/multi-bank.md`. When a point lands,
> doc-keeper moves it to `COMPLETED.md`.
## B1 — bank_book (pure)
**Goal:** REAPER-free bank registry wrapping N `BankIndex` instances: pool seeded +
privileged, create/rename/reorder/delete named banks, active-bank id, move/copy a
sample between banks, JSON round-trip + legacy-migration. The heart of the phase;
mirror of `bank_model` / `view_mode_model`; **`BankIndex` untouched (additive)**.
CONTEXT.md §Multi-bank (Module architecture — pure).
**Verify:** CTest green. Pool always present, un-deletable, un-renamable,
un-evacuable (rules rejected in-model). Active-bank defaults to pool. Move is
index-only (source loses entry, destination gains it) and observes destination
collapse-by-hash; copy leaves source intact. Delete drops member index entries.
Evacuate moves all members to the pool, leaving the bank empty. JSON round-trip
lossless across pool-as-bank-zero + named banks + per-bank indices + ordinals +
active id. Legacy `bank_index` JSON parses into `{ pool }` with zero named banks.
- [ ] Bank registry: ordered `{ bank id, display name, ordinal, BankIndex }`; pool
seeded with fixed id + fixed name; create / rename / reorder / delete named banks
(delete drops the bank's member index entries).
- [ ] Pool-privilege rules enforced in-model: reject delete-pool, reject
rename-pool, reject evacuate-pool, never allow zero banks.
- [ ] Active-bank id (get/set; defaults to pool); resolve active bank's `BankIndex`.
- [ ] Move sample between banks (index-only; destination collapse-by-hash observed;
source entry removed).
- [ ] Copy sample between banks (index-only; source entry retained; destination
collapse-by-hash observed).
- [ ] Evacuate bank: move every member to the pool (index-only; destination
collapse-by-hash observed), leaving the bank empty; pool cannot be evacuated.
- [ ] JSON round-trip: pool-as-bank-zero inside the blob + named banks + per-bank
indices + ordinals + active id.
- [ ] Legacy migration: a bare `bank_index` JSON promotes to the pool's index with
zero named banks (one-way, lossless; blob authoritative thereafter).
- [ ] Tests: pool privileges (delete/rename/evacuate rejected); move
source-loses/dest-gains; copy source-retained; evacuate empties source into pool
with dest collapse; cross-bank same-hash coexistence; dest collapse on move into a
bank already holding the hash; JSON lossless; legacy migration.
> **Phase-B-wide undo (fork R-B, settled 2026-07-24 — batched REAPER undo points).**
> Every index verb across B1B5 (create/rename/reorder/delete-bank, move, copy,
> evacuate, remove) wraps its bank/index mutation in a **batched REAPER undo point**
> (`Undo_BeginBlock` / `Undo_EndBlock`), so one bank operation is one Ctrl-Z. This is
> a cross-cutting decision that retro-touches B1B4, not a B5-local one; the
> per-verb points above inherit it. **Must-verify before build:** confirm against
> `vendor/reaper-sdk` that `"reasampler"` ext-state mutations participate correctly
> in `Undo_BeginBlock`/`Undo_EndBlock` undo blocks — the whole approach depends on
> it. See CONTEXT.md §Sample removal (Guardrails) + product notes §Fork R-B.
## B-cap — owned-file manifest seam (capture writes; prune consumes in Phase R)
**Goal:** Capture writes each file it creates into an **owned-file manifest**
persisted in the `"reasampler"` ext-state, so Phase R prune can later distinguish
the bank system's own orphans from hand-dropped files. Consumed only in Phase R
(R1/R2) — landed early here because reconstructing the manifest retroactively is a
backfill cliff (fork R-D, settled 2026-07-24: *defer the feature, design the seam*).
CONTEXT.md §Prune (Settled decisions — orphan attribution) + product notes §Fork R-D.
**Verify:** every file the capture path creates is recorded in the owned-file
manifest; the manifest round-trips through the `"reasampler"` ext-state (Save / Save
As / reopen); relative-paths-only preserved. Prune's consumption of it is Phase R.
**Depends on:** the capture add-path (M7) + persist blob machinery (M4 / B2).
- [ ] Capture records each created file into an owned-file manifest (the set of
files the book has created), persisted in the `"reasampler"` ext-state (sibling
key or folded into the `banks` blob — persistence shape is a small build-time
residual, not a fork).
- [ ] Manifest round-trips: survives Save / Save As / reopen via the M4 blob
machinery; relative-paths-only. (Consumed by Phase R R1/R2 — not consumed here.)
## B2 — persist slice (banks ↔ project ext state)
**Goal:** Serialize the book under the `banks` key in `"reasampler"` alongside the
existing sections, with the pool folded in as bank-zero; migrate a legacy
`bank_index` key into the pool on first load and retire the legacy key; reload-on-open
and Save-As survival via the existing M4 machinery. CONTEXT.md §Multi-bank (persist).
**Verify (in DAW):** Banks + named banks + active bank + all per-bank samples survive
Save / Save As / close+reopen; **relative paths only**; bank travels with the `.rpp`;
a project saved before this phase (legacy `bank_index` only) loads as pool + zero
named banks with no sample loss, and after save carries `banks` with no `bank_index`
written.
**Depends on:** B1. (Persistence-key fork settled — fork 1 (a): pool inside the
`banks` blob, legacy key retired after one-way migration.)
- [ ] Serialize/deserialize the book under the `banks` key (pool-as-bank-zero inside
the blob; distinct section from `view_state`; no `bank_index` key written going
forward).
- [ ] Legacy-migration path on load: absent `banks` + present `bank_index` → promote
into pool, mint the blob, treat blob as authoritative (legacy key retired).
- [ ] Session exposes the book; the active bank's `BankIndex` is the capture add
target (route the M7 capture family through it — additive to M7, no M7 rewrite).
- [ ] Confirm survival across Save / Save As; confirm legacy-project load path.
## B3 — actions
**Goal:** Bindable action set for the multi-bank workflow. CONTEXT.md §Multi-bank
(actions). **Verify (in DAW):** Each action registered (bindable in Actions list);
bank-activate + move/copy + evacuate MIDI-bindable; create/rename/delete/evacuate
drive the B1 model via the B2-persisted session.
**Depends on:** B1, B2.
- [ ] Create bank / rename bank / delete bank (delete drops member index entries;
confirm-on-non-empty offered at the UI layer in B4).
- [ ] Evacuate bank → pool (move all members back to the pool; refuses on the pool).
- [ ] Activate bank (direct-by-id + cycle).
- [ ] Move selected samples → bank / copy selected samples → bank (move is default).
- [ ] Pool full-height / banks full-height toggles.
- [ ] Register each (`command_id`/`gaccel`/`hookcommand`); bank-activate + move/copy
+ evacuate MIDI-bindable.
## B4 — bank_panel vertical split (UI)
**Goal:** The vertical-split bank window — pool on top, named-banks tab-page region
below, full-height toggles — extending the M5 docked grid. CONTEXT.md §Multi-bank
(bank_panel). **Verify (in DAW):** Pool grid renders on top; named-banks tab strip
below (empty when no named banks, one tab per named bank); active-bank **unmistakably**
indicated; both full-height toggles collapse the split correctly; sample move/copy
affordance works; non-empty delete confirms and offers evacuate; the Design View mode
switch in the header is unaffected.
**Depends on:** B1, B2, B3. (Tab rendering + move-affordance mechanics — fork 5 —
settled 2026-07-23: LICE-drawn tabs + both move affordances; see Phase B open questions
and product notes → *Fork 5 — settled*.)
- [ ] Vertical split: pool grid region (top) + named-banks tab-page region (bottom).
- [ ] Named-banks tab strip: **LICE-drawn** (matching the M5 grid + Design View
segmented switch, not SWELL-native — fork 5a); one tab per named bank; empty state
when none. **Verify LICE tab draw against the M5 reference before use.**
- [ ] Tab-strip overflow/scroll affordance — **in scope from the start** (fork 5a): a
naive fixed-width LICE strip breaks down at ~812 tabs, so ship scroll/chevron
overflow with the strip, do not defer it.
- [ ] Pool full-height / banks full-height toggle affordances wired to B3.
- [ ] Active-bank indicator — **visually unmistakable** (settled constraint);
placement (per-region header / single readout / lit-tab) is the residual polish
detail.
- [ ] Create / rename / delete / activate / evacuate affordances driving B3 actions.
- [ ] Delete confirms on a non-empty bank, naming the evacuate alternative.
- [ ] Sample move affordance — **both** (fork 5b): a "move to bank" menu on the current
selection (bindable front-end for the B3 move action) **and** drag-between-regions.
Copy is the deliberate secondary act, offered on the menu.
- [ ] Drag mis-drop mitigation (fork 5b): clear drop-target highlighting on the
destination region/tab during a drag; a mis-drop is recoverable by design (move is
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, settled
2026-07-24: **this-bank** is the default and only surfaced affordance; all-banks
stays a latent seam-only parameter, not shipped).
- [ ] `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
default gesture (3); active-bank/shown-tab distinct with an unmistakable indicator (4);
LICE-drawn tabs + overflow, and both move affordances with drop-highlighting (5).
Folded into CONTEXT.md §Multi-bank + the B1B4 points above. Phase B is fully settled and
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 — settled 2026-07-24:**
- **R-A — remove scope.** Settled: **this-bank**. Removes the entry from the bank in
view only; the `scope: this-bank | all-banks` seam stays in the action signature
but all-banks is a latent parameter, not a surfaced verb. Folded into the B5 action
point above.
- **R-B — undo model (Phase-B-wide).** Settled: **batched REAPER undo points**
(`Undo_BeginBlock`/`Undo_EndBlock`), one bank op = one Ctrl-Z. Applies across
B1B5 (retro-touches B1B4) — captured as the cross-cutting note under B1 above,
with the ext-state-participation SDK check as a must-verify-before-build.
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