From 7dc77e51b5e4c4bcfaa78de01577d12415da0d4e Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 27 Jul 2026 01:19:36 -0400 Subject: [PATCH] =?UTF-8?q?docs(product):=20settle=20Phase=20L=20=C2=A7L7?= =?UTF-8?q?=20forks=20F1/F2/F3=20(capture-time=20stamp,=20interchangeable?= =?UTF-8?q?=20substrate,=20drag=20rule=20+=20cursor=20cues=20+=20Alt-repla?= =?UTF-8?q?ce)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTEXT.md | 126 +++++++++++++++++++++++++++++++++++-------------- PLAN.md | 136 ++++++++++++++++++++++++++++++++--------------------- 2 files changed, 174 insertions(+), 88 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 7153b92..07b0cb5 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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 extent, keep interior gaps). - **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 - occupied slot displaces/swaps vs. inserts-and-shifts is settled at build — recommendation: - **drop-into-empty-slot places there; drop-onto-occupied inserts before and shifts the tail**, - matching common file-manager reorder.) + moves that sample's position to the target slot, gap-preserving. **Drop-into-empty-slot places + there; drop-onto-occupied inserts-before and shifts the tail** (matching common file-manager + reorder) — SETTLED as the *default* drop. (The **Alt+drop-onto-occupied = REPLACE** override is + specified below under Drag disambiguation, F3.) **JSON round-trip + migration (load-bearing).** `serialize`/`deserialize` stay lossless 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 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, -MIDI-bindable, MPC-style) is **explicitly deferred (Daniel, 2026-07-26).** L7's sparse-slot -model is a **partial overlap:** it builds the *addressable-position substrate* M9 would sit on, -but L7 adds **no** slot-numbered capture/insert actions and **no** MIDI bindings. Building L7 -un-defers the *coordinate model* portion of M9, not the *action* portion. If Daniel wants the -substrate to be explicitly M9-shaped (fixed numbered slots vs. a plain gap-preserving ordinal), -that is a fork to settle before the model is built. +**Position-model shape (F2 — SETTLED 2026-07-27: interchangeable substrate, NOT fixed slots).** +The carrier is the **plain gap-preserving interchangeable-slot substrate** — a per-bank id→slot +map (Daniel: *"I don't think I want fixed slots MPC style, but the substrate of interchangeable +slots is valuable"*). It is **not** M9-shaped: no slot *identities*, no numbered/addressable +slots that persist independent of their occupant, no slot actions, no MIDI-bindable slot numbers, +no capture-to-slot-N. A slot is just a display position a sample occupies; dragging cards +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) @@ -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 kit draw is shell. -**Bars.beats source (FORK F1 — Daniel to confirm).** bars.beats.subdivisions requires a -**tempo + time-signature** reference. `Sample` already carries `captureTempo` (BPM at capture) -and `lengthBeats`, but **no time-signature.** Two options: -- **(recommended) Capture-time stamp:** add `captureTimeSigNum` / `captureTimeSigDenom` to - `Sample` + its JSON round-trip, stamped on the capture path (read the project meter at capture - via the REAPER meter API — verify `TimeMap_GetTimeSigAtTime` / equivalent against the SDK at - build). *Why recommended:* the label is **stable** as the project's tempo/meter later changes; - a bank sample outlives the project state it was captured under, matching the existing - `captureTempo` stamp philosophy. **Cost:** this is a **capture-path write beyond draw work** — - its own flagged checkbox; old samples with no stamp fall back gracefully (blank musical - read-out, or a documented assumed 4/4). +**Bars.beats source (F1 — SETTLED 2026-07-27: capture-time stamp).** bars.beats.subdivisions +requires a **tempo + time-signature** reference. `Sample` already carries `captureTempo` (BPM at +capture) and `lengthBeats`, but **no time-signature.** **Decision: capture-time stamp** — add +`captureTimeSigNum` / `captureTimeSigDenom` to `Sample` + its JSON round-trip, stamped on the +capture path (read the project meter at capture via the REAPER meter API — verify +`TimeMap_GetTimeSigAtTime` / equivalent against the SDK at build). bars.beats.subdivisions renders +from the stamped tempo + meter, **stable under later project tempo/meter changes** — a bank sample +outlives the project state it was captured under, matching the existing `captureTempo` stamp +philosophy. This is a **capture-path write beyond draw work** (its own checkbox in the PLAN, now +settled). Old samples with no stamp fall back gracefully (blank musical read-out, or a documented +assumed 4/4). - **(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 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 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. -**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 first — the existing invariant #4 boundary). 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). + - 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 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 -ever stealing an intended bank-move or OS-drag, and keeps a same-bank 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). +live pointer + focused region + client rect + **modifier state (Alt)** and calls it. This keeps +the reorder gesture from ever stealing an intended bank-move or OS-drag, and keeps a same-bank +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) -Model: the position carrier + gap semantics + reorder mutator + JSON round-trip/migration are -**pure** (in `bank_book`, CTest-covered to the bar of its existing round-trip). Layout: the -sparse-aware slot↔rect math + point→slot hit-test + the drag-disambiguation decision are **pure** -(extend `bank_grid`; mirror `mode_switch` / `drag_out::decideGesture`). Formatting: the -bars.beats and s.ms formatters are **pure**. Shell (DAW-bound): the reorder drag wiring + -drop-target highlight, the capture-time-signature stamp (F1) read on the capture path, and the -kit overlay/selection-border draw. The L1 kit draws; no palette/font decision re-opened. +Model: the position carrier + gap semantics + **reorder mutator + Alt-replace mutator** (the +latter reusing the existing index-only remove-from-bank semantics + pool-privilege guard) + JSON +round-trip/migration are **pure** (in `bank_book`, CTest-covered to the bar of its existing +round-trip). Layout: the sparse-aware slot↔rect math + point→slot hit-test + the +drag-disambiguation decision (**including the resolved-gesture result that drives the cursor cue, +and the Alt-over-occupied → replace resolution**) are **pure** (extend `bank_grid`; mirror +`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 diff --git a/PLAN.md b/PLAN.md index 9f456e1..6b40444 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 > existing projects — scoped honestly below, model work separated from draw work. -> **Forks for Daniel (surface before build — see CONTEXT.md §L7):** -> - **F1 — bars.beats source.** `Sample` already carries `captureTempo` (BPM at capture) and -> `lengthBeats`, but **no time-signature field.** bars.beats.subdivisions needs a -> time-signature reference. **Recommendation: a capture-time stamp** (add `captureTimeSigNum` -> /`captureTimeSigDenom` to `Sample`, stamped on the capture path) so the label is stable as -> the project tempo/meter later changes — matching the existing `captureTempo` stamp -> philosophy. This adds a **capture-path stamp beyond draw work** (its own checkbox below, -> flagged). Alternative (live project meter at draw time) is rejected: the label would drift -> under the card as the project changes, and a bank sample outlives the project state it was -> captured under. -> - **F2 — M9 un-deferral awareness.** M9 "slots" (capture-to-slot-N / insert-slot-N, -> MIDI-bindable, MPC-style) is **explicitly deferred (Daniel, 2026-07-26).** L7's sparse -> placement is a **partial overlap** — both need addressable grid positions. Building L7's -> sparse-slot model effectively lays the *position substrate* M9 would sit on, but L7 does -> **not** add slot-numbered capture/insert actions or MIDI bindings. Flagged so Daniel knows -> L7 un-defers the *coordinate model* portion of M9, not the *action* portion. If Daniel wants -> the slot substrate to be explicitly M9-shaped (numbered addressable slots vs. a plain -> gap-preserving ordinal), that is a fork to settle before the model checkbox. -> - **F3 — drag-disambiguation rule (proposed, Daniel to confirm).** The panel already has -> internal bank-move/copy drag and OS drag-out. In-grid reorder must coexist. **Proposed -> rule:** a drag whose drop lands **within the SAME bank's own grid** = **reorder-to-slot**; -> a drop on **a tab / the other region's bank** = **move/copy** (as today); a drag that -> **leaves the client rect** = **OS drag-out** (as today). One clean precedence: leave-client -> wins → else other-bank wins → else same-bank-grid = reorder. See CONTEXT.md §L7. +> **Forks — all SETTLED 2026-07-27 (see CONTEXT.md §L7 for full detail):** +> - **F1 — SETTLED: capture-time stamp.** Add `captureTimeSigNum` / `captureTimeSigDenom` to +> `Sample`, stamped on the capture path alongside the existing `captureTempo`; +> bars.beats.subdivisions renders from the stamped tempo + meter, stable under later project +> changes. Live-meter-at-draw-time rejected (would drift). The capture-path stamp checkbox +> below is **settled, not flagged.** +> - **F2 — SETTLED: plain interchangeable-slot substrate, NOT fixed MPC-style slots.** The +> carrier is a **per-bank id→slot map** (gap-preserving interchangeable slots): a slot is just a +> display position an occupant sits in — **no slot identities, no numbered/addressable slots, no +> slot actions, no MIDI-bindable slot numbers, no capture-to-slot-N.** The "plain vs. M9-shaped" +> sub-fork is closed (plain). M9 (capture-to-slot-N / insert-slot-N, MIDI-bindable, MPC-style) +> stays **explicitly deferred (Daniel, 2026-07-26);** the substrate still eases a future M9 +> revival but L7 adds no M9 actions or bindings. +> - **F3 — SETTLED + EXTENDED.** Drag precedence: leave-client → OS drag-out; else tab/other-bank +> → move/copy (Ctrl = copy); else same-bank grid → reorder-to-slot. **Extensions:** +> (a) **drop-result cursor cues** — the cursor indicates the drop result via REAPER-idiomatic +> special cursors (reorder / move / copy / OS-drag-out / replace); the shell sets it via +> Win32/SWELL `SetCursor`, but the *decision* of which cue applies stays in the pure gesture +> helper. (b) **Alt+drop-onto-occupied = REPLACE** — default occupied-slot drop stays +> insert-and-shift; holding Alt replaces the occupant (index-only removal, existing +> remove-from-bank semantics — file stays on disk, prune governs bytes, `hashReferencedElsewhere` +> handles last-reference). **Pool rule:** Alt+Replace in the un-evacuable pool is allowed only +> when the same pool-privilege guard that `remove` applies would permit removing that sample — +> 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 — 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 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 — -and hover / focus / drag-target remain distinct; 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. +and hover / focus / drag-target remain distinct; during any drag the cursor shows the correct +special cue (reorder within the bank, move/copy over a tab/other bank, OS drag-out past the client +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 `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. **(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 - ordinal or slot coordinate — settle the shape against F2). Decide the home: `bank_model` - is stated untouched by `bank_book`'s wrapping — position is a **per-bank display concern**, so - it belongs with the bank's membership, not on `Sample`. Candidate: a per-`Bank` ordered/keyed - position map in `bank_book` (id → slot), leaving `bank_model` / `Sample` untouched. Settle the - exact carrier at build. +- [ ] Add an explicit per-sample **display position** to the persisted bank state — a + **per-`Bank` id→slot map in `bank_book`** (gap-preserving interchangeable slots, per F2 + settled). `bank_model` / `Sample` stay untouched (position is a per-bank display concern, not a + `Sample` property — a copy of a sample into two banks may sit at different slots). **No slot + identities / numbered addressable slots** — a slot is just a position an occupant sits in. - [ ] JSON round-trip for the position data (`serialize`/`deserialize` remain lossless: `deserialize(serialize(x)) == x` including positions). - [ ] **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. - [ ] **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 - 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; - deterministic; unit-tested. -- [ ] Undo integration: a reorder is one Ctrl-Z (batched undo point in the actions/shell layer, - matching the existing bank-verb undo discipline). + deterministic; unit-tested. Drop-into-empty places there; **drop-onto-occupied (default, no + modifier) inserts-before-and-shifts-tail.** +- [ ] **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):** - [ ] **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 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 - tab / other bank = move/copy (unchanged); leave client = OS drag-out (unchanged). Precedence +- [ ] **Drag-disambiguation** (F3): drop within the same bank's grid = reorder-to-slot (empty = + 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 - 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 - call the reorder mutator; drop-target slot highlight during the drag (distinct from the existing - region/tab drop highlights). + call the reorder mutator (or, when Alt is held over an occupied slot, the **Alt-replace + 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):** - [ ] **Pure formatters, unit-tested:** `bars.beats.subdivisions` from `lengthSeconds` + `captureTempo` + capture-time signature (F1) and `seconds.milliseconds` from `lengthSeconds`. 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 - capture path (read the project meter at capture). Only needed if F1 resolves to the capture-time - stamp (recommended). Old samples with no stamp fall back gracefully in the formatter. + capture path (read the project meter at capture — verify `TimeMap_GetTimeSigAtTime` / equivalent + 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 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 @@ -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 and track-tag action ids confirmed; active-mode read confirmed via the same `view().activeModeId()` the footer toggle uses. -- **L7 model + tempo surface (L7)** — settle at L7 build/spec-review: (1) **position carrier** — - where the per-sample display position lives (candidate: a per-`Bank` id→slot map in `bank_book`, - keeping `bank_model`/`Sample` untouched per CLAUDE.md); (2) **F1 bars.beats source** — confirm - the capture-time signature stamp with Daniel (recommended) vs. live project meter; the stamp adds - a capture-path write (its own flagged checkbox); (3) **F2 M9 overlap** — confirm L7's sparse-slot - substrate is not mis-scoped as M9's slot *actions*; (4) **F3 drag rule** — confirm the - same-bank-grid = reorder / other-bank = move-copy / leave-client = OS-drag precedence. Verify the - REAPER meter-read API for the stamp (`TimeMap_GetTimeSigAtTime` or equivalent) against the SDK at - build. +- **L7 model + tempo surface (L7)** — forks F1/F2/F3 **all settled 2026-07-27** (see the §L7 + forks block above and CONTEXT.md §L7): position carrier = per-`Bank` id→slot map in `bank_book` + (plain interchangeable substrate, `bank_model`/`Sample` untouched); F1 = capture-time signature + stamp; F3 = drag precedence + REAPER-idiomatic cursor cues + Alt-replace-over-occupied with the + stated pool guard. **Remaining build-time verification (SDK/API, not product decisions):** + (1) the REAPER meter-read API for the F1 stamp (`TimeMap_GetTimeSigAtTime` or equivalent) — + verify signature/argument order against the SDK; (2) the Win32/SWELL `SetCursor` path and cursor + resource loading/synthesis for the drop-result cues — confirm the cursor idiom against SWELL and + 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. ---