docs: collapse Phase Ψ's landed track specs, and record Ψ-W3 — the wave a review finding opened

This commit is contained in:
2026-08-01 23:32:25 -04:00
parent a3a9c91256
commit 0f870b1337
+114 -605
View File
@@ -2083,6 +2083,19 @@ provenance, cited throughout as Ψ.1–Ψ.7:
| Ψ.1 | Ψ-W2-T1 | `ppsi-w2-t1-capture-naming` | | Ψ.1 | Ψ-W2-T1 | `ppsi-w2-t1-capture-naming` |
| Ψ.6 | Ψ-W2-T2 | `ppsi-w2-t2-mono-collapse` | | Ψ.6 | Ψ-W2-T2 | `ppsi-w2-t2-mono-collapse` |
**Ψ-W3 is not one of the seven** — opened mid-phase, after Ψ-W2's review surfaced that
the track scope carried the same multi-track stem-collapse hole Ψ-W1-T1 had just closed
for item scope. It consolidates none of the original seven and carries no `Ψ-item` row
above; see Ψ-W3 below.
**All three waves have landed — Phase Ψ is complete.** W1 and W2 each carry their own
landed notes below; W3 carries its own too. See `docs/COMPLETED.md` for every track's
full narrative. **None of the seven tracks is DAW-verified** — all are code-complete and
unit-tested, several resting on a shared unverified inference about how REAPER's
selected-tracks render source interacts with custom time bounds, which Ψ-W3-T1's
refusal now also rests on; each track's DAW-verification obligation is restated inline
below.
Ψ.2 and Ψ.3 share one track deliberately: they share one chokepoint — `applyMode`, the Ψ.2 and Ψ.3 share one track deliberately: they share one chokepoint — `applyMode`, the
sole mode mutator (`shell/view/view.cpp:380-469`) — and one discriminator sole mode mutator (`shell/view/view.cpp:380-469`) — and one discriminator
(`targetModeId != model.activeModeId()`, the test that distinguishes a real switch from a (`targetModeId != model.activeModeId()`, the test that distinguishes a real switch from a
@@ -2094,9 +2107,8 @@ exception — Ψ-W2-T2 touches `shell/instrument/processor_reload.cpp` for a sta
and an index/file consistency check, and that touch is bounded to the minimum in its and an index/file consistency check, and that touch is bounded to the minimum in its
surface boundary precisely because Γ is live in that directory. surface boundary precisely because Γ is live in that directory.
**Three invariant amendments are DELIVERABLES of this phase, not asides.** Each is **Three invariant amendments were DELIVERABLES of this phase, not asides.** Each landed
scheduled in — and an acceptance criterion of — its owning track. This spec schedules in its owning track, as an acceptance criterion of that track:
them; the implementing track performs them:
1. **The never-touch-solo rule** (Ψ-W1-T2): `src/shell/view/CLAUDE.md:14-17`, 1. **The never-touch-solo rule** (Ψ-W1-T2): `src/shell/view/CLAUDE.md:14-17`,
`src/core/view/CLAUDE.md:10`, and `docs/product/design-view.md:160-165` + `:588-592`. `src/core/view/CLAUDE.md:10`, and `docs/product/design-view.md:160-165` + `:588-592`.
@@ -2123,416 +2135,68 @@ internal-drag path").
### Ψ-W1 — Exact bounds, disciplined switches, reachable actions, resolved drops ### Ψ-W1 — Exact bounds, disciplined switches, reachable actions, resolved drops
**Depends on:** nothing in this phase. **Four tracks, disjoint by surface:** **Depends on:** nothing in this phase.
| Track | Owns |
|---|---|
| **T1** `capture-range-exactness` | `core/capture/render_settings`, `shell/capture/capture.cpp`'s render-configuration block, `shell/capture/scope_resolve` (source-mode selection only), `tests/test_render_settings.cpp` — **plus, as landed:** the new pure `core/capture/render_window` + `core/capture/track_topology`, the new shells `shell/capture/render_selection` + `shell/capture/render_isolation`, and `renderOffline`'s guard block in `shell/capture/capture_orchestrator.cpp` (the one seam a fresh capture and a recipe replay both cross, so the refusal and the transient guards had to live there rather than in `scope_resolve`) |
| **T2** `mode-switch-discipline` | `core/view/view_mode_model` (solo cache), `shell/view/view.cpp` (`applyMode` seams + WANT block), `shell/actions/design_view_actions.cpp` (refusal feedback), `shell/panel/panel_input.cpp` **footer mode-segment block only** (`:300-309`), the mode segment's disabled state in `core/ui/footer_bar` + `shell/panel/panel_render.cpp` |
| **T3** `media-explorer-section` | `shell/actions/ingest.cpp` (register/dispatch/unregister), **the registration block in `src/app/main.cpp`** (the `hookcommand2` hook + unload mirror), the root-`CLAUDE.md` contract amendment |
| **T4** `drop-target-resolution` | `core/ui/drag_out`, `core/wire/instrument_drop`, `shell/panel/panel_drag.cpp`, `shell/actions/instrument_drop_win.cpp`, `shell/actions/drag_out_win.cpp` (hand-off timing), `tests/test_drag_out.cpp` |
**Why these four are parallel.** T1 is capture core/shell; T2 is view core/shell plus one
fenced block of `panel_input.cpp`; T3 is action registration; T4 is the drag chain. No
two tracks own the same function anywhere.
**Two shared-file adjacencies, named rather than discovered at merge.**
(a) `shell/panel/panel_input.cpp`: T2 owns the footer mode-segment block (`:300-309`)
only; T4 may touch the drag-arm block (`:382-402`) only. Separate blocks — textual
adjacency, not semantic contention; whichever lands second rebases.
(b) **`src/app/main.cpp` is T3's exclusively.** T4 must not touch `main.cpp` — its whole
redesign lives in the drag chain and registers nothing. T2 also does not touch it: the
playback gate lives inside `applyMode` conditioned on the real-switch discriminator, so
`main.cpp:173`'s project-load reapply passes through unchanged.
#### Ψ-W1-T1 — `capture-range-exactness` #### Ψ-W1-T1 — `capture-range-exactness`
**Goal.** A capture over a time selection or razor area on a source item substantially **Landed** — see `docs/COMPLETED.md` for the full narrative. A ranged item capture now
longer than the selection produces exactly the requested range — both scopes, no renders exactly the requested window instead of the whole item, by re-sourcing through
whole-item widening. (Ψ.7) the selected-tracks render when the item extent does not already print the window.
**Deviation:** the spec named two candidate architectures (re-source vs. render-then-
**What is already correct, so the fix does not wander.** Range resolution is correct end trim); the engineer shipped a conditional form of the re-source candidate — the
to end: `scope_resolve.cpp:100-112` (`resolveRange`) → razor union full-extent case runs literally unchanged code, keeping the byte-identity regression
(`render_settings.cpp:135-170`) or `GetSet_LoopTimeRange` (`scope_resolve.cpp:35-40`), floor structural and the fix cheap to revert if the override inference proves wrong.
passed verbatim (`capture_orchestrator.cpp:221-222`), landed as Also landed: a transient isolation guard (cutting `B_MAINSEND` on direct folder
`RENDER_BOUNDSFLAG=0` + exact `RENDER_STARTPOS`/`RENDER_ENDPOS` children, muting receives) so an item capture stays true to item scope, and a
(`capture.cpp:320-322`). There is no item-bounds fallback in `RunCapture`. **The widening post-render frame-count gate (±1 tolerance, tail-None only) that refuses and self-cleans
decision point is the source-mode bit**: item scope maps to a widened render. New modules `core/capture/render_window`, `core/capture/track_topology`,
`kRenderSelItems (&32) | kRenderSingleFile` (`render_settings.cpp:82-86`), and the repo's `shell/capture/render_selection`, `shell/capture/render_isolation`. The whole fix rests
own comments treat `&32` as item-extent-driven (`capture_batch.cpp:64-66`, `:98-99` — on the unverified inference that REAPER's selected-tracks render source overrides
the entire reason batch transiently selects one item per render). The working inference — custom time bounds — Ψ-W3-T1 (below) now also depends on it. **DAW-verification
**unverified without a DAW** — is that REAPER's "selected media items" render source obligation, unmet:** the four-cell scope × selection-type matrix over a source item
overrides the custom time bounds. Track scope uses `&128` (selected tracks via master), a substantially longer than the selection (tail None), plus one razor-union case — none of
normal time-bounded render, and **no code path was found that widens it**. it run in a live REAPER session yet.
**Behavior.**
- **The track opens with a DAW repro matrix, before any code change** `[verify]`:
item scope × {time selection, razor} and track scope × {time selection, razor}, over a
source item substantially longer than the selection, **tail mode None**. This confirms
the `&32`-overrides-bounds inference, and disambiguates Ψ.7's "item/track both" claim —
the candidates are: the reporter saw only the item scope; or a non-`None` panel tail
mode was active (`capture_orchestrator.cpp:217`; Auto adds an 8 s window,
`render_settings.cpp:28-37`). If track scope reproduces with tail None, the analysis
above is wrong and the track says so before proceeding.
- **The fix architecture is a design call at implementation review** `[propose]`, with
the two candidates named now:
- **(a) Re-source the ranged item capture.** When item scope carries an explicit range,
render time-bounded (e.g. selected-track source `&128` over the item's own track,
with the item-scope FX-bypass plan unchanged — it already neutralizes the track's own
track-FX chain and everything above). Semantic edge to resolve: another item on the
same track overlapping the range would now be audible in the capture, where `&32`
excluded it.
- **(b) Render-then-trim.** Keep `&32` (only the selected item's audio, REAPER's own
semantics) and trim the rendered file to the requested range afterward with the pure
`wav_codec` tools, sample-exactly (offset = requested start rendered start at the
file's sample rate). Precedent: `trimAutoTailInPlace`
(`capture_realtime_finalize.cpp:55-125`). Cost: renders more than needed; requires
knowing the rendered file's start time exactly.
Whichever candidate lands, the choice is judged against the acceptance criteria below,
not against convenience.
- **Ripple surfaces if the item-scope source mode changes**, enumerated so none is
discovered late: `capture_batch.cpp:98-99`'s select-one mechanism, item-scope FX
fingerprinting reading `CountSelectedMediaItems` (`scope_resolve.cpp:199-207`), and
`core/model/provenance` recipes storing `sourceMode` as an int
(`scope_resolve.cpp:186`) — a recorded recipe must replay to the same audio.
- **Batch semantics are deliberately different and stay so:** `RunBatchCaptureItems` is
one sample per item at item extent (`capture_batch.cpp:177-179`) — that is its meaning,
not this defect. Stated so nobody "fixes" it.
**Acceptance criteria.**
- **The bounds equality, stated as a number:** the captured file's frame count equals the
requested range's duration at the project sample rate — exactly, no rounding — for
BOTH scopes, with both a time selection and a razor area, over a source item
substantially longer than the selection, tail None. `[verify — DAW]`
- The null test holds for a ranged item capture: re-inserted at the range start, it nulls
against the source over the range. `[verify — DAW]`
- Bit-identical repeats hold for the ranged capture.
- Full-item captures (no time selection/razor, or bounds equal to the item) are
byte-identical to today's output.
- Recapture-from-source of a ranged capture reproduces it (the provenance recipe stays
truthful under the chosen fix).
- `tests/test_render_settings.cpp` pins the chosen mapping (today `:44-47` asserts only
that the `&32` bit is chosen — never its bounds interaction); any new pure trim/window
arithmetic lands in `core/capture` with unit tests.
- **DAW-verification obligation:** Daniel observes the four-cell matrix above, plus one
razor-union case (two disjoint razor areas — the union rule at
`render_settings.cpp:135-170` is bounds-driven and must not regress).
- Root `CLAUDE.md:208`'s **"Exact bounds"** invariant needs no amendment — this track
makes the code honor it; the invariant was always the spec.
**Open questions.** `[verify]` the override inference and the track-scope repro (the
matrix above). `[propose]` fix candidate (a) vs (b), including (a)'s overlapping-item
semantic edge and (b)'s rendered-start-time derivation.
#### Ψ-W1-T2 — `mode-switch-discipline` #### Ψ-W1-T2 — `mode-switch-discipline`
**Goal.** Switching the active mode caches and clears the outgoing mode's solo state and **Landed** — see `docs/COMPLETED.md` for the full narrative. Per-mode SOLO surfaces now
restores the incoming mode's — disjoint solo surfaces per mode — and the switch itself is cache, clear, and restore across a Design/Arrange switch, with the switch itself
refused, visibly, while the transport is playing or recording. (Ψ.2 + Ψ.3) refused, visibly, while the transport is playing or recording. The never-touch-solo
invariant — stated three times (`src/shell/view/CLAUDE.md`, `src/core/view/CLAUDE.md`,
**The invariant amendment comes first, because without it this track is a breach.** The `docs/product/design-view.md`) — is amended, in this track, to the snapshot sense of
never-touch-solo rule is stated three times: `src/shell/view/CLAUDE.md:14-17` ("Never non-destructive: solo is cached per mode on a real switch and restored verbatim, not
touches master or `B_MUTE`/`I_SOLO`… User mute/solo survives every toggle untouched"), left untouched absolutely the way `B_MUTE` and the master track are. Also closed: a
`src/core/view/CLAUDE.md:10`, and `docs/product/design-view.md:160-165` (framed as the pre-existing bug where a footer mode-segment click never persisted view state. New
analog of capture's non-destructive invariant) + `:588-592`. **The amended form:** the `core/view/solo_cache`, `shell/view/view_solo`. **DAW-verification obligation, unmet:**
tool never *loses* the user's solo state — solo is cached per mode on a real switch and the disjoint-surface solo matrix, the playback-gated refusal (playing and recording),
restored verbatim on return, the snapshot sense of non-destructive, exactly as the and the panel-persist case none run in a live REAPER session yet.
park/restore snapshots already treat visibility and FX state. `B_MUTE` stays untouched
absolutely; the master track stays untouched absolutely (and survives for free —
`handleByGuid` is built from `GetTrack`'s index space, which excludes master,
`view.cpp:90-91`). All three files are amended by this track.
**Surface boundary — owns:** `core/view/view_mode_model` (the solo cache: storage,
serialize/deserialize, reconcile participation — mirroring the existing `snapshots_` map,
`view_mode_model.h:284-358`), `shell/view/view.cpp` (the `applyMode` seams and its
`REAPERAPI_WANT` block, `:21-41`), `shell/actions/design_view_actions.cpp` (refusal
feedback on the action path), `shell/panel/panel_input.cpp`'s footer mode-segment block
ONLY (`:300-309`), the segment's disabled state in `core/ui/footer_bar` (pure predicate,
transport bool passed IN from the shell) and its paint in `shell/panel/panel_render.cpp`
(`:110-125` region), the three invariant-amendment files, `tests/test_view_mode_model.cpp`.
**Must not touch:** `panel_drag.cpp` (T4's), `src/app/main.cpp` (the gate's discriminator
makes the project-load reapply pass through unchanged), `B_MUTE`, master, and the
park/restore planner's existing semantics.
**Behavior.**
- **The discriminator is the hinge for both halves.** `applyMode` is also the reapply
path — called with `targetModeId == activeModeId()` from `reapplyActiveMode`
(`design_view_actions.cpp:106-108`, reached from tag/untag/show-both at
`:187,197,206`) and from project load (`main.cpp:173`). A real switch is
`targetModeId != model.activeModeId()`. **Only a real switch caches/clears/restores
solo, and only a real switch is playback-gated** — reapply during playback (tagging,
project load) keeps working, and there is no cache/clear/restore flicker on reapply.
- **Solo cache semantics.** On a real switch: read `I_SOLO` for every track in the live
enumeration (`handleByGuid`, keyed by GUID — all non-master tracks, not just managed
leaves: a mode's solo surface is the whole project as seen in that mode); store the
non-zero values into the model keyed by the OUTGOING mode id (`model.activeModeId()`
is still the outgoing mode at the capture seam); write `I_SOLO = 0` on every track that
had it set; after `setActiveMode` (`view.cpp:455`), restore the INCOMING mode's cached
entries verbatim and consume them (clear-on-restore, mirroring
`storeSnapshot`/`clearSnapshot`). **Values are cached and restored verbatim as the raw
`I_SOLO` int — never collapsed to a boolean** (solo-in-place and safe-solo variants
survive the round trip).
- **Seams in `applyMode`:** capture after `handleByGuid` is populated (`:386`) and before
`Undo_BeginBlock2` (`:399`); the clear and restore writes both ride the existing single
undo block (clear at its head, restore after `setActiveMode` `:455` and before
`TrackList_AdjustWindows` `:460`) — one mode toggle stays one Ctrl-Z.
- **Storage is pure, shell touches only the API pair.** The cache lives in
`ViewModeModel` beside `snapshots_` with the same `deserialize(serialize(x)) == x`
contract (`view_mode_model.h:347-350`); only the
`GetMediaTrackInfo_Value`/`SetMediaTrackInfo_Value` pair lives in `view.cpp`, exactly
as `snapshotTrack` (`:121-134`) does. `core/` includes no REAPER headers.
- **Reconcile participation — decided: the solo cache prunes on dead GUIDs**, like
snapshots and unlike membership (`view.cpp:389-395`). The hazard it removes: a stale
entry restoring onto a reused GUID. A pruned entry is simply lost solo state for a
track that no longer exists — correct.
- **Persistence rides the one blob.** `ViewModeModel::serialize()` →
`SetProjExtState(proj, "reasampler", "view_state")` (`ext_state_io.cpp:156-158`, key
`ext_keys.h:31`). `parseModel` skips unknown keys (`view_mode_model.cpp:603`), so the
new key is backward-safe: an older build reading newer state drops the solo cache
silently and nothing else breaks. Stated, accepted.
- **The playback gate.** Inside `applyMode`, when the discriminator says real switch and
`GetPlayStateEx(proj) & (1|4)` (bit 1 playing, bit 4 recording — the precedent is
`capture_realtime_shell.cpp:284`, project-scoped `*Ex` variants deliberately,
comment `:192`), refuse before touching anything: return `false`, no partial apply
(the same fail-closed shape as the mode-exists guard at `:381`). Add
`REAPERAPI_WANT_GetPlayStateEx` to `view.cpp`'s WANT block.
- **The refusal is visible, not a silent no-op.** The footer `[Arrange|Design]` segment
renders a disabled state while the transport runs, following the one existing disabled
precedent end to end: pure predicate (`mode_enable`-style, transport bool passed IN so
`core/ui` stays REAPER-free) → layout/paint state → no-op click. The action path's
refusal (`doToggleMode`/`doActivateMode`) reads `applyMode`'s `false` and skips
persist/invalidate.
- **The panel's direct-call divergence is closed in this track — decided.** The footer
segment click calls `applyMode` directly (`panel_input.cpp:300-309`, passing `nullptr`
project), skipping the `persistViewState()` + `bankPanelInvalidate()` the action path
does (`design_view_actions.cpp:165-180`) — a pre-existing defect: a panel-initiated
switch does not persist. The segment click now routes through `Main_OnCommand` of the
activate actions, exactly as the bottom-bar tag buttons already do
(`panel_input.cpp:74-75`). One path, one gate, one persist.
**Acceptance criteria.**
- **The disjoint-surface matrix, DAW-observed:** solo two tracks in Arrange; switch to
Design → no track is soloed; solo a third track in Design; switch back → exactly the
two Arrange solos are restored (raw `I_SOLO` values verbatim, including solo-in-place)
and the Design solo is gone; switch forward again → the Design solo is restored.
- Reapply paths (tag/untag/show-both, project load) neither clear nor flash solo state —
before or after this track, during playback or stopped.
- With the transport playing or recording: the switch is refused on EVERY path (action,
panel segment), the segment visibly renders disabled, and project state is untouched by
the refused attempt. Stopped → the switch works. Recording gates identically to
playing.
- A panel-initiated switch now persists (the divergence closure): switch via the footer
segment, save, reload → the project reopens in the switched mode.
- Solo cache round-trips save/reload: save mid-disjunction, reload → the inactive mode's
cached solos restore on the next switch.
- One mode toggle remains one Ctrl-Z; `B_MUTE` is never written; master is never touched.
- The three invariant-amendment files land amended, in this track.
- Pure model changes covered in `tests/test_view_mode_model.cpp` (store/consume,
serialize round-trip, reconcile pruning); the enable predicate is pure and tested.
- **DAW-verification obligation:** Daniel runs the solo matrix, the playback-gated
refusal (playing AND recording), and the panel-persist case.
**Open questions.** `[verify]` `I_SOLO` is settable via `SetMediaTrackInfo_Value` and its
value domain — against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h`, per root
`CLAUDE.md`'s API-verification rule (settability is inference today). `[verify]` adding
`REAPERAPI_WANT_GetPlayStateEx` to `view.cpp` alone compiles (`main.cpp:15` does not
define `REAPERAPI_MINIMAL`, so it should — inference, one compile answers it).
`[propose]` the repaint trigger for the disabled segment (the panel must notice transport
transitions; lean: poll play state in the panel's existing timer tick and invalidate on
change). `[propose]` restore-onto-parked-track edge: a track soloed in mode B, then
re-tagged so it is parked when B next activates, would on verbatim restore solo a
silent track and mute the mix — lean: restore skips tracks parked in the incoming mode
and drops those entries, with the alternative (retain until the track re-enters) named
and rejected as zombie state.
#### Ψ-W1-T3 — `media-explorer-section` #### Ψ-W1-T3 — `media-explorer-section`
**Goal.** The Media-Explorer import action appears in the Media Explorer action section, **Landed** — see `docs/COMPLETED.md` for the full narrative. The Media Explorer import
so it can be bound and placed on the Media Explorer toolbar. (Ψ.4) action now registers into REAPER's Media Explorer action section (32063) via
`custom_action` + `hookcommand2`, alongside its existing Main-section entry so existing
**Why it lands in Main today (SDK-verified):** the action registers via `"gaccel"` keybindings survive — a second FOREVER-STABLE id, `INGEST_IMPORT_MEDIA_EXPLORER_MX`,
(`ingest.cpp:457-468`), and `gaccel_register_t` registers into the main keyboard section minted per channel. Root `CLAUDE.md`'s "REAPER extension contract" is amended, in this
only — the struct has no section field (`vendor/reaper-sdk/sdk/reaper_plugin.h:1106-1117`). track, with the second, non-main registration mechanism beside the original four-step
**The supported mechanism (SDK-verified):** `custom_action_register_t` main-section pattern. **DAW-verification obligation, unmet:** adding the action to the
(`reaper_plugin.h:1090-1103`) — `{uniqueSectionId, idStr, name, extra}`, with **Media Media Explorer toolbar and firing it from there; confirming the Main-section binding
Explorer = section 32063** (`:1099`, corroborated `reaper_plugin_functions.h:3467`); still fires; and confirming unload/reload does not leak a duplicate Media Explorer entry
`Register("custom_action", &ca)` returns the command id or 0; `idStr` must be unique (the `-custom_action` unload mirror is unconfirmed against the SDK header) — none run in
**across all sections** (`:1100`). Dispatch for non-main sections MUST come from a live REAPER session yet.
`"hookcommand2"` (`reaper_plugin.h:212-221`) — `"hookcommand"` runs only for the main
section (`:200-205`), and `main.cpp:309` registers only `"hookcommand"` today.
`custom_action_register_t` carries no `ACCEL`, so the non-Main entry ships no default
keybinding — acceptable; toolbar placement is the ask.
**Settled, not a fork: the action registers in BOTH Main and Media Explorer.** Ψ.4 asks
for reach, not relocation, and a bare move would orphan any existing Main-section
keybinding — a half-fix. Because `idStr` is unique across all sections, the Media
Explorer registration **mints a second FOREVER-STABLE command-id suffix:**
**`INGEST_IMPORT_MEDIA_EXPLORER_MX`**, composed per channel via `channelCommandId` like
every other id (`core/version/app_version.cpp:43-47,84-86`) — a new permanent entry in
each channel's id family, never to change after shipping. The existing
`INGEST_IMPORT_MEDIA_EXPLORER` string is preserved unchanged for the Main entry, so root
`CLAUDE.md:180-183`'s never-change contract is honored literally. Both registrations
share the one handler (`doImportFromMediaExplorer`, `ingest.cpp:317-395`).
**Surface boundary — owns:** `shell/actions/ingest.cpp` (the second registration, the
`hookcommand2` claim, the `-custom_action` unload mirror beside the existing mirrors at
`:476-481`), **the registration block in `src/app/main.cpp`** (registering the
`"hookcommand2"` hook beside `"hookcommand"` at `:309`, and its unload mirror at the
`rec == nullptr` path), and the root-`CLAUDE.md` §"REAPER extension contract" amendment
(the second registration mechanism, documented beside the 4-step main-section pattern).
**Must not touch:** any other action family's registration, `panel_*`, `capture*`. T4
must not touch `main.cpp`; this track is why.
**Behavior.**
- Main-section registration byte-identical to today: same id string, same gaccel, same
keybindings surviving.
- Second registration: `custom_action_register_t{32063, channelCommandId(new suffix),
channelActionName(same display phrase — decided: identical phrase in both sections,
for findability), nullptr}` via `Register("custom_action", ...)`. A 0 return is
tolerated gracefully (the Main entry still works; a console note, not a failure).
- `hookcommand2` claims ONLY the new MX command id and returns `false` otherwise —
`hookcommand2` fires for every section including Main, and the Main id stays claimed by
the existing `hookcommand` path (`ingest.cpp:470-474` via `main.cpp:222-230`); the two
ids differ, so no double dispatch is possible. Stated as a criterion, not an accident.
- Unload mirrors everything: `-custom_action` with the same struct/pointer, alongside the
existing `-gaccel`/`-command_id`.
- Beta channel forks both ids automatically through `channelCommandId` — each channel
gains exactly one new permanent id.
**Acceptance criteria.**
- The Actions list, section "Media Explorer", shows the channel-prefixed action; it can
be added to the Media Explorer toolbar; firing it from that toolbar imports the last
played Media Explorer file into a ReaSampler on the selected track — identical behavior
to the Main-section entry. `[verify — DAW]`
- The Main-section entry is unchanged: same command id, existing keybindings intact.
- Extension unload unregisters both entries cleanly; reload re-registers both.
- The new suffix `INGEST_IMPORT_MEDIA_EXPLORER_MX` is recorded as FOREVER-STABLE, per
channel, in the same breath as the registration lands.
- Root `CLAUDE.md`'s "REAPER extension contract" section gains the non-main mechanism
(`custom_action` / `hookcommand2` / `-custom_action` mirror) — a deliverable of this
track.
- **DAW-verification obligation:** Daniel adds the action to the Media Explorer toolbar
and imports from it; also confirms the Main-section binding still fires. Also: unload,
reload, confirm no duplicate Media Explorer entry — the `-custom_action` unload mirror
is unconfirmed against the SDK header (root `CLAUDE.md` §"REAPER extension contract",
`[verify — DAW]`), and if unsupported the entry leaks across a reload.
**Open questions.** `[verify]` re-verify `custom_action_register_t` and `hookcommand2`
argument order/types against the SDK headers at implementation time (root `CLAUDE.md`
rule; the section ids and struct shapes above are already header-verified, the rule
applies regardless). None classified `[propose]` — the ruling above is settled.
#### Ψ-W1-T4 — `drop-target-resolution` #### Ψ-W1-T4 — `drop-target-resolution`
**Goal.** Dragging cards out of the panel resolves its target from what is actually under **Landed** — see `docs/COMPLETED.md` for the full narrative. The drag-out gesture is now
the cursor — continuously, reversibly, with a defined outcome and a visible cue for every a per-move, stateless law: target class resolves from what is under the cursor on every
surface, and no silent no-op release anywhere. (Ψ.5) move, every class transition is reversible until release or until the pointer leaves
REAPER, and the OS hand-off is reserved for leaving REAPER entirely — the whole TCP/MCP
**The root causes, so the redesign is judged against them.** The current chain locks a is now an instrument-drop hotspot, and a single-card arrange drop lands a timeline item
drag's fate at its first processed move outside the client rect at the pointer's track and time. New `shell/actions/arrange_drop_win`.
(`core/ui/drag_out.cpp:18-26`): a single-card drag over ANY REAPER surface — **DAW-verification obligation, unmet:** the full target-class matrix (single- and
`GetThingFromPoint` returns non-empty info for the arrange too multi-card), reversibility across a drag that crosses the arrange en route to an FX
(`instrument_drop_win.cpp:84-86`) — locks to `InstrumentDrop` and returns before the OS window, drag-speed independence, and the narrow-TCP case — none run in a live REAPER
hand-off is ever evaluated (`panel_drag.cpp:285-294`); over the arrange the info string session yet.
fails the FX-hotspot rule, so release falls to a **silent no-op** (root cause A — the
verified headline: single-card arrange drops do nothing, while multi-card drags skip the
FX resolve (`:276`) and work via OS drag, which is exactly Ψ.5's "sometimes"). The
`OsDrag` branch is irreversible — `ReleaseCapture` + `resetDragState` + modal
`DoDragDrop` (`:299-335`) — and `WM_MOUSEMOVE` coalescing makes the deciding exit pixel
vary with drag speed (root cause B). The FX "hotspot" is only the FX-button glyph family
(`fx_*`/`tcp.fx*`/`mcp.fx*`, `core/wire/instrument_drop.cpp:90-95`) — TCP body/name, or
a TCP too narrow to draw the button, yields `"tcp"` and a cue-less no-op (root cause C).
Multi-card drags can never instrument-drop (root cause D). One unresolvable hand-off
evaluation sets `dragOsHandoffBlocked` for the remainder of the drag (`:320`, cleared
only in `resetDragState` — root cause E). Coordinate spaces and DPI are NOT causes
(verified); bank-to-bank never leaves the client and is CTest-covered
(`tests/test_drag_out.cpp:33-112`) — consistent with "dragging between banks seems
fine", and it must stay byte-identical.
**The redesign is a gesture law, not a patch.** The law: **the target class is resolved
from what is under the cursor on every move, every class transition is reversible until
release or until the pointer leaves REAPER entirely, and the OS hand-off is reserved for
leaving REAPER** — every REAPER-internal target is executed natively on release. The
minimal patch (re-evaluating the hotspot per move but keeping the early irreversible
`OsDrag` fork) is named and rejected: it re-creates root cause B — a drag that crosses
the arrange en route to an FX button would still lose the instrument drop forever.
**The class enumeration and its defined outcomes (single-card / multi-card):**
| Under the cursor | Single card | Multi card |
|---|---|---|
| Inside the panel client | Internal drag (unchanged, byte-identical) | Internal drag (unchanged) |
| FX hotspot (`fx_*` window, or track panel — see below) | **Instrument drop** (existing `.vstpreset` path) | **Refuse-with-cue** (no instrument drop for a multi payload — root cause D becomes a defined, cued outcome) |
| TCP/MCP, whole panel (not just the FX glyph; `*.fxembed` still excluded) | **Instrument drop — decided:** the whole track panel is the hotspot. Root cause C IS the FX-glyph-only rule; a capture dropped on a track's panel means "sampler on this track." The rejected alternative — TCP drop = item at edit cursor — is silent-placement-adjacent and has no time coordinate; the arrange owns placement. | Refuse-with-cue |
| Arrange | **Timeline item at the pointer's track/time** — native insertion on release | Native insertion, shape at review (see open questions) |
| Elsewhere in REAPER (ruler, transport, docker chrome) | Refuse-with-cue | Refuse-with-cue |
| Outside REAPER entirely | OS drag-out (`DoDragDrop`, CF_HDROP, copy-only — unchanged mechanics, new trigger condition) | OS drag-out |
- **Every release either performs the resolved action or visibly refuses** — the cue
(cursor via the existing `applyDragCursor` precedent, plus the panel's drag paint
state) tracks the resolved class continuously, so "will this work" is visible before
release, and no cell in the matrix is a silent nothing.
- **The blocked flag is retired as a drag-lifetime latch** (root cause E): an
unresolvable evaluation refuses that evaluation, not the rest of the drag.
- **The arrange outcome does not violate the load-bearing principle, and the spec says so
where a reviewer will look:** the item is placed because the USER dragged it to that
spot — user-initiated placement, the same class of act as `RunInsertSelected`, not a
capture auto-insert. Root `CLAUDE.md`'s "capture and placement are separate acts" is
about capture never placing; a deliberate drop is placement on demand.
- **The pure decision stays pure.** The class enumeration, its precedence, and the
single/multi split land in `core/ui/drag_out` (the law) +
`core/wire/instrument_drop` (the info-string classification), CTest-covered over the
full matrix; the shell reads live pointer/window state and executes outcomes, exactly
the discipline `card_drag` already follows.
- **Hot-path discipline:** the inside-client path stays free of SDK hit-tests
(`GetThingFromPoint` is evaluated only outside the client rect, as today —
`panel_drag.cpp:273-274`); per-move resolution outside the client now runs for multi
payloads too, which is new but still per-mouse-move cold.
**Surface boundary — owns:** `core/ui/drag_out` (the gesture law, rewritten),
`core/wire/instrument_drop` (classification), `shell/panel/panel_drag.cpp` (routing +
release dispatch), `shell/actions/instrument_drop_win.cpp` (hit-test shell),
`shell/actions/drag_out_win.cpp` (hand-off trigger timing), `tests/test_drag_out.cpp`.
May touch `panel_input.cpp`'s drag-arm block (`:382-402`) only — T2's footer block is
fenced. **Must not touch:** `src/app/main.cpp` (T3's — this track registers nothing),
`core/ui/card_drag` and the in-grid reorder/replace semantics (bank-to-bank stays
byte-identical), the capture pillar, the insert action's own semantics.
**Acceptance criteria.**
- The full matrix above, DAW-observed, single AND multi: every cell produces its defined
outcome with its cue, and in particular — a single-card drop onto the arrange lands an
item at the pointer's track and time (the headline defect); a single-card drop onto a
track panel (not just the FX glyph) loads the instrument; a multi-card drop onto the
arrange still lands items; every refuse cell visibly refuses.
- **Reversibility:** leave the client, cross the arrange, reach an FX window →
instrument drop still available; return into the panel client → internal drag resumes.
One unresolvable evaluation does not change any later evaluation's outcome.
- **Drag speed does not change outcomes:** fast flicks and slow drags to the same release
point resolve identically (the coalescing hazard is designed out with the early lock,
not mitigated).
- Bank-to-bank drags are byte-identical to today; `tests/test_drag_out.cpp:33-112` (or
their successors) stay green with unchanged semantics.
- The pure law's CTest matrix covers every class × single/multi combination, including
the `*.fxembed` exclusions and the null-track/non-empty-info cases the SDK documents
for `GetThingFromPoint` (`reaper_plugin_functions.h:3440-3446`).
- No new SDK hit-test inside the client rect.
- **DAW-verification obligation:** Daniel runs the matrix, explicitly including the
narrow-TCP case (root cause C's trigger), fast-flick drags, and a drag that crosses the
arrange before reaching an FX window.
**Open questions.** `[verify]` pointer→(track, time) for the native arrange insertion —
candidate API `GetSet_ArrangeView2` (pixel-column → time mapping) plus
`GetThingFromPoint`'s track; verify signatures and behavior against the SDK header
before building on them; if no exact pixel→time mapping exists, the arrange outcome's
mechanism (not its existence) is re-proposed. `[verify]` what REAPER does with a CF_HDROP
drop re-entering its windows during our `DoDragDrop` (the accepted residual once the
pointer has left REAPER and returns mid-modal-loop — confirm it is REAPER's own file
import, then state it). `[propose]` the multi-card arrange insertion shape (lean: one
item per file mirroring REAPER's own multi-file drop convention; alternative: retain the
OS hand-off for the multi-arrange cell only — costs a second delivery mechanism and a
modal fork, named to be rejected on uniformity unless the native shape fails
verification). `[propose]` the exact refuse-with-cue rendering (cursor-only vs cursor +
panel status text).
--- ---
@@ -2544,215 +2208,58 @@ render-configuration block in `shell/capture/capture.cpp` and
TUs, so W2 dispatches only after W1-T1 is on `dev`. (T2T4 of W1 gate nothing here; the TUs, so W2 dispatches only after W1-T1 is on `dev`. (T2T4 of W1 gate nothing here; the
wave boundary is the file collision, not a semantic dependency.) wave boundary is the file collision, not a semantic dependency.)
**Two tracks, parallel, disjoint by field family:** T1 owns the naming fields
(`baseName` / `uniqueTag` / `displayName` / `capture_paths`), T2 owns the channel fields
(`channelCount` / PCM / `wav_codec`). **The known collision seam, named now:** both
tracks touch `shell/capture/capture.cpp` and
`shell/capture/capture_realtime_finalize.cpp` at adjacent lines. In `capture.cpp`, T1
owns the tag/paths mint (`:310-314`) and the `Sample` id/label lines (`:398-402`); T2
owns `RENDER_CHANNELS` (`:353-354`), the new post-render collapse step inserted between
the exists-check (`:390-395`) and the `Sample` population (`:398`), and
`stampCaptureSample`'s channel echo (`:207`). In `capture_realtime_finalize.cpp`, T1 owns
the label site (`:195`); T2 owns the layout-parse/channel lines (`:188-198`) and the
collapse insertion. Adjacent lines, disjoint fields — textual merge adjacency, not
semantic contention; whichever lands second rebases.
#### Ψ-W2-T1 — `capture-naming` #### Ψ-W2-T1 — `capture-naming`
**Goal.** Captures are labeled after their source track's name plus a discriminator, on **Landed** — see `docs/COMPLETED.md` for the full narrative. Captures are now named
every mint site — and the name is visible where the user looks. (Ψ.1) after their source track's name plus a discriminator
(`<Track> [+N] [#ordinal] MM-DD HHMM`) at every interactive mint site, with the name
**Today, and why it reads as broken:** every offline capture's `displayName` is the shown on the panel card over a scrim clearing the 4.5:1 contrast floor. New
literal `"item"` or `"track"` (`CaptureActionDef` → `render_settings.cpp:175-183` → `core/capture/capture_name`. Recapture, ingest, and the bake deliberately keep their own
`capture.cpp:402`), the filename is `"item_<epoch>-<counter>.wav"` naming — the bake's naming-and-lineage open question stays Ξ-W1-T1's/Ξ-W2-T1's to close,
(`makeUniqueTag`, `capture.cpp:187-199`; `deriveBankPaths`, untouched here. **DAW-verification obligation, unmet:** capturing from a named track, an
`core/capture/capture_paths.cpp:48-72`) — and **the source track name is read nowhere in unnamed track, and a multi-item selection, and confirming the labels show on the card
the tree** (repo-wide: zero hits for `P_NAME`/`GetTrackName`; the capture path holds and in the instrument — none run in a live REAPER session yet.
`MediaTrack*` and GUIDs but never a name). Worse, **the label barely surfaces:** the
panel card draws no name at all (`panel_render.cpp:23-63` — waveform, bars.beats,
seconds.ms only); the sample name appears only inside the VST3 editor. A good name
nobody can see is a half-fix, so this track also surfaces it.
**Surface boundary — owns:** `shell/capture/scope_resolve` (the ONE place already
walking source tracks before the FX-bypass guard — gains the name read, plumbed through
`ResolvedSource`, `scope_resolve.h:33`), `shell/capture/capture.h` (`CaptureRequest`),
`capture_orchestrator.cpp`'s naming lines (`:229`, `:405`), `capture.cpp`'s mint/label
lines (`:310-314`, `:398-402`), `capture_batch.cpp`'s mint sites (`:220`, `:284`,
`:395`), the realtime label chain (`capture_realtime_shell.cpp:309-310`,
`capture_realtime_finalize.cpp:195`, `core/capture/capture_realtime.cpp:31-32`),
`core/capture/render_settings` only for `CaptureActionDef::baseName` semantics
(`render_settings.h:170-175` — the literals become fallbacks), and the card name
surface: pure geometry in `core/ui` (`bank_grid` / `card_meta`) + its draw in
`shell/panel/panel_render.cpp`. **Must not touch:** `channelCount`/PCM/`wav_codec`
(T2's), the render-configuration block W1-T1 landed (read-only here),
`core/capture/capture_paths`'s purity — it stays REAPER-free path arithmetic with **no
filesystem access** (`core/capture/CLAUDE.md`); any collision handling stays
`makeUniqueTag`'s, shell-side.
**Behavior.**
- **The name is resolved shell-side in `scope_resolve`,** before the FX-bypass guard,
alongside the GUID walk it already does (`scope_resolve.cpp:53-72`, `:114-127`):
track scope → the selected track's name; item scope → the selected item's owning
track's name. `[verify]` the read API (`GetSetMediaTrackInfo_String` with `"P_NAME"`
vs `GetTrackName`) against the SDK header before use.
- **Label composition:** `displayName = "<TrackName> <discriminator>"`. The
discriminator's exact format is `[propose]` (lean: a compact date-time derived from the
capture's own `createdTimestamp`, e.g. `Bass 08-01 1432` — Daniel's "date, etc."
names it); `displayName` remains explicitly NOT unique, per the existing rule
(`core/model/CLAUDE.md` §`resample_name`) — the discriminator serves legibility, not
uniqueness.
- **The filename becomes meaningful too:** `baseName` = the sanitized track name, so the
stem is `"Bass_<tag>.wav"` via the existing `sanitizeStem` + `makeUniqueTag` pipeline —
stem uniqueness remains entirely `makeUniqueTag`'s job, unchanged. A name the sanitizer
reduces to nothing (non-ASCII) falls back to `"capture"` for the stem
(`capture_paths.cpp:24-46`'s existing rule) while `displayName` keeps the real name.
- **Fallback for an unnamed track:** deterministic, matching REAPER's own display
convention (`"Track N"` by index at capture time) — never the old literals on the
interactive paths.
- **All five mint sites covered, each stated:** offline (above); **realtime** — same
resolution at `begin` time (the action has a selected track); **batch** — per-unit
owning-track name, the batch ordinal retained as an additional discriminator
(`"Bass 1"`, `"Bass 2"`… exact composition settled with the discriminator format);
**recapture** — decided: preserves the existing entry's `displayName` (it regenerates
an entry, it does not mint a new identity; only the file stem re-mints);
**ingest** — unchanged (already meaningful: the source file's stem,
`ingest.cpp:249-266`); **bake** — unchanged (`model::nextIterationName`, the
`Kick r2` precedent). **The bake's naming-and-lineage open question is Ξ's, jointly
held with Ξ-W1-T1, and this track does not close or touch it.**
- **The card shows the name — decided in-scope:** one truncated name line on the panel
card, geometry pure (`bank_grid`/`card_meta`, CTest-covered), drawn by palette role
through the kit. Truncation is display-only; the stored value is never shortened. If
review finds the card too crowded at the smallest cell size, the fallback is
name-in-tooltip — `[propose]` at review, with on-card as the lean.
- **Existing bank entries are untouched** — new captures only; no retroactive rename.
- `displayName` already flows to the instrument (component-state copy + one-way refresh,
`component_state_io.cpp:74-88`, `sample_map.cpp:96`) — no instrument-side work.
**Acceptance criteria.**
- An item capture from a track named `Bass` yields `displayName` `Bass <discriminator>`
(format as settled at review) and a file `Bass_<tag>.wav`; a track capture likewise;
the literals `"item"`/`"track"` never appear as labels on any interactive capture path
again. `[verify — DAW]`
- Realtime and batch captures follow the same scheme (batch retains per-unit ordinals);
recapture preserves the entry's existing name; ingest and bake naming are unchanged.
- An unnamed source track produces the deterministic `Track N` fallback.
- The panel card shows the name, truncated to its cell, pure-geometry tested; the
instrument's browse list shows the same name with no mechanism change.
- File-stem uniqueness is still solely `makeUniqueTag`'s; `capture_paths` gains no
filesystem access; existing entries' labels are byte-identical after the track lands.
- **DAW-verification obligation:** Daniel captures from a named track, an unnamed track,
and a multi-item selection; sees the labels on the card and in the instrument.
**Open questions.** `[verify]` the track-name read API against the SDK header.
`[propose]` the discriminator format (lean above). `[propose]` multi-track item
selections (items spanning several tracks in one capture — lean: first source track's
name plus a `+N` marker; alternatives: joined names, or the scope literal as fallback).
`[propose]` card-name fallback to tooltip only if the card proves too crowded.
#### Ψ-W2-T2 — `mono-collapse` #### Ψ-W2-T2 — `mono-collapse`
**Goal.** A new capture whose channels are bit-identical is collapsed losslessly to one **Landed** — see `docs/COMPLETED.md` for the full narrative. A capture whose channels
channel — one channel of data on disk, a mono arrange item on insert, the instrument are bit-identical now collapses losslessly to one mono channel, written via temp file
loading in Mono mode. (Ψ.6) plus atomic rename, with `Sample::channelCount` measured off the landed file rather than
echoed from the request on every capture path — including realtime, which previously
parsed the layout and echoed the request anyway. Root `CLAUDE.md:208`'s
channel-count-preserved invariant is amended, in this track, to the landed wording:
channel count preserved, except that bit-identical channels may collapse losslessly to
mono; a lossy fold remains forbidden. The `shell/instrument/processor_reload.cpp` touch
stayed to the minimum named in its surface boundary (Phase Γ was live in that
directory). **Open, not closed here:** whether bake landings collapse too — `[propose]`,
leaning yes, still deferred to be confirmed against what Ξ-W2-T1 actually shipped.
**DAW-verification obligation, unmet:** capturing a dead-center mono source and a
true-stereo source, inserting both, loading both into the instrument, and running the
null test on the collapsed one (REAPER's mono-item-on-stereo-track summing at unity is
the specific thing to confirm) — none run in a live REAPER session yet.
**The invariant amendment is part of this track.** Root `CLAUDE.md:208` — "channel count ---
preserved (no silent stereo fold)" — is honored in spirit (the parenthetical forbids a
LOSSY fold; this collapse is lossless by predicate) but contradicted in text. The
amendment: channel count is preserved except that bit-identical channels may collapse
losslessly to mono; a lossy fold remains forbidden. Noted in the amendment: the old text
was already untrue in the other direction — a mono source renders at `RENDER_CHANNELS=2`
today (`capture_orchestrator.cpp:227`, hardcoded and never measured). `[verify — DAW]`
"lossless" is proven at the file-bytes level; it is not the same claim as the null
test's playback-chain property (whether REAPER sums a 1-channel item on a stereo track
at the same unity gain as a dual-mono 2-channel item) — see the acceptance criteria's
own `[verify — DAW]` on that bullet below.
**Surface boundary — owns:** `core/capture/wav_codec` (the pure bit-identity predicate + ### Ψ-W3 — Closing the track-scope stem-collapse hole
collapse plan, with `wav_codec` unit tests), `shell/capture/capture.cpp` (the post-render
collapse step between the exists-check `:390-395` and the `Sample` population `:398`,
plus `stampCaptureSample`'s channel echo `:207` — derive from the FILE, the
`bake_land.cpp:105,131,177` / `ingest.cpp:199,268` precedent),
`shell/capture/capture_realtime_finalize.cpp` (the collapse on the realtime path, and
the same echo fix — it parses the layout at `:188-198` and STILL echoes the request
value), the root-`CLAUDE.md:208` amendment, and — **minimally, because Phase Γ is live in
`shell/instrument/`** — `shell/instrument/processor_reload.cpp` only for the stale
"always 2 for extension captures" comment (`:64-77` vs `:150-157`) and any index/file
consistency fix that falls out; nothing else instrument-side. **Must not touch:** the
naming fields (T1's), `capture_paths`, the panel, the instrument's channel-mode logic
(`sample_map.cpp:58-62` already maps `channelCount == 1` → Mono, explicit user toggle
winning — no change needed).
**Behavior.** **Depends on Ψ-W2 for:** existing at all — this wave did not exist when the phase was
- **The predicate is pure and bit-exact:** all channels bit-identical per frame (float scoped. Daniel opened it after Ψ-W2's review surfaced that the track scope carried the
bit patterns, never epsilon) → collapse to 1 channel. Generalized to N channels same multi-track stem-collapse hole Ψ-W1-T1 had just closed for item scope.
(all-identical → mono; no partial collapse, e.g. never 4→2) — N-channel captures are
not reachable today (`channelCount` hardcoded 2), so the generalization is
future-proofing the predicate, stated as such.
- **The collapse mirrors the one existing rewrite precedent** — `trimAutoTailInPlace`
(`capture_realtime_finalize.cpp:55-125`): `parseWavLayout` → `extractFloatFrames` →
`buildFloat32Wav(1, …)` → truncating rewrite, all with the existing `wav_codec` tools
(`wav_codec.h:48-89` — `buildFloat32Wav` already takes arbitrary `nch`). There is no
PCM assembly on the offline path today (REAPER writes the file; the extension only
stats and hashes it) — this step is the first, and it lives shell-side at the named
insertion point with its plan pure.
- **Scope — decided: every extension capture path** — offline, realtime, batch,
recapture (batch and recapture route through the same backends). **Ingest is
excluded** — imported files are the user's bytes, not our capture; rewriting them is a
mutation this tool has no license for. **Unconditional, no user opt-out** — Ψ.6 asks
for the behavior, not a preference; dual-mono stereo carries zero information the mono
file lacks. **New captures only, never retroactive** — existing bank entries and files
are untouched.
- **The file and the index value are written together — the stated hazard.** A collapsed
file with `Sample::channelCount` left at 2 still plays (the instrument's
`extractChannel` clamps out-of-range to the last channel, `sample_map.cpp:174-186`,
yielding dual-mono) but the mono/stereo toggle and the waveform lane count would read
Stereo — so the criterion is equality with the file's `fmt` on every path, not absence
of crashes.
- **Insert needs no change, stated so nobody invents work:** `insert.cpp:129-152` passes
only a path to `InsertMedia`; REAPER derives the item's channel count from the file —
a 1-channel WAV yields a mono item for free.
- **Behavior change, stated and accepted:** `hashWavContent` covers the `fmt ` body +
`data` payload, so a collapsed capture will NOT hash-dedup against a pre-existing
stereo twin of the same audio (`bank_model.h:143-148`). Accepted — the predicate is
deterministic, so repeats of the same request still dedup against each other.
- **Bit-identical repeats survive:** a deterministic predicate over deterministic bytes;
identical requests still produce identical files (now identically-collapsed ones).
**Acceptance criteria.** **One track. Consolidates none of the seven** — it came from a review finding, not from
- A capture of dead-center mono content yields a 1-channel float32 WAV whose PCM is Ψ.1–Ψ.7.
bit-identical to either source channel and whose frame count is unchanged — losslessness
asserted by `extractFloatFrames` equality in the pure tests, observed in the DAW on the
real path. `[verify — DAW]`
- A capture with ANY differing sample pair is byte-identical to today's 2-channel output
— the not-collapsed path is unchanged, the same discipline as "bypassed means
byte-identical."
- `Sample::channelCount` equals the produced file's `fmt` channel count on EVERY capture
path — including realtime, whose finalize currently parses the layout and echoes the
request anyway; that defect is fixed here.
- Inserting a collapsed capture yields a mono arrange item; the instrument loads it in
Mono channel mode with a single waveform lane, explicit user toggle still winning.
`[verify — DAW]`
- The null test holds for a collapsed capture re-inserted at its source position.
`[verify — DAW: REAPER's mono-item-on-stereo-track summing at unity is the thing to
confirm]`
- Bit-identical repeats hold across the collapse; realtime and batch behave identically
to offline; ingest is demonstrably untouched (an imported dual-mono file stays stereo).
- The predicate + collapse plan are pure with `wav_codec` test coverage, including the
N-channel all-identical case and the one-sample-differs case.
- Root `CLAUDE.md:208` lands amended, in this track; the `processor_reload.cpp` touch is
the comment + consistency fix only.
- **DAW-verification obligation:** Daniel captures a dead-center source and a true-stereo
source, inserts both, loads both into the instrument, and runs the null test on the
collapsed one.
**Open questions.** `[propose]` whether bake landings collapse too (lean YES — the bake #### Ψ-W3-T1 — `track-scope-range`
"writes a file plus an index entry, like every other capture", `bake_land` already
derives channel count from the file, and a dead-center instrument render is exactly the **Landed** — see `docs/COMPLETED.md` for the full narrative. Any multi-track
dual-mono case; it is `[propose]` rather than decided only because `bake_land` is Ξ-W2's selected-tracks render now refuses, in both scopes, keyed on the render *source* rather
freshly-landed surface and the collapse there should be confirmed against what actually than the capture scope — closing the hole Ψ-W1-T1 left open for track scope. Realtime
shipped). `[verify]` REAPER's mono-item summing for the null test (above). Decided, not deliberately diverges and was left untouched, because it sums correctly. The refusal
open: unconditional; new-captures-only; ingest excluded; N-generalized predicate; no rests on the same unverified inference Ψ-W1-T1 rests on — that REAPER's selected-tracks
partial collapse. render source overrides custom time bounds — so if that inference is wrong, this refusal
costs a working capture. `docs/verify-track-scope-multitrack.md` is a new standalone
verification script on this branch, for this track's multi-track refusal specifically.
**DAW-verification obligation, unmet:** confirmed nowhere in a live REAPER session yet.
--- ---
@@ -2804,11 +2311,13 @@ proof it exists to give.
deck-rework entry (whose original "one row of taller decks with within-deck stacking" shape deck-rework entry (whose original "one row of taller decks with within-deck stacking" shape
Daniel explicitly superseded), and **Γ-W1-T1 discharges "Raise the stage-time ceiling above Daniel explicitly superseded), and **Γ-W1-T1 discharges "Raise the stage-time ceiling above
2 s"** (Γ-F3 reversed). 2 s"** (Γ-F3 reversed).
- **All of Phase Ψ** (`ppsi-*`). **Six tracks across two waves**, from a direct list of - **All of Phase Ψ** (`ppsi-*`). **Seven tracks across three waves**, from a direct list
seven defects and refinements (Daniel, 2026-08-01), not from `TODO-1.0.md`. Listed here of seven defects and refinements (Daniel, 2026-08-01), not from `TODO-1.0.md`. Listed
as a block, like Γ; unlike Γ it has no backing product doc — the seven are recorded here as a block, like Γ; unlike Γ it has no backing product doc — the seven are
verbatim in the phase header as its provenance (Ψ.1–Ψ.7), and the design content lives recorded verbatim in the phase header as its provenance (Ψ.1–Ψ.7), and the design
inline in its tracks. content lives inline in its tracks. The seventh track, Ψ-W3-T1, is not one of the
seven defects/refinements itself — it came from a review finding mid-phase; see the
Phase Ψ section for detail.
- **Γ-W3-T2 `bake-reset-amendment` is a CORRECTION, not a feature**, and belongs on this list - **Γ-W3-T2 `bake-reset-amendment` is a CORRECTION, not a feature**, and belongs on this list
for a different reason from the others: it exists only because Ξ-W2-T1 shipped ahead of this for a different reason from the others: it exists only because Ξ-W2-T1 shipped ahead of this
plan's sequencing claim. If more corrections of this shape appear, they belong here rather plan's sequencing claim. If more corrections of this shape appear, they belong here rather