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
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