docs: spec Phase Ψ — the extension trust pass, seven items across two waves, six tracks, three scheduled invariant amendments

This commit is contained in:
2026-08-01 19:01:01 -04:00
parent 0a7778b396
commit 8bf6841f7b
+748 -5
View File
@@ -4,7 +4,10 @@ The post-1.0 roadmap. Seventeen queued items consolidated into overlapping areas
sequenced into a Phase → Wave → Track hierarchy that implementation specialists can be
dispatched against directly — **plus Phase Γ**, which did not come from those seventeen
(it came from a direct interview, 2026-08-01) and is scoped in
`docs/product/instrument-control-surface.md`.
`docs/product/instrument-control-surface.md`, **and Phase Ψ**, which likewise did not
come from the seventeen: it came from a direct list of seven defects and refinements
(Daniel, 2026-08-01) and is specified inline in its own section below — there is no
backing product doc for it.
## What this doc is, and how it relates to the others
@@ -25,8 +28,9 @@ dispatched against directly — **plus Phase Γ**, which did not come from those
cited section rather than reading a file whole.
**Worktree slug convention:** `p<phase>-w<wave>-t<track>-<slug>`. Greek phase letters
transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**. So Θ-W1-T1 dispatches into
`pth-w1-t1-zone-retirement` and Γ-W1-T1 into `pg-w1-t1-knob-interaction-law`.
transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**, **Ψ → `psi`**. So Θ-W1-T1
dispatches into `pth-w1-t1-zone-retirement`, Γ-W1-T1 into `pg-w1-t1-knob-interaction-law`,
and Ψ-W1-T1 into `ppsi-w1-t1-capture-range-exactness`.
## Decision state
@@ -113,8 +117,11 @@ in Γ-W4-T1 and changes no wave boundary.
## Phase-wide acceptance criteria
These bind every track in all three phases and are stated once here rather than repeated
per track. **Phase Γ adds a set of its own**, stated in its phase header.
These bind every track in all four phases and are stated once here rather than repeated
per track. **Phase Γ adds a set of its own**, stated in its phase header. **Phase Ψ adds
none** — the structural heuristics, performance guardrails, and product invariants below
bind it exactly as written (its phase header states its performance posture against the
named hot paths).
### Structural (root `CLAUDE.md`, Daniel 2026-07-28)
@@ -2029,6 +2036,719 @@ value semantics, any deck geometry, or the bake's reset *membership* (W3-T2's).
---
## Phase Ψ — The extension trust pass: exact bounds, disjoint solo surfaces, reachable actions, honest drops, real names, true mono
**Ships:** capture ranges that mean what was asked — a time selection or razor area over
a longer item captures the selection, both scopes; per-mode solo surfaces that cache,
clear, and restore across the Design/Arrange switch, with the switch itself refused while
the transport runs; the Media-Explorer import action registered into the Media Explorer
action section so it can live on that toolbar; a drag-out gesture law that resolves its
target from what is actually under the cursor, continuously and reversibly, with no
silent no-op release anywhere; captures labeled after their source track instead of the
literal `"item"`/`"track"`, surfaced on the panel card; and lossless mono collapse for
new captures whose channels are bit-identical.
**Consolidates: none of the seventeen.** Phase Ψ came from a direct list of seven defects
and refinements (Daniel, 2026-08-01). There is no backing product doc — the design
content lives inline in the tracks below, and the seven are recorded here as the phase's
provenance, cited throughout as Ψ.1–Ψ.7:
> **Ψ.1** — item and track captures should be named (labeled) after their source track
> name, plus a discriminator (date, etc.); currently they aren't named anything useful.
> **Ψ.2** — Design vs. Arrange modes: any SOLO state in one mode is cached and removed
> when switching to another mode, disjoining the solo surfaces.
> **Ψ.3** — the Design/Arrange active-mode toggle is gated if playback is running; only
> allow the switch when the project is not playing.
> **Ψ.4** — the action that moves a Media Explorer item to a new ReaSampler on the
> selected track is not in the Media Explorer action category, so it cannot be added to
> the Media Explorer toolbar; fix this.
> **Ψ.5** — drag-and-drop targets are inexact: sometimes dropping into the arrange
> doesn't work, sometimes dropping into the FX area doesn't work; dragging between banks
> is fine.
> **Ψ.6** — capture into mono: if the left and right channels of a new capture are
> bit-identical, collapse to mono — one channel of data, mono arrange items, ReaSamplers
> load in mono mode.
> **Ψ.7** — capture item / capture track with a small time selection on a large item
> captures the entire item, not the selection/razor; make capture regions consistent and
> correct.
**Where the seven land:**
| Ψ-item | Track | Worktree slug |
|---|---|---|
| Ψ.7 | Ψ-W1-T1 | `ppsi-w1-t1-capture-range-exactness` |
| Ψ.2 + Ψ.3 | Ψ-W1-T2 | `ppsi-w1-t2-mode-switch-discipline` |
| Ψ.4 | Ψ-W1-T3 | `ppsi-w1-t3-media-explorer-section` |
| Ψ.5 | Ψ-W1-T4 | `ppsi-w1-t4-drop-target-resolution` |
| Ψ.1 | Ψ-W2-T1 | `ppsi-w2-t1-capture-naming` |
| Ψ.6 | Ψ-W2-T2 | `ppsi-w2-t2-mono-collapse` |
Ψ.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
reapply). Splitting them is two tracks fighting over the same function.
**Concurrency with Γ and Ξ.** Phase Ψ is extension-side. Γ and Ξ-W3 live in
`core/instrument/` + `shell/instrument/`; the surfaces are disjoint with ONE named
exception — Ψ-W2-T2 touches `shell/instrument/processor_reload.cpp` for a stale comment
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:
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`.
Ψ.2 requires writing `I_SOLO`; the amended form is stated in the track. A track that
lands solo writes without the amendment reads as an invariant breach in review.
2. **The action-registration contract** (Ψ-W1-T3): root `CLAUDE.md`
§"REAPER extension contract" documents only the main-section 4-step pattern; the
second, non-main mechanism (`custom_action` + `hookcommand2` + `-custom_action`
mirror) must be added beside it.
3. **The channel-count-preserved rule** (Ψ-W2-T2): root `CLAUDE.md:208` — "channel count
preserved (no silent stereo fold)" — is contradicted in text (not in spirit) by a
lossless bit-identical collapse, and was already untrue in the other direction (a mono
source renders at `RENDER_CHANNELS=2` today). The amended form is stated in the track.
**Performance posture.** Every Ψ surface is cold — per-capture, per-click,
per-mode-switch, per-mouse-move. None of the named hot paths (peaks envelope compute,
audition, realtime-capture tick, instrument `process()`) is touched. Two disciplines
carry anyway: the realtime tick's single-pointer-test idle fast path is untouched by
Ψ-W1-T1's render work, and Ψ-W1-T4 keeps the inside-client drag path free of SDK
hit-tests, exactly as today (`panel_drag.cpp:273-274` — "costs nothing on the common
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` |
| **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`
**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.
#### Ψ-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.
#### Ψ-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.
**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`
**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).
---
### Ψ-W2 — Names and channels, over the settled render block
**Depends on Ψ-W1 — specifically Ψ-W1-T1**, which rewrites the capture
render-configuration block in `shell/capture/capture.cpp` and
`core/capture/render_settings.cpp`; both W2 tracks edit adjacent regions of those same
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.)
**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_<epoch>-<counter>.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 = "<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`
**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)
**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).
**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).
**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).
**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.
**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.
---
## Traceability — all seventeen items
The check that nothing was dropped. Every row points at a track that exists above.
@@ -2077,6 +2797,11 @@ 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.
- **Γ-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
@@ -2176,4 +2901,22 @@ Phase Γ — The instrument's control surface (none of the seventeen; ends
(W1-T1 | W1-T3), editor_session.cpp (W2-T1 | W2-T2) — all textual adjacency, not
semantic contention. W3-T2's disjointness from W3-T1 is CONDITIONAL: confirm it against
what Xi-W2-T1 shipped, and serialize behind T1 if it does not hold.
Phase Psi — The extension trust pass (none of the seventeen; a direct list of seven)
W1 Exact bounds, disciplined switches, reachable actions, resolved drops [4 tracks]
T1 capture-range-exactness ..... Psi.7 [opens with a DAW repro matrix]
T2 mode-switch-discipline ...... Psi.2 + Psi.3 [one chokepoint: applyMode]
T3 media-explorer-section ...... Psi.4 [BOTH sections; new FOREVER-STABLE id]
T4 drop-target-resolution ...... Psi.5 [gesture law, not a patch]
W2 Names and channels, over the settled render block [gated on W1-T1's render block]
T1 capture-naming .............. Psi.1 [+ the card shows the name]
T2 mono-collapse ............... Psi.6 [lossless only; ingest excluded]
Three invariant amendments are track deliverables: never-touch-solo (W1-T2, three
files), the action-registration contract (W1-T3, root CLAUDE.md), channel-count-
preserved (W2-T2, root CLAUDE.md:208).
Shared files, named: panel_input.cpp (W1-T2 footer block | W1-T4 drag-arm block);
capture.cpp + capture_realtime_finalize.cpp (W2-T1 naming lines | W2-T2 channel
lines) — all textual adjacency, not semantic contention. main.cpp is W1-T3's
exclusively.
```