docs(product): settle Phase L §L7 forks F1/F2/F3 (capture-time stamp, interchangeable substrate, drag rule + cursor cues + Alt-replace)

This commit is contained in:
2026-07-27 01:19:36 -04:00
parent e4bcc8f075
commit 7dc77e51b5
2 changed files with 174 additions and 88 deletions
+92 -34
View File
@@ -1692,10 +1692,10 @@ id→slot map) is settled at build; the *contract* below holds either way.
trimmed for content-height purposes (the recommendation: trim trailing empties for scroll trimmed for content-height purposes (the recommendation: trim trailing empties for scroll
extent, keep interior gaps). extent, keep interior gaps).
- **Reorder:** the user drags a card to a target slot within its bank; the pure reorder mutator - **Reorder:** the user drags a card to a target slot within its bank; the pure reorder mutator
moves that sample's position to the target slot, gap-preserving. (Whether a drop onto an moves that sample's position to the target slot, gap-preserving. **Drop-into-empty-slot places
occupied slot displaces/swaps vs. inserts-and-shifts is settled at build — recommendation: there; drop-onto-occupied inserts-before and shifts the tail** (matching common file-manager
**drop-into-empty-slot places there; drop-onto-occupied inserts before and shifts the tail**, reorder) — SETTLED as the *default* drop. (The **Alt+drop-onto-occupied = REPLACE** override is
matching common file-manager reorder.) specified below under Drag disambiguation, F3.)
**JSON round-trip + migration (load-bearing).** `serialize`/`deserialize` stay lossless **JSON round-trip + migration (load-bearing).** `serialize`/`deserialize` stay lossless
including positions (`deserialize(serialize(x)) == x`). **A pre-L7 project blob has no position including positions (`deserialize(serialize(x)) == x`). **A pre-L7 project blob has no position
@@ -1708,13 +1708,20 @@ round-trip + legacy migration.
**Undo.** A reorder is **one Ctrl-Z** — the actions/shell layer wraps the mutation in a batched **Undo.** A reorder is **one Ctrl-Z** — the actions/shell layer wraps the mutation in a batched
undo point (`Undo_BeginBlock2`/`EndBlock2`), matching every existing bank-verb's undo discipline. undo point (`Undo_BeginBlock2`/`EndBlock2`), matching every existing bank-verb's undo discipline.
**M9 overlap (flagged — Daniel's awareness).** M9 "slots" (capture-to-slot-N / insert-slot-N, **Position-model shape (F2 — SETTLED 2026-07-27: interchangeable substrate, NOT fixed slots).**
MIDI-bindable, MPC-style) is **explicitly deferred (Daniel, 2026-07-26).** L7's sparse-slot The carrier is the **plain gap-preserving interchangeable-slot substrate** — a per-bank id→slot
model is a **partial overlap:** it builds the *addressable-position substrate* M9 would sit on, map (Daniel: *"I don't think I want fixed slots MPC style, but the substrate of interchangeable
but L7 adds **no** slot-numbered capture/insert actions and **no** MIDI bindings. Building L7 slots is valuable"*). It is **not** M9-shaped: no slot *identities*, no numbered/addressable
un-defers the *coordinate model* portion of M9, not the *action* portion. If Daniel wants the slots that persist independent of their occupant, no slot actions, no MIDI-bindable slot numbers,
substrate to be explicitly M9-shaped (fixed numbered slots vs. a plain gap-preserving ordinal), no capture-to-slot-N. A slot is just a display position a sample occupies; dragging cards
that is a fork to settle before the model is built. rearranges which sample sits where.
**M9 overlap (awareness note — unchanged intent).** M9 "slots" (capture-to-slot-N /
insert-slot-N, MIDI-bindable, MPC-style) remains **explicitly deferred (Daniel, 2026-07-26).**
The interchangeable substrate L7 builds still *eases* a future M9 revival — it lays the
addressable-position groundwork M9 would sit on — but L7 adds **no** slot-numbered capture/insert
actions and **no** MIDI bindings. The "plain vs. M9-shaped" sub-fork is closed: plain
gap-preserving substrate, per F2 above.
### 2. Decorative metadata overlay (bars.beats · s.ms) ### 2. Decorative metadata overlay (bars.beats · s.ms)
@@ -1728,17 +1735,17 @@ subtle shadowed variant for legibility over the peaks), subordinate to the wavef
the speed constraint (no animation).** Pure formatting helpers (below) are unit-tested; only the the speed constraint (no animation).** Pure formatting helpers (below) are unit-tested; only the
kit draw is shell. kit draw is shell.
**Bars.beats source (FORK F1 — Daniel to confirm).** bars.beats.subdivisions requires a **Bars.beats source (F1 — SETTLED 2026-07-27: capture-time stamp).** bars.beats.subdivisions
**tempo + time-signature** reference. `Sample` already carries `captureTempo` (BPM at capture) requires a **tempo + time-signature** reference. `Sample` already carries `captureTempo` (BPM at
and `lengthBeats`, but **no time-signature.** Two options: capture) and `lengthBeats`, but **no time-signature.** **Decision: capture-time stamp** — add
- **(recommended) Capture-time stamp:** add `captureTimeSigNum` / `captureTimeSigDenom` to `captureTimeSigNum` / `captureTimeSigDenom` to `Sample` + its JSON round-trip, stamped on the
`Sample` + its JSON round-trip, stamped on the capture path (read the project meter at capture capture path (read the project meter at capture via the REAPER meter API — verify
via the REAPER meter API — verify `TimeMap_GetTimeSigAtTime` / equivalent against the SDK at `TimeMap_GetTimeSigAtTime` / equivalent against the SDK at build). bars.beats.subdivisions renders
build). *Why recommended:* the label is **stable** as the project's tempo/meter later changes; from the stamped tempo + meter, **stable under later project tempo/meter changes** — a bank sample
a bank sample outlives the project state it was captured under, matching the existing outlives the project state it was captured under, matching the existing `captureTempo` stamp
`captureTempo` stamp philosophy. **Cost:** this is a **capture-path write beyond draw work** philosophy. This is a **capture-path write beyond draw work** (its own checkbox in the PLAN, now
its own flagged checkbox; old samples with no stamp fall back gracefully (blank musical settled). Old samples with no stamp fall back gracefully (blank musical read-out, or a documented
read-out, or a documented assumed 4/4). assumed 4/4).
- **(rejected) Live project meter at draw time:** the label would drift under the card as the - **(rejected) Live project meter at draw time:** the label would drift under the card as the
project tempo/meter changes, and would be wrong for any sample captured under a different project tempo/meter changes, and would be wrong for any sample captured under a different
meter than the project's current one. meter than the project's current one.
@@ -1766,30 +1773,81 @@ grid-card interaction states stay **visually distinct and coherent:**
`accent/hot` outline on the target slot) must not be confusable with the purple selection `accent/hot` outline on the target slot) must not be confusable with the purple selection
border; spec the exact treatment at build. border; spec the exact treatment at build.
### Drag disambiguation (FORK F3 — proposed rule, Daniel to confirm) ### Drag disambiguation (F3 — SETTLED 2026-07-27)
In-grid reorder must coexist with the existing internal bank-move/copy drag and OS drag-out. In-grid reorder must coexist with the existing internal bank-move/copy drag and OS drag-out.
**Proposed precedence (one clean rule, evaluated live during the drag):** **Precedence (one clean rule, evaluated live during the drag):**
1. **Pointer leaves the client rect → OS drag-out** (unchanged; `drag_out::decideGesture` wins 1. **Pointer leaves the client rect → OS drag-out** (unchanged; `drag_out::decideGesture` wins
first — the existing invariant #4 boundary). first — the existing invariant #4 boundary).
2. **Else drop lands on a tab / the OTHER region's bank → move/copy** (unchanged; Ctrl = copy). 2. **Else drop lands on a tab / the OTHER region's bank → move/copy** (unchanged; Ctrl = copy).
3. **Else drop lands within the SAME bank's own grid → reorder-to-slot** (new). 3. **Else drop lands within the SAME bank's own grid → reorder-to-slot** (new).
- Onto an **empty** slot → place there.
- Onto an **occupied** slot, **no modifier** → insert-before-and-shift-tail (the default,
above).
- Onto an **occupied** slot, **Alt held** → **REPLACE** the occupant (below).
So: leave-client wins → else other-bank wins → else same-bank-grid = reorder. The precedence is So: leave-client wins → else other-bank wins → else same-bank-grid = reorder. The precedence is
encoded in a **pure decision helper** (mirror `drag_out::decideGesture`); the shell reads the encoded in a **pure decision helper** (mirror `drag_out::decideGesture`); the shell reads the
live pointer + focused region + client rect and calls it. This keeps the reorder gesture from live pointer + focused region + client rect + **modifier state (Alt)** and calls it. This keeps
ever stealing an intended bank-move or OS-drag, and keeps a same-bank in-grid drag from being the reorder gesture from ever stealing an intended bank-move or OS-drag, and keeps a same-bank
mis-read as a no-op (today a same-bank drop is a no-op; L7 gives it reorder meaning). in-grid drag from being mis-read as a no-op (today a same-bank drop is a no-op; L7 gives it
reorder meaning).
**Alt+drop-onto-occupied = REPLACE (SETTLED 2026-07-27).** Holding **Alt** at drop over an
occupied slot **replaces the occupant** instead of inserting-and-shifting. Replace semantics,
precisely:
- The replaced sample is **removed from THAT bank's index only** — same semantics as the existing
remove-from-bank verb (`BankBook::removeSample` index-only). **The file stays on disk;** the
owned-manifest and Phase R prune govern its bytes. If the replaced sample's last reference
disappears, that is exactly the existing cross-bank-reference story (`hashReferencedElsewhere`
reports whether the hash is still referenced elsewhere; prune later reclaims a now-orphaned
owned file). Replace introduces **no new deletion authority** — it never touches the disk.
- The dragged sample then takes the vacated slot (the slot's position is preserved; only its
occupant changes).
- **Pool case (un-evacuable pool — SETTLED rule).** The pool's privileges (un-deletable,
un-renamable, **un-evacuable**, never zero banks) are enforced in `bank_book`. **Alt+Replace is
allowed in the pool only when it does not violate a pool privilege.** Concretely: replacing a
pool entry is an index-only removal of that pool sample; it is **permitted** as long as it does
not empty the pool below the pool's floor and does not remove the *last* reference in a way the
pool's rules forbid. The consistent rule the model enforces: **the replace's index-removal step
is the same operation as remove-from-bank, and it must pass the same pool-privilege guard that
remove already applies — if remove-from-pool would be rejected for that sample, Alt+Replace over
it is rejected too** (the drop falls back to a no-op or the default insert-shift; settle the
exact rejection UX at build). No special pool-only replace path; one rule, guarded by the
existing pool invariants.
**Drop-result cursor cues (SETTLED 2026-07-27 — REAPER-idiomatic special cursors).** During a
drag the cursor must indicate **what the drop will do**, following REAPER's idiomatic use of
distinct action cursors. The cue set:
- **Reorder-to-slot** (within the same bank's grid) — a move/reorder cursor.
- **Move/copy to another bank or tab** — the move (or copy, when Ctrl is held) cursor, matching
the existing internal-drag semantics.
- **OS drag-out** (pointer left the client rect) — the OS copy/drag cursor (owned by the OS drag
loop once handed off).
- **Replace** — a distinct replace cursor, shown **only when Alt is actually held over an
occupied slot** (i.e. only when precedence resolves to the Alt+replace case). It must not appear
over an empty slot or when Alt is not held.
**Shell mechanism:** the shell sets the cursor via Win32/SWELL `SetCursor` (REAPER ships its own
action-cursor idiom as the visual reference — match its feel; load or synthesize kit-consistent
cursors at build). **The *decision* of which cue applies stays in the pure gesture/disambiguation
helper** — the same helper that resolves precedence returns the resolved gesture (reorder / move /
copy / os-drag-out / replace), and the shell maps that pure result to a cursor. No cue logic in
the shell; the shell only owns the `SetCursor` call and the cursor resources.
### Pure/shell discipline (L7) ### Pure/shell discipline (L7)
Model: the position carrier + gap semantics + reorder mutator + JSON round-trip/migration are Model: the position carrier + gap semantics + **reorder mutator + Alt-replace mutator** (the
**pure** (in `bank_book`, CTest-covered to the bar of its existing round-trip). Layout: the latter reusing the existing index-only remove-from-bank semantics + pool-privilege guard) + JSON
sparse-aware slot↔rect math + point→slot hit-test + the drag-disambiguation decision are **pure** round-trip/migration are **pure** (in `bank_book`, CTest-covered to the bar of its existing
(extend `bank_grid`; mirror `mode_switch` / `drag_out::decideGesture`). Formatting: the round-trip). Layout: the sparse-aware slot↔rect math + point→slot hit-test + the
bars.beats and s.ms formatters are **pure**. Shell (DAW-bound): the reorder drag wiring + drag-disambiguation decision (**including the resolved-gesture result that drives the cursor cue,
drop-target highlight, the capture-time-signature stamp (F1) read on the capture path, and the and the Alt-over-occupied → replace resolution**) are **pure** (extend `bank_grid`; mirror
kit overlay/selection-border draw. The L1 kit draws; no palette/font decision re-opened. `mode_switch` / `drag_out::decideGesture`). Formatting: the bars.beats and s.ms formatters are
**pure**. Shell (DAW-bound): the reorder/replace drag wiring + drop-target highlight, the
**cursor `SetCursor` call mapping the pure resolved-gesture to a cursor resource**, the
capture-time-signature stamp (F1, settled) read on the capture path, and the kit
overlay/selection-border draw. The L1 kit draws; no palette/font decision re-opened.
## The L3 gate + Phase S coordination contract ## The L3 gate + Phase S coordination contract
+82 -54
View File
@@ -389,50 +389,55 @@ is model + layout + draw only; the "capture ≠ placement" principle is untouche
> per-sample ordinal/slot to the persisted state and its JSON round-trip + a migration for > per-sample ordinal/slot to the persisted state and its JSON round-trip + a migration for
> existing projects — scoped honestly below, model work separated from draw work. > existing projects — scoped honestly below, model work separated from draw work.
> **Forks for Daniel (surface before build — see CONTEXT.md §L7):** > **Forks — all SETTLED 2026-07-27 (see CONTEXT.md §L7 for full detail):**
> - **F1 — bars.beats source.** `Sample` already carries `captureTempo` (BPM at capture) and > - **F1 — SETTLED: capture-time stamp.** Add `captureTimeSigNum` / `captureTimeSigDenom` to
> `lengthBeats`, but **no time-signature field.** bars.beats.subdivisions needs a > `Sample`, stamped on the capture path alongside the existing `captureTempo`;
> time-signature reference. **Recommendation: a capture-time stamp** (add `captureTimeSigNum` > bars.beats.subdivisions renders from the stamped tempo + meter, stable under later project
> /`captureTimeSigDenom` to `Sample`, stamped on the capture path) so the label is stable as > changes. Live-meter-at-draw-time rejected (would drift). The capture-path stamp checkbox
> the project tempo/meter later changes — matching the existing `captureTempo` stamp > below is **settled, not flagged.**
> philosophy. This adds a **capture-path stamp beyond draw work** (its own checkbox below, > - **F2 — SETTLED: plain interchangeable-slot substrate, NOT fixed MPC-style slots.** The
> flagged). Alternative (live project meter at draw time) is rejected: the label would drift > carrier is a **per-bank id→slot map** (gap-preserving interchangeable slots): a slot is just a
> under the card as the project changes, and a bank sample outlives the project state it was > display position an occupant sits in — **no slot identities, no numbered/addressable slots, no
> captured under. > slot actions, no MIDI-bindable slot numbers, no capture-to-slot-N.** The "plain vs. M9-shaped"
> - **F2 — M9 un-deferral awareness.** M9 "slots" (capture-to-slot-N / insert-slot-N, > sub-fork is closed (plain). M9 (capture-to-slot-N / insert-slot-N, MIDI-bindable, MPC-style)
> MIDI-bindable, MPC-style) is **explicitly deferred (Daniel, 2026-07-26).** L7's sparse > stays **explicitly deferred (Daniel, 2026-07-26);** the substrate still eases a future M9
> placement is a **partial overlap** — both need addressable grid positions. Building L7's > revival but L7 adds no M9 actions or bindings.
> sparse-slot model effectively lays the *position substrate* M9 would sit on, but L7 does > - **F3 — SETTLED + EXTENDED.** Drag precedence: leave-client → OS drag-out; else tab/other-bank
> **not** add slot-numbered capture/insert actions or MIDI bindings. Flagged so Daniel knows > → move/copy (Ctrl = copy); else same-bank grid → reorder-to-slot. **Extensions:**
> L7 un-defers the *coordinate model* portion of M9, not the *action* portion. If Daniel wants > (a) **drop-result cursor cues** — the cursor indicates the drop result via REAPER-idiomatic
> the slot substrate to be explicitly M9-shaped (numbered addressable slots vs. a plain > special cursors (reorder / move / copy / OS-drag-out / replace); the shell sets it via
> gap-preserving ordinal), that is a fork to settle before the model checkbox. > Win32/SWELL `SetCursor`, but the *decision* of which cue applies stays in the pure gesture
> - **F3 — drag-disambiguation rule (proposed, Daniel to confirm).** The panel already has > helper. (b) **Alt+drop-onto-occupied = REPLACE** — default occupied-slot drop stays
> internal bank-move/copy drag and OS drag-out. In-grid reorder must coexist. **Proposed > insert-and-shift; holding Alt replaces the occupant (index-only removal, existing
> rule:** a drag whose drop lands **within the SAME bank's own grid** = **reorder-to-slot**; > remove-from-bank semantics — file stays on disk, prune governs bytes, `hashReferencedElsewhere`
> a drop on **a tab / the other region's bank** = **move/copy** (as today); a drag that > handles last-reference). **Pool rule:** Alt+Replace in the un-evacuable pool is allowed only
> **leaves the client rect** = **OS drag-out** (as today). One clean precedence: leave-client > when the same pool-privilege guard that `remove` applies would permit removing that sample —
> wins → else other-bank wins → else same-bank-grid = reorder. See CONTEXT.md §L7. > one rule, no pool-only path; the replace cursor cue shows only when Alt is actually held over
> an occupied slot.
**Verify (in DAW):** capture several samples; drag a card to a new position within its bank — **Verify (in DAW):** capture several samples; drag a card to a new position within its bank —
the order persists across project save/reload and Ctrl-Z restores the prior order; drag the the order persists across project save/reload and Ctrl-Z restores the prior order; drag the
first card below the second so the first row is empty — the gap holds across reload; each card first card below the second so the first row is empty — the gap holds across reload; each card
shows bars.beats.subdivisions bottom-left and s.ms bottom-right, legible over the peaks; a shows bars.beats.subdivisions bottom-left and s.ms bottom-right, legible over the peaks; a
selected card shows the normal cell with a purple (`accent/tertiary`) border — no inversion — selected card shows the normal cell with a purple (`accent/tertiary`) border — no inversion —
and hover / focus / drag-target remain distinct; dragging a card to another bank's tab still and hover / focus / drag-target remain distinct; during any drag the cursor shows the correct
moves/copies (not reorder), and dragging out of the panel still starts an OS file drag; an special cue (reorder within the bank, move/copy over a tab/other bank, OS drag-out past the client
existing project (saved before L7) loads cleanly with cards in their prior insertion order. edge); Alt+drop over an occupied card **replaces** the occupant (the replaced sample leaves that
bank's grid but its file survives on disk; if it was the last reference, prune later reclaims it),
and the replace cursor appears only while Alt is held over an occupied slot; dragging a card to
another bank's tab still moves/copies (not reorder), and dragging out of the panel still starts an
OS file drag; an existing project (saved before L7) loads cleanly with cards in their prior
insertion order.
**Depends on:** L1 (the kit) for all draw; sequences **after L6** (both rework the same **Depends on:** L1 (the kit) for all draw; sequences **after L6** (both rework the same
`bank_panel` grid). **Ungated by Phase S** (the dock panel is on dev); independent of the L3 `bank_panel` grid). **Ungated by Phase S** (the dock panel is on dev); independent of the L3
gate. No `capture` / `insert` behavior change; no palette/font decision re-opened. gate. No `capture` / `insert` behavior change; no palette/font decision re-opened.
**(a) Persisted order + sparse-position model (pure, hard-tested) — model work, separate from draw:** **(a) Persisted order + sparse-position model (pure, hard-tested) — model work, separate from draw:**
- [ ] Add an explicit per-sample **display position** to the persisted bank state (gap-preserving - [ ] Add an explicit per-sample **display position** to the persisted bank state — a
ordinal or slot coordinate — settle the shape against F2). Decide the home: `bank_model` **per-`Bank` id→slot map in `bank_book`** (gap-preserving interchangeable slots, per F2
is stated untouched by `bank_book`'s wrapping — position is a **per-bank display concern**, so settled). `bank_model` / `Sample` stay untouched (position is a per-bank display concern, not a
it belongs with the bank's membership, not on `Sample`. Candidate: a per-`Bank` ordered/keyed `Sample` property — a copy of a sample into two banks may sit at different slots). **No slot
position map in `bank_book` (id → slot), leaving `bank_model` / `Sample` untouched. Settle the identities / numbered addressable slots** — a slot is just a position an occupant sits in.
exact carrier at build.
- [ ] JSON round-trip for the position data (`serialize`/`deserialize` remain lossless: - [ ] JSON round-trip for the position data (`serialize`/`deserialize` remain lossless:
`deserialize(serialize(x)) == x` including positions). `deserialize(serialize(x)) == x` including positions).
- [ ] **Migration for existing projects** (old blobs with no position data): load cleanly with a - [ ] **Migration for existing projects** (old blobs with no position data): load cleanly with a
@@ -440,32 +445,54 @@ gate. No `capture` / `insert` behavior change; no palette/font decision re-opene
a pre-L7 project is visually identical on first load post-L7. a pre-L7 project is visually identical on first load post-L7.
- [ ] **Gap semantics on insert / delete / prune** (pure, tested): a new capture takes the next - [ ] **Gap semantics on insert / delete / prune** (pure, tested): a new capture takes the next
free slot after the last occupied one (append); a delete/remove/prune **leaves a gap** (does free slot after the last occupied one (append); a delete/remove/prune **leaves a gap** (does
not re-pack) so positions stay stable; define whether an empty trailing tail is trimmed. not re-pack) so positions stay stable. **Confirmed defaults:** trailing empty tail trimmed for
scroll-extent purposes; interior gaps preserved.
- [ ] **Reorder mutator** (pure): move sample `id` to target slot within its bank, gap-preserving; - [ ] **Reorder mutator** (pure): move sample `id` to target slot within its bank, gap-preserving;
deterministic; unit-tested. deterministic; unit-tested. Drop-into-empty places there; **drop-onto-occupied (default, no
- [ ] Undo integration: a reorder is one Ctrl-Z (batched undo point in the actions/shell layer, modifier) inserts-before-and-shifts-tail.**
matching the existing bank-verb undo discipline). - [ ] **Alt-replace mutator** (pure): Alt+drop over an occupied slot **replaces the occupant**
index-only removal of the occupant from THAT bank (reuse the existing remove-from-bank
semantics: file stays on disk, owned-manifest/prune govern bytes, `hashReferencedElsewhere`
handles the last-reference case), then the dragged sample takes the vacated slot (position
preserved). **Pool guard:** if the sample sits in the un-evacuable pool, the replace's
index-removal step passes the **same pool-privilege guard that `remove` applies** — if
remove-from-pool would be rejected for that sample, Alt+Replace over it is rejected (falls back
to no-op / default insert-shift; settle rejection UX at build). One rule, no pool-only path.
Deterministic; unit-tested. **No new deletion authority — never touches disk.**
- [ ] Undo integration: a reorder **or a replace** is one Ctrl-Z (batched undo point in the
actions/shell layer, matching the existing bank-verb undo discipline).
**(b) Reorder-drag gesture + slot hit-test (pure geometry + shell wiring):** **(b) Reorder-drag gesture + slot hit-test (pure geometry + shell wiring):**
- [ ] **Sparse-aware grid layout** (pure, extend `bank_grid`): map per-slot positions (with gaps) - [ ] **Sparse-aware grid layout** (pure, extend `bank_grid`): map per-slot positions (with gaps)
to `CellRect`s — an empty slot is drawn empty (or skipped), not back-filled. Slot ↔ pixel-rect to `CellRect`s — an empty slot is drawn empty (or skipped), not back-filled. Slot ↔ pixel-rect
math + point→slot hit-test stay pure and unit-tested (mirror `mode_switch`/`bank_grid`). math + point→slot hit-test stay pure and unit-tested (mirror `mode_switch`/`bank_grid`).
- [ ] **Drag-disambiguation** (F3): drop within the same bank's grid = reorder-to-slot; drop on a - [ ] **Drag-disambiguation** (F3): drop within the same bank's grid = reorder-to-slot (empty =
tab / other bank = move/copy (unchanged); leave client = OS drag-out (unchanged). Precedence place; occupied+no-modifier = insert-shift; **occupied+Alt = replace**); drop on a tab / other
bank = move/copy (unchanged, Ctrl = copy); leave client = OS drag-out (unchanged). Precedence
encoded in a **pure decision helper** (mirror `drag_out::decideGesture`); shell reads live encoded in a **pure decision helper** (mirror `drag_out::decideGesture`); shell reads live
pointer + region + client rect and calls it. pointer + region + client rect + **modifier state (Alt)** and calls it. The helper returns the
**resolved gesture** (reorder / move / copy / os-drag-out / replace) so the shell can map it to
a cursor cue.
- [ ] **Drop-result cursor cues** (F3 extension): the shell sets a REAPER-idiomatic special cursor
per the pure resolved-gesture (reorder / move / copy / OS-drag-out / replace) via Win32/SWELL
`SetCursor`; the **replace** cursor shows only when Alt is actually held over an occupied slot.
**Cue-decision logic stays in the pure helper;** the shell owns only the `SetCursor` call and
the cursor resources (load/synthesize kit-consistent cursors at build; REAPER's own action
cursors as the visual reference).
- [ ] Shell wiring: on a same-bank reorder drop, resolve the target slot via the pure hit-test and - [ ] Shell wiring: on a same-bank reorder drop, resolve the target slot via the pure hit-test and
call the reorder mutator; drop-target slot highlight during the drag (distinct from the existing call the reorder mutator (or, when Alt is held over an occupied slot, the **Alt-replace
region/tab drop highlights). mutator**); drop-target slot highlight during the drag (distinct from the existing region/tab
drop highlights, and — for a pending replace — distinct from the pending-reorder highlight).
**(c) Metadata overlay (pure formatting + kit draw):** **(c) Metadata overlay (pure formatting + kit draw):**
- [ ] **Pure formatters, unit-tested:** `bars.beats.subdivisions` from `lengthSeconds` + - [ ] **Pure formatters, unit-tested:** `bars.beats.subdivisions` from `lengthSeconds` +
`captureTempo` + capture-time signature (F1) and `seconds.milliseconds` from `lengthSeconds`. `captureTempo` + capture-time signature (F1) and `seconds.milliseconds` from `lengthSeconds`.
Deterministic string output; edge cases (zero length, missing tempo → graceful blank/fallback). Deterministic string output; edge cases (zero length, missing tempo → graceful blank/fallback).
- [ ] **[FLAGGED — F1, capture-path stamp beyond draw work] Capture-time signature stamp:** add - [ ] **Capture-time signature stamp (F1, settled — capture-path write beyond draw work):** add
`captureTimeSigNum` / `captureTimeSigDenom` to `Sample` + its JSON round-trip, stamped on the `captureTimeSigNum` / `captureTimeSigDenom` to `Sample` + its JSON round-trip, stamped on the
capture path (read the project meter at capture). Only needed if F1 resolves to the capture-time capture path (read the project meter at capture — verify `TimeMap_GetTimeSigAtTime` / equivalent
stamp (recommended). Old samples with no stamp fall back gracefully in the formatter. against the SDK at build). Old samples with no stamp fall back gracefully in the formatter
(blank musical read-out, or a documented assumed 4/4).
- [ ] **Kit draw:** overlay the two strings on the card — bars.beats bottom-LEFT, s.ms - [ ] **Kit draw:** overlay the two strings on the card — bars.beats bottom-LEFT, s.ms
bottom-RIGHT — via the kit `text()` in the micro / value-mono class, `text/dim` (or a subtle bottom-RIGHT — via the kit `text()` in the micro / value-mono class, `text/dim` (or a subtle
shadowed variant for legibility over the waveform); **decorative, non-interactive** (no shadowed variant for legibility over the waveform); **decorative, non-interactive** (no
@@ -513,15 +540,16 @@ Phase S to reach dev. Phase S feature work runs in parallel, ungated by Phase L.
pure module; sourced from the registered action phrase, prefix stripped at draw time); item-move pure module; sourced from the registered action phrase, prefix stripped at draw time); item-move
and track-tag action ids confirmed; active-mode read confirmed via the same and track-tag action ids confirmed; active-mode read confirmed via the same
`view().activeModeId()` the footer toggle uses. `view().activeModeId()` the footer toggle uses.
- **L7 model + tempo surface (L7)**settle at L7 build/spec-review: (1) **position carrier** - **L7 model + tempo surface (L7)**forks F1/F2/F3 **all settled 2026-07-27** (see the §L7
where the per-sample display position lives (candidate: a per-`Bank` id→slot map in `bank_book`, forks block above and CONTEXT.md §L7): position carrier = per-`Bank` id→slot map in `bank_book`
keeping `bank_model`/`Sample` untouched per CLAUDE.md); (2) **F1 bars.beats source** — confirm (plain interchangeable substrate, `bank_model`/`Sample` untouched); F1 = capture-time signature
the capture-time signature stamp with Daniel (recommended) vs. live project meter; the stamp adds stamp; F3 = drag precedence + REAPER-idiomatic cursor cues + Alt-replace-over-occupied with the
a capture-path write (its own flagged checkbox); (3) **F2 M9 overlap** — confirm L7's sparse-slot stated pool guard. **Remaining build-time verification (SDK/API, not product decisions):**
substrate is not mis-scoped as M9's slot *actions*; (4) **F3 drag rule** — confirm the (1) the REAPER meter-read API for the F1 stamp (`TimeMap_GetTimeSigAtTime` or equivalent) —
same-bank-grid = reorder / other-bank = move-copy / leave-client = OS-drag precedence. Verify the verify signature/argument order against the SDK; (2) the Win32/SWELL `SetCursor` path and cursor
REAPER meter-read API for the stamp (`TimeMap_GetTimeSigAtTime` or equivalent) against the SDK at resource loading/synthesis for the drop-result cues — confirm the cursor idiom against SWELL and
build. REAPER's own action cursors; (3) confirm the existing remove-from-bank pool-privilege guard is
reusable as-is for the Alt-replace index-removal step.
--- ---