docs(multi-bank): settle Phase B forks 1-4; draft fork-5 options analysis
Fold Daniel's decisions into CONTEXT.md, PLAN.md, and the product notes: - Fork 1 (persistence): pool folds into the `banks` blob; legacy `bank_index` key retired after a one-way lossless migration. - Fork 2 (delete): delete drops member index entries; add an `evacuate` verb (B1 pure op + B3 action) returning members to the pool. Design the orphaned-until-prune window and confirm-on-non-empty-delete guardrail. - Fork 3 (move): move is the default gesture, copy the deliberate secondary. - Fork 4 (active bank): active-bank and shown-tab stay distinct; the active-bank indicator must be visually unmistakable. Fork 5 (tab rendering + move affordance) remains open with an options analysis and pending recommendations. Additive; M0-M11 and Phase D untouched.
This commit is contained in:
+79
-55
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user