From 0f870b13377939d2a964891c7cf16520fa06c70c Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sat, 1 Aug 2026 23:32:25 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20collapse=20Phase=20=CE=A8's=20landed=20?= =?UTF-8?q?track=20specs,=20and=20record=20=CE=A8-W3=20=E2=80=94=20the=20w?= =?UTF-8?q?ave=20a=20review=20finding=20opened?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/PLAN.md | 719 ++++++++------------------------------------------- 1 file changed, 114 insertions(+), 605 deletions(-) diff --git a/docs/PLAN.md b/docs/PLAN.md index a62c9d6..058ad9c 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -2083,6 +2083,19 @@ provenance, cited throughout as Ψ.1–Ψ.7: | Ψ.1 | Ψ-W2-T1 | `ppsi-w2-t1-capture-naming` | | Ψ.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 sole mode mutator (`shell/view/view.cpp:380-469`) — and one discriminator (`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 surface boundary precisely because Γ is live in that directory. -**Three invariant amendments are DELIVERABLES of this phase, not asides.** Each is -scheduled in — and an acceptance criterion of — its owning track. This spec schedules -them; the implementing track performs them: +**Three invariant amendments were DELIVERABLES of this phase, not asides.** Each landed +in its owning track, as an acceptance criterion of that track: 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`. @@ -2123,416 +2135,68 @@ internal-drag path"). ### Ψ-W1 — Exact bounds, disciplined switches, reachable actions, resolved drops -**Depends on:** nothing in this phase. **Four tracks, disjoint by surface:** - -| 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. +**Depends on:** nothing in this phase. #### Ψ-W1-T1 — `capture-range-exactness` -**Goal.** A capture over a time selection or razor area on a source item substantially -longer than the selection produces exactly the requested range — both scopes, no -whole-item widening. (Ψ.7) - -**What is already correct, so the fix does not wander.** Range resolution is correct end -to end: `scope_resolve.cpp:100-112` (`resolveRange`) → razor union -(`render_settings.cpp:135-170`) or `GetSet_LoopTimeRange` (`scope_resolve.cpp:35-40`), -passed verbatim (`capture_orchestrator.cpp:221-222`), landed as -`RENDER_BOUNDSFLAG=0` + exact `RENDER_STARTPOS`/`RENDER_ENDPOS` -(`capture.cpp:320-322`). There is no item-bounds fallback in `RunCapture`. **The widening -decision point is the source-mode bit**: item scope maps to -`kRenderSelItems (&32) | kRenderSingleFile` (`render_settings.cpp:82-86`), and the repo's -own comments treat `&32` as item-extent-driven (`capture_batch.cpp:64-66`, `:98-99` — -the entire reason batch transiently selects one item per render). The working inference — -**unverified without a DAW** — is that REAPER's "selected media items" render source -overrides the custom time bounds. Track scope uses `&128` (selected tracks via master), a -normal time-bounded render, and **no code path was found that widens it**. - -**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. +**Landed** — see `docs/COMPLETED.md` for the full narrative. A ranged item capture now +renders exactly the requested window instead of the whole item, by re-sourcing through +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- +trim); the engineer shipped a conditional form of the re-source candidate — the +full-extent case runs literally unchanged code, keeping the byte-identity regression +floor structural and the fix cheap to revert if the override inference proves wrong. +Also landed: a transient isolation guard (cutting `B_MAINSEND` on direct folder +children, muting receives) so an item capture stays true to item scope, and a +post-render frame-count gate (±1 tolerance, tail-None only) that refuses and self-cleans +a widened render. New modules `core/capture/render_window`, `core/capture/track_topology`, +`shell/capture/render_selection`, `shell/capture/render_isolation`. The whole fix rests +on the unverified inference that REAPER's selected-tracks render source overrides +custom time bounds — Ψ-W3-T1 (below) now also depends on it. **DAW-verification +obligation, unmet:** the four-cell scope × selection-type matrix over a source item +substantially longer than the selection (tail None), plus one razor-union case — none of +it run in a live REAPER session yet. #### Ψ-W1-T2 — `mode-switch-discipline` -**Goal.** Switching the active mode caches and clears the outgoing mode's solo state and -restores the incoming mode's — disjoint solo surfaces per mode — and the switch itself is -refused, visibly, while the transport is playing or recording. (Ψ.2 + Ψ.3) - -**The invariant amendment comes first, because without it this track is a breach.** The -never-touch-solo rule is stated three times: `src/shell/view/CLAUDE.md:14-17` ("Never -touches master or `B_MUTE`/`I_SOLO`… User mute/solo survives every toggle untouched"), -`src/core/view/CLAUDE.md:10`, and `docs/product/design-view.md:160-165` (framed as the -analog of capture's non-destructive invariant) + `:588-592`. **The amended form:** the -tool never *loses* the user's solo state — solo is cached per mode on a real switch and -restored verbatim on return, the snapshot sense of non-destructive, exactly as the -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. +**Landed** — see `docs/COMPLETED.md` for the full narrative. Per-mode SOLO surfaces now +cache, clear, and restore across a Design/Arrange switch, with the switch itself +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`, +`docs/product/design-view.md`) — is amended, in this track, to the snapshot sense of +non-destructive: solo is cached per mode on a real switch and restored verbatim, not +left untouched absolutely the way `B_MUTE` and the master track are. Also closed: a +pre-existing bug where a footer mode-segment click never persisted view state. New +`core/view/solo_cache`, `shell/view/view_solo`. **DAW-verification obligation, unmet:** +the disjoint-surface solo matrix, the playback-gated refusal (playing and recording), +and the panel-persist case — none run in a live REAPER session yet. #### Ψ-W1-T3 — `media-explorer-section` -**Goal.** The Media-Explorer import action appears in the Media Explorer action section, -so it can be bound and placed on the Media Explorer toolbar. (Ψ.4) - -**Why it lands in Main today (SDK-verified):** the action registers via `"gaccel"` -(`ingest.cpp:457-468`), and `gaccel_register_t` registers into the main keyboard section -only — the struct has no section field (`vendor/reaper-sdk/sdk/reaper_plugin.h:1106-1117`). -**The supported mechanism (SDK-verified):** `custom_action_register_t` -(`reaper_plugin.h:1090-1103`) — `{uniqueSectionId, idStr, name, extra}`, with **Media -Explorer = section 32063** (`:1099`, corroborated `reaper_plugin_functions.h:3467`); -`Register("custom_action", &ca)` returns the command id or 0; `idStr` must be unique -**across all sections** (`:1100`). Dispatch for non-main sections MUST come from -`"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. +**Landed** — see `docs/COMPLETED.md` for the full narrative. The Media Explorer import +action now registers into REAPER's Media Explorer action section (32063) via +`custom_action` + `hookcommand2`, alongside its existing Main-section entry so existing +keybindings survive — a second FOREVER-STABLE id, `INGEST_IMPORT_MEDIA_EXPLORER_MX`, +minted per channel. Root `CLAUDE.md`'s "REAPER extension contract" is amended, in this +track, with the second, non-main registration mechanism beside the original four-step +main-section pattern. **DAW-verification obligation, unmet:** adding the action to the +Media Explorer toolbar and firing it from there; confirming the Main-section binding +still fires; and confirming unload/reload does not leak a duplicate Media Explorer entry +(the `-custom_action` unload mirror is unconfirmed against the SDK header) — none run in +a live REAPER session yet. #### Ψ-W1-T4 — `drop-target-resolution` -**Goal.** Dragging cards out of the panel resolves its target from what is actually under -the cursor — continuously, reversibly, with a defined outcome and a visible cue for every -surface, and no silent no-op release anywhere. (Ψ.5) - -**The root causes, so the redesign is judged against them.** The current chain locks a -drag's fate at its first processed move outside the client rect -(`core/ui/drag_out.cpp:18-26`): a single-card drag over ANY REAPER surface — -`GetThingFromPoint` returns non-empty info for the arrange too -(`instrument_drop_win.cpp:84-86`) — locks to `InstrumentDrop` and returns before the OS -hand-off is ever evaluated (`panel_drag.cpp:285-294`); over the arrange the info string -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). +**Landed** — see `docs/COMPLETED.md` for the full narrative. The drag-out gesture is now +a per-move, stateless law: target class resolves from what is under the cursor on every +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 +is now an instrument-drop hotspot, and a single-card arrange drop lands a timeline item +at the pointer's track and time. New `shell/actions/arrange_drop_win`. +**DAW-verification obligation, unmet:** the full target-class matrix (single- and +multi-card), reversibility across a drag that crosses the arrange en route to an FX +window, drag-speed independence, and the narrow-TCP case — none run in a live REAPER +session yet. --- @@ -2544,215 +2208,58 @@ render-configuration block in `shell/capture/capture.cpp` and TUs, so W2 dispatches only after W1-T1 is on `dev`. (T2–T4 of W1 gate nothing here; the 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` -**Goal.** Captures are labeled after their source track's name plus a discriminator, on -every mint site — and the name is visible where the user looks. (Ψ.1) - -**Today, and why it reads as broken:** every offline capture's `displayName` is the -literal `"item"` or `"track"` (`CaptureActionDef` → `render_settings.cpp:175-183` → -`capture.cpp:402`), the filename is `"item_-.wav"` -(`makeUniqueTag`, `capture.cpp:187-199`; `deriveBankPaths`, -`core/capture/capture_paths.cpp:48-72`) — and **the source track name is read nowhere in -the tree** (repo-wide: zero hits for `P_NAME`/`GetTrackName`; the capture path holds -`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 = " "`. 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_.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 ` - (format as settled at review) and a file `Bass_.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. +**Landed** — see `docs/COMPLETED.md` for the full narrative. Captures are now named +after their source track's name plus a discriminator +(` [+N] [#ordinal] MM-DD HHMM`) at every interactive mint site, with the name +shown on the panel card over a scrim clearing the 4.5:1 contrast floor. New +`core/capture/capture_name`. Recapture, ingest, and the bake deliberately keep their own +naming — the bake's naming-and-lineage open question stays Ξ-W1-T1's/Ξ-W2-T1's to close, +untouched here. **DAW-verification obligation, unmet:** capturing from a named track, an +unnamed track, and a multi-item selection, and confirming the labels show on the card +and in the instrument — none run in a live REAPER session yet. #### Ψ-W2-T2 — `mono-collapse` -**Goal.** A new capture whose channels are bit-identical is collapsed losslessly to one -channel — one channel of data on disk, a mono arrange item on insert, the instrument -loading in Mono mode. (Ψ.6) +**Landed** — see `docs/COMPLETED.md` for the full narrative. A capture whose channels +are bit-identical now collapses losslessly to one mono channel, written via temp file +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 + -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). +### Ψ-W3 — Closing the track-scope stem-collapse hole -**Behavior.** -- **The predicate is pure and bit-exact:** all channels bit-identical per frame (float - bit patterns, never epsilon) → collapse to 1 channel. Generalized to N channels - (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). +**Depends on Ψ-W2 for:** existing at all — this wave did not exist when the phase was +scoped. Daniel opened it 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. -**Acceptance criteria.** -- A capture of dead-center mono content yields a 1-channel float32 WAV whose PCM is - 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. +**One track. Consolidates none of the seven** — it came from a review finding, not from +Ψ.1–Ψ.7. -**Open questions.** `[propose]` whether bake landings collapse too (lean YES — the bake -"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 -dual-mono case; it is `[propose]` rather than decided only because `bake_land` is Ξ-W2's -freshly-landed surface and the collapse there should be confirmed against what actually -shipped). `[verify]` REAPER's mono-item summing for the null test (above). Decided, not -open: unconditional; new-captures-only; ingest excluded; N-generalized predicate; no -partial collapse. +#### Ψ-W3-T1 — `track-scope-range` + +**Landed** — see `docs/COMPLETED.md` for the full narrative. Any multi-track +selected-tracks render now refuses, in both scopes, keyed on the render *source* rather +than the capture scope — closing the hole Ψ-W1-T1 left open for track scope. Realtime +deliberately diverges and was left untouched, because it sums correctly. The refusal +rests on the same unverified inference Ψ-W1-T1 rests on — that REAPER's selected-tracks +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 Daniel explicitly superseded), and **Γ-W1-T1 discharges "Raise the stage-time ceiling above 2 s"** (Γ-F3 reversed). -- **All of Phase Ψ** (`ppsi-*`). **Six tracks across two waves**, from a direct list of - seven defects and refinements (Daniel, 2026-08-01), not from `TODO-1.0.md`. Listed here - as a block, like Γ; unlike Γ it has no backing product doc — the seven are recorded - verbatim in the phase header as its provenance (Ψ.1–Ψ.7), and the design content lives - inline in its tracks. +- **All of Phase Ψ** (`ppsi-*`). **Seven tracks across three waves**, from a direct list + of seven defects and refinements (Daniel, 2026-08-01), not from `TODO-1.0.md`. Listed + here as a block, like Γ; unlike Γ it has no backing product doc — the seven are + recorded verbatim in the phase header as its provenance (Ψ.1–Ψ.7), and the design + 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 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