diff --git a/CONTEXT.md b/CONTEXT.md index 5ebb370..91a0892 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -369,7 +369,7 @@ arrange; the only change is *which* index the entry lands in. active bank's `BankIndex`, then adds exactly as today. No capture backend changes; only the add-target is selected upstream. Activating a bank is a model mutation + a persist write; it never touches the timeline. -- **Movement moves the index entry, not the file (default).** Moving a sample from +- **Movement moves the index entry, not the file.** Moving a sample from bank A to bank B is an **index-only** operation: remove the `Sample` from A's `BankIndex`, add it to B's. The underlying file stays in the project bank folder — banks are logical groupings over one shared file pool, not separate folders on @@ -383,18 +383,36 @@ arrange; the only change is *which* index the entry lands in. **not** enforced: the same hash may exist in the pool and in a named bank simultaneously (that is the point — copy lets a sample be grouped into "Drums" while still living in the pool). -- **Move vs. copy are distinct acts.** *Move* removes from source, adds to - destination (one logical sample, regrouped). *Copy* adds to destination and leaves - the source entry intact (same file, two index entries, two banks). Both are - index-only; both share the destination-collapse rule. Copy is what lets a sample - live in the pool *and* a named group at once. -- **Persistence: a new ext-state key, pool migrates in place.** The multi-bank - state serializes to a new key `banks` in the existing `"reasampler"` namespace, - alongside `bank_index`, `view_state`, and `project_guid`. **Migration:** on load, - if a `banks` key is absent but a legacy `bank_index` key is present, the legacy - index becomes the pool's `BankIndex` and the book is `{ pool }` with zero named - banks — a one-way, lossless promotion. (Whether `bank_index` is retired or kept as - the pool's canonical slot is an open fork — see below.) +- **Move vs. copy are distinct acts; move is the default.** *Move* removes from + source, adds to destination (one logical sample, regrouped) — it is the **primary, + low-friction gesture**, so a sample lives in exactly one bank at a time. *Copy* adds + to destination and leaves the source entry intact (same file, two index entries, two + banks) — the **deliberate secondary act** for the "in two places at once" case. Both + are index-only; both share the destination-collapse rule. Under move-as-default the + pool is the default home and staging ground, not a permanent superset: moving a + sample into a named bank takes it out of the pool. (See product notes for the + mental-model reconciliation.) +- **Delete drops members; evacuate returns them.** Deleting a **named** bank drops + its member index entries (files are **not** deleted — file lifecycle stays owned by + the capture/prune path). A separate **evacuate** operation moves all of a bank's + members back to the pool (index-only, same destination-collapse-by-hash as move), + leaving the bank empty. Intended workflow: *evacuate then delete* to keep the + samples, *plain delete* to drop the grouping and its members. A plain delete of a + non-empty bank orphans those members out of every index — their files persist on + disk until prune, referenced by no bank — so the UI **confirms on non-empty delete** + and offers evacuate as the alternative. Evacuate cannot be applied to the pool. +- **Persistence: a new ext-state key; the pool folds in and the legacy key is + retired.** The multi-bank state serializes to a new key `banks` in the existing + `"reasampler"` namespace, alongside `view_state` and `project_guid`. The pool's + index rides *inside* the `banks` blob as bank-zero — persisted identically to any + named bank (one blob, one section, one JSON shape). **Migration:** on load, if a + `banks` key is absent but a legacy `bank_index` key is present, the legacy index is + promoted into the pool inside a freshly-minted `banks` blob and the book is + `{ pool }` with zero named banks — a one-way, lossless promotion. After migration + the `banks` blob is **authoritative**; the legacy `bank_index` key is **retired** (not + written or read back going forward). The one-way retirement trades pre-multi-bank + backward-read compatibility for the clean single-blob shape — an accepted, + forward-only migration consistent with how M4 project state already moves forward. - **Vertical-split UI, pool on top.** The bank window splits vertically: **pool on top**, the **named-banks region below** (a tab-page strip, one tab per named bank, empty when none exist). Two full-height toggles collapse the split: **pool @@ -409,12 +427,15 @@ arrange; the only change is *which* index the entry lands in. the relative-path invariant at its `add` boundary — the book adds no new path handling, because movement is index-only and files never relocate. The invariant is enforced N times (once per bank) by the exact code that enforces it today. -- **Non-destructive.** Bank create / rename / delete / activate and sample +- **Non-destructive.** Bank create / rename / delete / activate / evacuate and sample move / copy mutate only index + ext-state; no file is written, moved, or deleted, - and no timeline item is touched. Deleting a **named** bank does not delete its - samples' files (they may be referenced by the pool or another bank via copy); - file lifecycle stays owned by the capture/prune path, not the bank container. See - open fork on what "delete a named bank" does to its member samples. + and no timeline item is touched. Deleting a **named** bank drops its member index + entries but does **not** delete their files; file lifecycle stays owned by the + capture/prune path, not the bank container. A file referenced *only* by the deleted + bank becomes an orphan on disk — present but indexed by no bank — until the + capture/prune path reclaims it. That orphaned-until-prune window is designed, not + accidental; the *evacuate* verb and the confirm-on-non-empty-delete guardrail exist + to keep the user out of it unintentionally. - **Travels-with-the-.rpp preserved.** The `banks` blob rides the same ext-state namespace and the same GUID-primary project-identity / Save-As-relocation machinery as the bank index does today (M4). One shared physical bank folder, one @@ -429,27 +450,33 @@ Pure (no REAPER types, unit-tested — the mirror of `bank_model` / `view_mode_m - `bank_book` — ordered bank registry (`{ bank id, display name, ordinal, BankIndex }`); pool seeded with fixed id + name; create / rename / reorder / delete named banks (pool-privilege rules enforced here: reject delete/rename of - pool); active-bank id (get/set, defaults to pool); **move** and **copy** a sample - between banks (index-only, destination-collapse observed); query a bank's index; - JSON round-trip of the whole book (banks + per-bank indices + active id + - ordinals) and legacy-`bank_index`→pool migration on parse. + pool; delete drops member index entries); **evacuate** a bank (move every member to + the pool, index-only, destination-collapse observed; pool cannot be evacuated); + active-bank id (get/set, defaults to pool); **move** and **copy** a sample between + banks (index-only, destination-collapse observed); query a bank's index; JSON + round-trip of the whole book (pool-as-bank-zero inside the blob + named banks + + per-bank indices + active id + ordinals) and legacy-`bank_index`→pool migration on + parse (one-way; blob authoritative thereafter). REAPER-facing: - `persist` (slice) — serialize/deserialize the book under the `banks` key in - `"reasampler"`; migrate a legacy `bank_index` key into the pool on first load; - reload-on-open and Save-As survival ride the existing M4 machinery. The session - exposes the book the way it exposes the bank today; the active bank's `BankIndex` - is what the capture layer adds to. + `"reasampler"` (pool-as-bank-zero inside the blob; no separate `bank_index` key + going forward); migrate a legacy `bank_index` key into the pool on first load + (one-way; blob authoritative thereafter, legacy key retired); reload-on-open and + Save-As survival ride the existing M4 machinery. The session exposes the book the + way it exposes the bank today; the active bank's `BankIndex` is what the capture + layer adds to. - `bank_panel` (extension) — the vertical split: pool grid on top, named-banks tab-page region below; two full-height toggles; the active-bank indicator; the create / rename / delete / activate affordances; sample move/copy affordance (drag between regions and/or a "send to bank" menu on selection). Reuses the existing LICE grid render loop per bank region. -- `actions` (entries) — create bank / rename bank / delete bank; activate bank - (direct + cycle); move selected samples → bank; copy selected samples → bank; - pool/banks full-height toggles. Registered with the `command_id` / `gaccel` / - `hookcommand` pattern; bank-activate + move/copy MIDI-bindable to suit the - capture-heavy workflow. +- `actions` (entries) — create bank / rename bank / delete bank (confirm on + non-empty delete); evacuate bank → pool; activate bank (direct + cycle); move + selected samples → bank; copy selected samples → bank; pool/banks full-height + toggles. Registered with the `command_id` / `gaccel` / `hookcommand` pattern; + bank-activate + move/copy + evacuate MIDI-bindable to suit the capture-heavy + workflow. ## REAPER API surface (verify all signatures) @@ -474,8 +501,12 @@ verify against the SDK header where they extend existing surfaces: - **No cross-bank dedup enforcement.** The same hash may exist in multiple banks (that is what copy is for). Do not add a global dedup that collapses across banks. - **Pool privileges are inviolable.** No action path may delete or rename the pool, - or leave a project with zero banks. Enforce in the pure rules layer, not just the - UI. + leave a project with zero banks, or **evacuate** the pool (the pool is evacuation's + destination, not a source). Enforce in the pure rules layer, not just the UI. +- **Delete drops members; files are never deleted by a bank op.** Deleting a named + bank removes its member index entries only. No bank operation writes, moves, or + deletes a file — file lifecycle stays with capture/prune. The UI **confirms on + non-empty delete** and offers evacuate; do not silently orphan members. - **Capture still never inserts into the arrange.** The load-bearing principle is unchanged; multi-bank only redirects which index the capture lands in. - **Additive only.** Do not alter `BankIndex`, the M0–M11 capture roadmap, or Phase @@ -484,25 +515,18 @@ verify against the SDK header where they extend existing surfaces: ## Open questions to resolve during build -- **`bank_index` key retirement vs. retention.** Two shapes: (a) the pool's index - rides *inside* the `banks` blob and the legacy `bank_index` key is retired after a - one-way migration; (b) the `bank_index` key is *kept* as the pool's canonical - storage slot and `banks` holds only the named banks + ordering + active id. (a) is - cleaner (one blob, one section) but rewrites where the pool lives; (b) is more - conservative (existing pool persistence untouched, named banks are pure addition) - at the cost of the pool being stored differently from named banks. Leaning (b) for - additive-minimalism against in-flight M7/M8 persist work — but this is a genuine - fork; see product notes. -- **What "delete a named bank" does to its members.** Options: (i) *reabsorb* — - member samples move back to the pool (no sample is ever lost to a bank delete); - (ii) *orphan-check* — delete members whose file is referenced by no other bank, - keep the rest; (iii) *forbid non-empty delete* — require the bank be emptied - first. (i) is the safest and simplest mental model (a named bank is a *grouping*, - deleting the group returns things home) and pairs naturally with index-only - movement. Leaning (i); flag for Daniel. -- **Move via drag vs. menu as the primary affordance**, and whether the named-banks - region is REAPER-native tabs (SWELL tab control) or LICE-drawn tabs matching the - grid aesthetic. UI-mechanics detail for the panel build; verify SWELL tab-control - availability against the M5 reference. -- **Active-bank indicator placement** — in the pool/bank region headers, or a - single header readout. Panel-polish detail. +Forks 1–4 are settled (see product notes → *Settled forks*, and the settled-decision +prose above). Two panel-build items remain open. + +- **Fork 5 — tab rendering + move affordance (B4).** Two sub-questions: (5a) the + named-banks region as a SWELL-native tab control vs. LICE-drawn tabs matching the + grid aesthetic; (5b) the move gesture as drag-between-regions vs. a "send to bank" + menu vs. both. Full options analysis (pros/cons across visual consistency, keyboard + nav, HiDPI, cross-platform SWELL parity, scaling, discoverability, mis-drop risk, + MIDI-bindability, implementation cost) and pending recommendations in product notes + → *Fork 5*. **Verify SWELL tab-control availability and cross-platform behavior + parity against the SWELL headers / SWS reference** before committing to native tabs. + Decision pending Daniel. +- **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. diff --git a/PLAN.md b/PLAN.md index 426df77..24df068 100644 --- a/PLAN.md +++ b/PLAN.md @@ -98,42 +98,53 @@ privileged, create/rename/reorder/delete named banks, active-bank id, move/copy 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 (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. JSON round-trip lossless across banks + per-bank indices + ordinals + +**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. + 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, never allow zero banks. + 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). -- [ ] JSON round-trip: banks + per-bank indices + ordinals + active id. +- [ ] 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). -- [ ] Tests: pool privileges (delete/rename rejected); move source-loses/dest-gains; - copy source-retained; cross-bank same-hash coexistence; dest collapse on move into - a bank already holding the hash; JSON lossless; legacy migration. + 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. ## B2 — persist slice (banks ↔ project ext state) **Goal:** Serialize the book under the `banks` key in `"reasampler"` alongside the -existing sections; migrate a legacy `bank_index` key into the pool on first load; -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. -**Depends on:** B1. Resolve the `bank_index` retirement-vs-retention fork first -(see Phase B open questions). +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 (shared blob, distinct - section from `bank_index` / `view_state`). -- [ ] Legacy-migration path on load: absent `banks` + present `bank_index` → pool. +- [ ] 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. @@ -141,44 +152,54 @@ only) loads as pool + zero named banks with no sample loss. ## 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 MIDI-bindable; create/rename/delete drive the B1 model via -the B2-persisted session. +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 honors the resolved - member-disposition rule — see open questions). +- [ ] 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 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 - MIDI-bindable. + + 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 indicated; -both full-height toggles collapse the split correctly; sample move/copy affordance -works (drag and/or menu); the Design View mode switch in the header is unaffected. -**Depends on:** B1, B2, B3. +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 — +pending Daniel's decision; see Phase B open questions and product notes → *Fork 5*.) - [ ] Vertical split: pool grid region (top) + named-banks tab-page region (bottom). - [ ] Named-banks tab strip: one tab per named bank; empty state when none. + (SWELL-native vs. LICE-drawn — fork 5a, pending.) - [ ] Pool full-height / banks full-height toggle affordances wired to B3. -- [ ] Active-bank indicator. -- [ ] Create / rename / delete / activate affordances driving B3 actions. -- [ ] Sample move/copy affordance (drag between regions and/or "send to bank" menu). +- [ ] 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/copy affordance (drag between regions and/or "send to bank" menu — + fork 5b, pending). ## Phase B open questions -- **`bank_index` key retirement vs. retention** — (a) fold the pool into the `banks` - blob and retire the legacy key, vs (b) keep `bank_index` as the pool's canonical - slot and store only named banks under `banks`. Leaning (b) for additive-minimalism - against in-flight M7/M8 persist work. Resolve before B2. (blocks B2) -- **Named-bank delete → member disposition** — (i) reabsorb into pool, (ii) - orphan-check by file reference, (iii) forbid non-empty delete. Leaning (i). - (touches B1/B3) -- **Move affordance + tab rendering** — drag vs. menu as primary; SWELL-native vs. - LICE-drawn tabs. Verify SWELL tab-control availability against the M5 reference. +Forks 1–4 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). Folded into CONTEXT.md §Multi-bank + the B1–B4 points above. Remaining: + +- **Fork 5 — tab rendering + move affordance** — (5a) SWELL-native tab control vs. + LICE-drawn tabs matching the grid aesthetic; (5b) drag-between-regions vs. + "send to bank" menu vs. both. Options analysis + pending recommendations in product + notes → *Fork 5*. Verify SWELL tab-control availability + cross-platform parity + against the SWELL headers / SWS reference before choosing native tabs. Decision + pending Daniel. (touches B4) +- **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) -- **Active-bank indicator placement** — per-region headers vs. single header - readout. (touches B4) diff --git a/docs/product/multi-bank.md b/docs/product/multi-bank.md index 71bcf86..acbf124 100644 --- a/docs/product/multi-bank.md +++ b/docs/product/multi-bank.md @@ -6,8 +6,9 @@ detail in `CONTEXT.md` (§Multi-bank). This doc holds the *why* — the workflow narrative, the pool-privilege reasoning, the movement semantics, and the design-direction recommendations — so those don't clutter the build docs. -Status: framed by product-designer (2026-07-23), pending Daniel review. Open items -for Daniel are listed at the bottom. +Status: framed by product-designer (2026-07-23). Forks 1–4 settled by Daniel +(2026-07-23); fork 5 (tab rendering + move affordance) still open — options +analysis at the bottom. Settled forks are folded into the prose below. --- @@ -18,9 +19,11 @@ banks.** Today ReaSampler has exactly one bank (`BankIndex`) — every capture l in it, and it is the whole library. That is fine at ten samples and unusable at two hundred. Multi-bank keeps the one bank as **the pool** — the default catch-all every capture still lands in — and lets the user create **named banks** ("Drums", -"1-Shots", "Synth Hits") that group samples for a purpose. Samples move (or copy) -freely between any banks, including to and from the pool. One bank at a time is the -**active bank**: the capture target. The pool is active by default. +"1-Shots", "Synth Hits") that group samples for a purpose. **Move** is the primary +gesture — a sample relocates from one bank to another, living in exactly one bank at +a time — with **copy** the deliberate secondary act when the user wants a sample in +two banks at once. One bank at a time is the **active bank**: the capture target. The +pool is active by default. **It is not a new file layout.** Banks are *logical groupings over one shared file pool*, not folders on disk. Moving a sample between banks moves an index entry, not @@ -94,14 +97,17 @@ tested core, not a modification of it. ## Movement semantics (the settled rules, in prose) -- **Move is index-only.** Moving a sample from bank A to bank B removes the `Sample` - from A's `BankIndex` and adds it to B's. The file never moves — one shared file - pool under the project bank folder, exactly where M4 put it. Cheap, - non-destructive, immune to the path-rewrite bug class M4 closed. -- **Copy is index-only too.** Copy adds the sample to B and *leaves it in A*. Two - index entries, one file, two banks. Copy is the mechanism that lets a sample live - in the pool *and* in a named group at once — the pool stays the complete library, - and "Drums" is a curated view into part of it. +- **Move is index-only, and is the default gesture.** Moving a sample from bank A to + bank B removes the `Sample` from A's `BankIndex` and adds it to B's. The file never + moves — one shared file pool under the project bank folder, exactly where M4 put it. + Cheap, non-destructive, immune to the path-rewrite bug class M4 closed. After a move + the sample lives in exactly one bank; this is the primary, low-friction affordance + (see design-direction, below). +- **Copy is index-only too, and is the deliberate secondary act.** Copy adds the + sample to B and *leaves it in A*. Two index entries, one file, two banks. Copy is + the mechanism that lets a sample live in the pool *and* in a named group at once — + or in two named groups. It is the explicit way to keep a sample in more than one + place; move (the default) puts it in one place. - **Dedup-by-hash is per-bank, and collapse is observed across a move.** Each `BankIndex` dedups within itself, unchanged. If you move (or copy) a sample into a bank that already holds its hash, the destination *collapses* onto its existing @@ -127,10 +133,18 @@ trivially correct. ## Persistence and migration The book rides the existing `"reasampler"` project ext-state namespace under a new -key, `banks`, alongside `bank_index`, `view_state`, and `project_guid`. Same -namespace, same travel-with-the-`.rpp` guarantee, same GUID-primary identity and -Save-As-relocation machinery from M4. One shared physical bank folder; one -ext-state namespace; now three logical sections. +key, `banks`, alongside `view_state` and `project_guid`. Same namespace, same +travel-with-the-`.rpp` guarantee, same GUID-primary identity and Save-As-relocation +machinery from M4. One shared physical bank folder; one ext-state namespace. + +**The pool folds into the `banks` blob; the legacy `bank_index` key is retired +(fork 1, settled (a)).** The pool's index is stored *inside* the `banks` blob as +bank-zero, exactly like every named bank — one blob, one section, one JSON shape. +The old top-level `bank_index` key is not written going forward; after migration the +`banks` blob is the single authoritative source for all bank state including the +pool. This is the cleaner shape and keeps the "pool is just bank-zero" data-model +uniformity honest all the way down to storage: the pool is not persisted +differently from a named bank. **Migration of an existing single-bank project is the load-bearing edge case.** A project saved before this phase has a `bank_index` key and no `banks` key. On load, @@ -140,12 +154,17 @@ the user opens their old project and finds everything in the pool exactly as bef now with the ability to add named banks. This must be a first-class, tested path (B1 pure-model migration + B2 in-DAW load), not an afterthought. -There is one genuine fork here, called out in the open items: whether the pool's -index lives *inside* the `banks` blob (retire the legacy `bank_index` key) or the -`bank_index` key is *kept* as the pool's canonical slot with `banks` holding only -named banks. Both migrate cleanly; the trade is cleanliness (one blob) vs. -additive-minimalism (existing pool persistence untouched, named banks pure -addition). The lean recommendation is the conservative one — see open items. +**The migration is a one-time promotion, and it is one-way.** On load: legacy +`bank_index` present, `banks` absent → promote the legacy index into the pool inside +a freshly-minted `banks` blob, then treat the blob as authoritative. Once the `banks` +blob exists it is the only source consulted; the legacy key is not read back or +kept in sync. A project touched by this version and saved will carry `banks` and +(going forward) will not carry a written `bank_index` — the retirement is complete +after the first save. The promotion is lossless: no sample, no path, no ordinal is +dropped. This trades a small amount of forward-only commitment (a project saved by +this version will not re-open cleanly in a pre-multi-bank build that only knows +`bank_index`) for the clean single-blob shape — an acceptable, one-directional +migration, consistent with how M4 project state already moves forward. --- @@ -159,31 +178,87 @@ are properties of the capture path and the file. The invariants it *does* touch: `BankIndex` in the book, by the exact code that enforces it today. Movement adds no path handling because files never relocate. - **Non-destructive** extends to bank operations: create / rename / delete / - activate and sample move / copy mutate only index + ext-state. No file is written, - moved, or deleted; no timeline item is touched. In particular, **deleting a named - bank does not delete its samples' files** — a file may be referenced by the pool - or another bank via copy, and file lifecycle stays owned by the capture/prune - path, never the bank container. + activate / evacuate and sample move / copy mutate only index + ext-state. No file + is written, moved, or deleted; no timeline item is touched. In particular, + **deleting a named bank drops its member index entries but does not delete their + files** — file lifecycle stays owned by the capture/prune path, never the bank + container (fork 2, settled). A file that was referenced *only* by the deleted bank + becomes an orphan on disk — present but indexed by no bank — until the + capture/prune path reclaims it. That orphaned-until-prune window is a designed + consequence of "delete drops members, files persist," not an accident (see + *design-direction → evacuate* below for how the workflow avoids it). - **Travels-with-the-.rpp** is preserved by riding the M4 machinery unchanged. --- -## Design-direction recommendations (opinionated) +## Design-direction (settled forks 2–4, plus rationale) -### The pool stays the complete library; named banks are curated views +### Move is the default; the pool is a bank, not a permanent mirror -The strongest mental model — and the one the index-only, copy-friendly design is -built for — is: **the pool is everything you've captured; named banks are curated -subsets you assemble by hand.** Copy (not move) into a named bank keeps the pool -complete, so the user can always fall back to "it's in the pool somewhere" while -"Drums" stays a clean working set. Move is there for the user who wants a strict -partition, but copy is the gentler default the UI should make easy. This is -borrowed from playlist-vs-library models (music apps, Lightroom collections): the -library is authoritative and complete; collections are lightweight views over it. +**Settled (fork 3): move is the primary, low-friction gesture; copy is the +deliberate secondary act.** This inverts the earlier framing (which leaned +copy-as-default on a "pool is the complete library, banks are views" model). Daniel's +call is a *partition* mental model: a sample lives in one bank at a time, and moving +it to "Drums" takes it *out* of the pool. Copy remains available for the user who +genuinely wants a sample in two banks at once, but it is the explicit, deliberate act +— not the default. -Recommendation: make **copy the low-friction gesture** (drag, or a one-click "add to -bank") and **move the deliberate one** (explicit "move" menu item). Do not force the -user to choose partition semantics up front. +This changes what "the pool is the complete library" means, and the docs must be +honest about it. Under move-as-default the pool is **not** a permanent mirror of +everything ever captured — it is the default catch-all every capture lands in *and* +the from-which/back-to-which home for organizing, but once a user moves a sample into +a named bank it is no longer in the pool. The honest framing is: + +- **The pool is the default home and the staging ground**, not a guaranteed superset. + Fresh captures land there; from there the user *moves* them into named banks. What + is "in the pool" is what hasn't been filed yet plus whatever the user deliberately + keeps there. +- **Named banks are where filed samples live**, not lightweight views over a + still-complete library. A sample in "Drums" is *in Drums* and nowhere else unless + copied. +- **Copy is the escape hatch** for the "I want this in two places" case — borrowed + from the move-vs-copy distinction every file manager makes. It is deliberately the + less prominent of the two so the default stays a clean partition. + +Prior art: this is the file-manager / DAW-media-pool model (move relocates, copy +duplicates) rather than the playlist-library model. It suits a capture-heavy workflow +where the user is actively filing rather than curating views over a fixed corpus. + +### Evacuate: the "keep the samples, drop the grouping" gesture + +**Settled (fork 2): delete drops a bank's member index entries; a separate +*evacuate* operation moves all members back to the pool.** These are two distinct +verbs for two distinct intents: + +- **Delete** removes the bank *and its member index entries*. Files are not touched + (capture/prune owns file lifecycle). Use it when you want the grouping *and its + contents' index entries* gone. +- **Evacuate** moves every member of the bank back to the pool (index-only, observing + the same destination-collapse-by-hash as a normal move), leaving the bank empty. + Use it when you want to dissolve the grouping *but keep the samples*. + +The intended workflow is **"evacuate, then delete"** when the user wants to keep the +samples and drop only the grouping, versus a **plain delete** when they want the +grouping and its members gone. Evacuate is a pure `bank_book` operation (B1) and a +bindable action (B3). Pool-privilege rules still apply: you cannot evacuate the pool +(it has nowhere to evacuate *to*, and it is the evacuation destination). + +**The non-empty-delete hazard, and the recommended guardrail.** Because delete drops +members (rather than reabsorbing them, the earlier lean) and move is the default (so a +sample is often in exactly one bank), a plain delete of a *non-empty* named bank can +orphan those samples out of every index — their files persist on disk until prune, +but no bank references them. That is the orphaned-until-prune window called out in the +precision-invariant section. It is a real footgun: "I deleted Drums and my kick is +gone from the library." + +Recommendation (B4, panel-polish, pending Daniel): **confirm on non-empty delete**, +with the confirmation naming the escape hatch. Something like *"Delete 'Drums'? Its 14 +samples will be removed from the library (files remain on disk until pruned). +Evacuate to pool instead?"* — a three-way affordance (Delete / Evacuate then delete / +Cancel) turns the hazard into a teaching moment for the evacuate verb. An *empty* +bank deletes with no confirmation (nothing to lose). This keeps delete fast where it's +safe and guarded where it bites. I recommend adopting the confirm-on-non-empty +guardrail; the exact copy and button set are a B4 detail. ### The vertical split: pool on top, named banks as a tab strip below @@ -203,20 +278,25 @@ curated views are the swappable bottom region. (hide the pool — "I'm organizing"). These are mode-of-work toggles, not layout fiddling, and they map to the two halves of the workflow (capture vs. curate). -### Keep "active bank" distinct from "shown tab" +### Active bank and shown tab stay distinct (settled fork 4) -A subtle but important call: the **capture-active** bank (where new captures land) -and the **currently-shown** named-bank tab are *different* concepts. You might be -looking at "Drums" while capturing into the pool, or vice versa. Conflating them -("the tab you're viewing is the capture target") would make it too easy to -capture into the wrong bank by merely browsing. +**Settled (fork 4): the capture-active bank and the currently-shown named-bank tab +are distinct concepts, and the active bank carries an unmistakable visual +indicator.** The **capture-active** bank (where new captures land) and the +**currently-shown** tab are *different* things — you might be looking at "Drums" while +capturing into the pool, or vice versa. Browsing a tab **never** retargets capture. +Conflating them ("the tab you're viewing is the capture target") was considered and +rejected: it makes it too easy to capture into the wrong bank by merely browsing, and +capture-into-wrong-bank is a silent error the user won't notice until later. -Recommendation: **active bank is an explicit state with a clear indicator**, set by -an explicit "activate" affordance (and action), *not* implied by which tab is shown. -The pool is active by default; activating a named bank is a deliberate act. The -indicator should be unmistakable — the capture target is a thing you want to be sure -of before you hit capture. Placement (per-region header vs. single readout) is a -panel-polish open item. +Active bank is an explicit state, set by an explicit "activate" affordance (and +action), *not* implied by which tab is shown. The pool is active by default; +activating a named bank is a deliberate act. **The indicator must be visually +unmistakable** — this is now a settled constraint, not a suggestion. The capture +target is a thing the user must be able to verify at a glance before hitting capture. +The indicator's exact placement (per-region header vs. single header readout vs. +lit-tab treatment) remains a B4 panel-polish detail — but that it reads unmistakably +is fixed. ### The Design View mode switch and multi-bank are orthogonal — and should read that way @@ -247,11 +327,14 @@ Stable command-id strings follow the sampler family prefix. Bank-activate and move/copy are MIDI-bindable to suit the capture-heavy, hands-on workflow. - **Create bank** / **Rename bank** / **Delete bank** — manage the named-bank set. - (Pool is un-deletable / un-renamable — the actions refuse on the pool.) + (Pool is un-deletable / un-renamable — the actions refuse on the pool.) Delete on a + non-empty bank confirms (see evacuate, above). +- **Evacuate bank → pool** — move all of a bank's members back to the pool, leaving + it empty; the "keep the samples, drop the grouping" verb. (Refuses on the pool.) - **Activate bank** (direct-by-id) / **Cycle active bank** — set the capture target; distinct from browsing a tab. - **Move selected samples → bank** / **Copy selected samples → bank** — the two - movement verbs; copy the low-friction default per the recommendation above. + movement verbs; move is the low-friction default, copy the deliberate secondary. - **Pool full-height** / **Banks full-height** (toggles) — collapse the split. --- @@ -269,20 +352,25 @@ Mirrors the capture and Design View pillars exactly. - Active-bank id (get/set, defaults to pool); resolve the active bank's `BankIndex`. - Move / copy a sample between banks — index-only, destination collapse-by-hash observed, move removes the source entry. +- Evacuate a bank — move every member to the pool (index-only, same destination + collapse-by-hash as move), leaving the bank empty; pool cannot be evacuated. - JSON round-trip of the whole book (banks + per-bank indices + ordinals + active - id); legacy-`bank_index`→pool migration on parse. + id), pool included as bank-zero *inside* the blob; legacy-`bank_index`→pool + migration on parse (one-way, blob becomes authoritative). - `BankIndex` is untouched — `bank_book` wraps, never modifies it. **`persist` slice:** - Serialize/deserialize the book under the `banks` key in `"reasampler"` (shared - blob, distinct section from `bank_index` / `view_state`). -- Migrate a legacy `bank_index` key into the pool on first load. + namespace, distinct section from `view_state`; the pool rides *inside* the `banks` + blob as bank-zero — no separate `bank_index` key going forward). +- Migrate a legacy `bank_index` key into the pool on first load (one-way, lossless; + after migration the `banks` blob is authoritative and the legacy key is retired). - Reload-on-open and Save-As survival via the existing M4 machinery; the session exposes the book; the active bank's `BankIndex` is the capture add target. -**`actions` entries:** the set listed above, registered with the -`command_id` / `gaccel` / `hookcommand` pattern; bank-activate + move/copy -MIDI-bindable. +**`actions` entries:** the set listed above (including **evacuate bank → pool**), +registered with the `command_id` / `gaccel` / `hookcommand` pattern; bank-activate + +move/copy + evacuate MIDI-bindable. **UI (extending the M5 `bank_panel`):** the vertical split (pool grid top, named-banks tab-page region bottom); the two full-height toggles; the active-bank @@ -292,33 +380,142 @@ the M5 LICE grid render loop per region. --- -## Open items for Daniel +## Settled forks (Daniel, 2026-07-23) -1. **`bank_index` key retirement vs. retention.** Two persistence shapes: (a) fold - the pool's index *into* the `banks` blob and retire the legacy `bank_index` key - after a one-way migration — cleaner, one blob, one section; (b) *keep* the - `bank_index` key as the pool's canonical storage slot and store only named banks - + ordering + active id under `banks` — more conservative, existing pool - persistence untouched, named banks a pure addition. I lean **(b)** because M7/M8 - persist work is in flight in another worktree and (b) leaves the existing - `bank_index` write path alone. But it's a genuine fork with a real trade — your - call before B2 is scoped. -2. **Named-bank delete → member disposition.** When a user deletes a *named* bank - holding samples: (i) *reabsorb* — members move back to the pool, nothing is ever - lost to a delete; (ii) *orphan-check* — delete members no other bank references, - keep the rest; (iii) *forbid non-empty delete* — require emptying first. I lean - **(i)** — a named bank is a grouping, deleting the group returns things home, and - it pairs naturally with index-only movement. Confirm or redirect. -3. **Move vs. copy defaults.** I'm recommending **copy as the low-friction gesture** - (drag / one-click) and **move as the deliberate one** (explicit menu), on the - "pool is the complete library" model. If you'd rather move be the default (strict - partition mental model), say so — it changes the primary affordance. -4. **Active-bank vs. shown-tab separation.** I'm recommending these stay *distinct* - (browsing a tab does not change the capture target; activation is explicit). If - you'd prefer the simpler "the tab you're viewing is the capture target" coupling, - flag it — it's a real simplification but risks capturing into the wrong bank by - browsing. -5. **Tab rendering + move affordance mechanics.** SWELL-native tab control vs. - LICE-drawn tabs matching the grid aesthetic; drag-between-regions vs. menu as the - primary move gesture. Panel-build detail — noted for B4, not phase-defining, and - the SWELL tab-control availability needs verification against the M5 reference. +Four of the five open forks are now decided; their implications are folded into the +prose above. Recorded here as a scan-line so the decisions don't have to be +reconstructed from the body text. + +1. **Persistence key — (a), retire `bank_index`.** The pool folds into the `banks` + blob as bank-zero; the legacy `bank_index` key is retired after a one-way, + lossless migration; the blob is authoritative thereafter. (See *persistence and + migration*.) +2. **Delete drops members; add an *evacuate* verb.** Deleting a named bank drops its + member index entries (files untouched, owned by capture/prune). A separate + *evacuate* operation moves all members back to the pool. Workflow: "evacuate then + delete" to keep samples; plain delete to drop the grouping and its members. + Non-empty delete should confirm and offer evacuate. (See *design-direction → + evacuate*; orphaned-until-prune consequence in *precision-invariant implications*.) +3. **Move is the default gesture; copy is the deliberate secondary.** A sample lives + in one bank at a time; copy is the explicit "in two places" escape hatch. (See + *design-direction → move is the default*.) +4. **Active bank and shown tab stay distinct; active bank gets an unmistakable + indicator.** Browsing never retargets capture; the "visually unmistakable" + requirement is a settled constraint (placement remains a B4 detail). (See + *design-direction → active bank and shown tab stay distinct*.) + +--- + +## Fork 5 — options for discussion, decision pending + +The one fork Daniel wants to weigh before deciding. Two sub-questions, both B4 +panel-build mechanics, neither phase-defining but both shaping the panel's feel. +Nothing below is decided; the recommendations at the end are *pending Daniel's call*. + +### 5a — Named-banks tab rendering: SWELL-native tabs vs. LICE-drawn custom tabs + +The named-banks region needs a tab strip (one tab per named bank). Two ways to draw +it. + +**Option A — SWELL-native tab control** (`SWELL`'s tab-control, the cross-platform +Win32 `SysTabControl32` analog). *Verify against the SWELL headers* — SWELL tab-control +availability and behavior-parity across Win/mac/Linux is a known soft spot and must be +checked, not assumed; the SWS extension is the reference for which SWELL controls are +safe to lean on. + +- **Pros:** near-zero custom draw/hit-test code; keyboard navigation (arrow between + tabs, focus handling) comes for free on platforms where SWELL implements it; + gets native-ish affordances (overflow, tooltips) without hand-rolling. +- **Cons:** *visual inconsistency* — a native tab control will not match the M5 LICE + grid aesthetic or the Design View segmented switch; it will read as a bolted-on OS + widget in an otherwise custom-drawn surface. HiDPI/scaling behavior of SWELL controls + is uneven across platforms. Cross-platform parity is the real risk: the control may + look and behave three different ways on Win/mac/Linux, and SWELL's tab + implementation is less battle-tested than its buttons/edits. +- **Scaling:** native tab controls handle many tabs via built-in overflow/scroll, so + it degrades more gracefully past ~10–15 banks than a naive custom strip would. + +**Option B — LICE-drawn custom tabs** matching the grid aesthetic (the same render +path M5 already uses, and the same approach the Design View segmented mode switch +`[ Arrange | Design ]` took). + +- **Pros:** *visual consistency* — tabs match the grid and the existing segmented + switch; one coherent custom-drawn surface. Full control over HiDPI (draw at the + panel's own scale factor, as M5 already must). Identical look and behavior on all + three platforms because we own every pixel — no SWELL-parity risk. Precedent + exists in-repo: the Design View segmented switch is already a LICE-drawn + multi-segment selector, so the pattern and hit-testing are partly solved. +- **Cons:** we hand-roll hit-testing, keyboard navigation, and overflow. Keyboard nav + (tab/arrow to move between banks) is real work we'd otherwise get free from a native + control. Overflow past the visible width needs a scroll or chevron affordance we + build ourselves. +- **Scaling:** a naive fixed-width custom strip breaks down when named banks exceed the + region width (~8–12 tabs depending on name length) unless we build scroll/overflow. + This is the sharpest cost of B — but the Design View switch only ever has two + segments, so its precedent doesn't cover the many-tabs case; we'd extend it. + +*Framing note:* the choice is largely "buy keyboard-nav + overflow from SWELL and pay +in visual inconsistency + cross-platform risk" (A) vs. "pay to build keyboard-nav + +overflow and get visual consistency + platform certainty" (B). How many named banks +we expect in practice is the swing factor: if a user rarely exceeds ~6–8 banks, B's +overflow cost is small and its consistency win is large; if banks routinely run into +the dozens, A's built-in overflow starts to matter. + +### 5b — Move affordance: drag-between-regions vs. "send to bank" menu vs. both + +The move gesture (now the default, per fork 3) needs a surface. Three shapes. + +**Option A — drag between regions** (drag selected samples from the pool grid onto a +named-bank tab, or between tabs). + +- **Pros:** spatially intuitive; matches the mental model ("put this there"); direct. +- **Cons:** *precision / mis-drop risk* is real on a dense grid — dragging onto the + wrong tab or dropping in dead space is easy, and a mis-drop that silently moves a + sample to the wrong bank is exactly the "silent wrong-bank" error we're guarding + against elsewhere. Not keyboard- or MIDI-bindable — but B3 already specifies the + move action *is* MIDI-bindable, so drag can't be the *only* surface. Drag also has + to interoperate cleanly with the M5 grid's existing multi-select drag behavior + (verify there's no gesture collision). + +**Option B — "send to bank" / "move to bank" menu** on the current selection (a +context menu or a toolbar dropdown listing banks; the selected samples move to the +chosen bank). + +- **Pros:** *precise* — you pick the destination from a named list, no mis-drop. Maps + directly onto the B3 bindable action (the menu is just a visual front-end for the + same `move → bankId` operation, so it's consistent with keyboard/MIDI binding). + Consistent with how the M5 grid already handles selection (act-on-current-selection, + not per-item widgets — the Design View tag/untag already works this way). Scales to + many banks (a menu list handles dozens of destinations fine). Cheapest to build in + the LICE/SWELL surface (a menu, not a drag-and-drop hit-test system). +- **Cons:** less *discoverable* than drag for a first-time user; an extra click vs. a + direct drag; feels less "direct-manipulation" than dragging. + +**Option C — both.** Menu as the precise, bindable, discoverable-via-right-click +primary; drag as the direct-manipulation accelerator for users who prefer it. + +- **Pros:** covers both interaction styles; the menu carries the bindable/precise + path while drag serves direct manipulation. This is what most media managers do + (Finder, Lightroom, Ableton browser all offer drag *and* a menu/right-click move). +- **Cons:** most implementation cost (build both, and make them agree); drag still + carries its mis-drop risk even as a secondary path. + +### Recommendations (pending Daniel's decision) + +- **5a — recommend LICE-drawn custom tabs (Option B).** Visual consistency with the + M5 grid and the Design View segmented switch is worth the hand-rolled keyboard-nav + and overflow, *and* it removes the cross-platform SWELL-parity risk entirely (we own + every pixel on all three platforms). The Design View segmented switch is a partial + precedent to extend rather than a from-scratch build. The one thing that would flip + me to native tabs: evidence that users routinely run dozens of named banks, where + built-in overflow earns its keep. Recommend B; revisit if the expected bank count is + high. *Verify SWELL tab-control availability against the headers regardless* — even + choosing B, we should know what the native fallback offers. +- **5b — recommend the "send to bank" menu as primary (Option B), with drag as a + later accelerator (toward C).** The menu is precise, cheap, scales to many banks, + is the natural front-end for the already-bindable B3 move action, and matches how + the M5 grid and Design View already act on the current selection. It sidesteps the + mis-drop hazard that would otherwise fight our "no silent wrong-bank" stance. Drag + is a nice accelerator but should not be the primary or only surface (it can't be + MIDI-bound, and B3 requires the action be bindable). Recommend menu-first; add drag + in a later B4 pass if it earns the cost. Net: ship B, leave the seam for C.