# PLAN The post-1.0 roadmap. Seventeen queued items consolidated into overlapping areas and 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`, **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); it has landed, and now that its section here is retired, the list is carried verbatim in `docs/COMPLETED.md` — there is no backing product doc for it, **and Phase Ε**, which likewise did not come from the seventeen: it came from a direct request (Daniel, 2026-08-02) and is scoped in `docs/product/bank-package.md`, **and Phase Ρ**, likewise a direct request (Daniel, 2026-08-02), scoped in `docs/product/render-in-place.md`, **and Phase Λ** — the Linux port of both artifacts — likewise a direct request (Daniel, 2026-08-02), scoped in `docs/product/linux-readiness.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-03), and like Ψ it has **no backing product doc of its own** — the seven are carried verbatim in its phase header below as Ω.1…Ω.7. It does amend two clauses of `docs/product/instrument-control-surface.md`, which are edited there rather than merely contradicted here. ## What this doc is, and how it relates to the others - **`docs/PLAN.md`** (this file) — the active on-deck specification list. Each track is written so a specialist brief is writable from this file alone: goal, consolidated source items, surface boundary, behavior, acceptance criteria, open questions, prerequisites. - **`docs/TODO-1.0.md`** — retained as the **verbatim-provenance appendix**. It holds Daniel's raw asks and every answer round, unedited; where this plan compresses a behavior bullet, that file is the backing record. Items are cited here by number (e.g. "consolidates items 1, 8, 14"). It is not a work queue any more; this file is. - **`docs/TODO.md`** — deferred follow-ups with recorded rationale, unrelated to the seventeen (with one flagged intersection: see "Flagged for awareness" below). - **`docs/COMPLETED.md`** / **`docs/ARCHIVE.md`** — doc-keeper's. When a track here finishes, its point is removed from this file and appended to `COMPLETED.md` with any deviation between landed code and spec noted. - **`docs/product/`** — the product-design reasoning behind prior phases. Grep for a cited section rather than reading a file whole. **Worktree slug convention:** `p-w-t-`. Greek phase letters transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**, **Ψ → `psi`**, **Ε → `e`**, **Ρ → `r`**, **Λ → `l`**, **Ω → `om`**. So Θ-W1-T1 dispatches into `pth-w1-t1-zone-retirement`, Γ-W1-T1 into `pg-w1-t1-knob-interaction-law`, Ψ-W1-T1 into `ppsi-w1-t1-capture-range-exactness`, Λ-W2-T1 into `pl-w2-t1-linux-compile-blockers` (Phase Λ's two audit tracks already ran under `pl-w1-t1-build-toolchain-audit` and `pl-w1-t2-source-runtime-audit`), and Ω-W1-T1 into `pom-w1-t1-mode-switch-responsiveness`. ## Decision state Everything carried forward from `TODO-1.0.md` is classified **[verify]** (answerable by reading code or running the DAW) or **[propose]** (a design call made at implementation review with a proposal, not a Daniel call); that classification is preserved per question, attached to the track that will answer it. **Read the Phase Λ paragraph below before relying on the plan-wide "nothing is unanswered" claim the Γ, Ε and Ρ paragraphs make.** Λ (added 2026-08-02) carries **four open [Daniel]-class forks**, so that claim is no longer true of the plan as a whole; it is true of every phase except Λ, and Λ's paragraph states exactly which of its four forks gate anything. Θ-W3-T1's two genuine **[Daniel]** questions — which no amount of code-reading could answer — are both ruled on and the track has landed; see `docs/COMPLETED.md` for the full narrative. **Reload tier = Grouping B** (continuous knobs live: filter cutoff/Q/ morph/drive/mod amount/key-track, every envelope stage time and level; root note, loop span, and start frame still trigger a full reload). **Mid-stage rule = candidate (iv), hold normalized stage position** (φ = elapsed/duration held fixed across a duration change, then advancing at 1/newDuration). **Phase Γ opened six [Daniel]-class forks (Γ-F1…Γ-F6) and all six are ruled** (Daniel, 2026-08-01) — the rulings are folded into the tracks below and indexed in `docs/product/instrument-control-surface.md` §8. **Γ-F6 closed with a correction to the analysis, not merely a ruling**: dynamic reported latency is routine for VST3 instruments and REAPER handles it as a matter of course; what makes the mandated restart expensive *here* was self-inflicted (`setActive(true)` called `reloadInstrument`), so the cost was ours to reduce. **That reduction landed in Γ-W3** — see `docs/COMPLETED.md`. **Γ-F3 was subsequently REVERSED and a seventh fork opened AND CLOSED, all by Daniel's later rulings of 2026-08-01.** Γ-F3 (*"the stage-time ceiling stays 2.0 s"*) is replaced by *"extend the stage lengths to 10s"* — the ceiling moves in Γ-W1-T1. **Γ-F7** (the VST3 parameter *order*) is **RULED: signal flow** — *"signal flow order."* **There is now NO unanswered [Daniel]-class question anywhere in this plan.** **Phase Ε (added 2026-08-02) opened three more [Daniel]-class forks and ALL THREE ARE RULED**, same day (Daniel, 2026-08-02): **Ε-F1** container format — *"proprietary container"*, the hand-rolled `RSBK`; **Ε-F2** import target — *"always lands as a new bank, with an auto suffix if name collision"*; **Ε-F3** import under a degraded tracking ledger — *"refuse mismatched import."* Ε-W1 through Ε-W3 have landed; see `docs/COMPLETED.md` for the full narrative, and the rulings remain indexed at `docs/product/bank-package.md` §"Rulings". **Two of the three landed somewhere other than the framing recommendation:** Ε-F2 dropped the proposed rename prompt in favour of a deterministic suffix, and Ε-F3 reversed allow-with-confirm to refuse — carried by the framing's own counter-argument, that the accepted tracking residual contemplates *one* untracked capture while a bulk import strands hundreds in a single gesture. **The plan-wide claim above therefore holds unqualified — no unanswered [Daniel]-class question remains anywhere in this plan, Phase Ε included — and no track in this plan is gated on a decision.** **Phase Ρ (added 2026-08-02) opened three more [Daniel]-class forks and ALL THREE ARE RULED**, same day (Daniel, 2026-08-02): **Ρ-F1** multi-track — *"refuse"*, one track per fire, a settled non-goal rather than a deferral; **Ρ-F2** the result track's mode — *"for this action which is not a capture, the result track should always go to arrange"*; **Ρ-F3** tail — *"follow panel tail settings."* Ρ-W1-T1 has landed; see `docs/COMPLETED.md` for the full narrative, and the rulings remain indexed at `docs/product/render-in-place.md` §"Rulings". **Ρ-F2 overrode the request's own original wording** ("stays in whatever mode was active") and is the only one of the three that changed the spec: the result track is now an Arrange member unconditionally, the A/B-on-the-bench behaviour mode-following would have enabled is gone, and the ruling pulls in a two-line fix to the panel's auto-tag detector that would otherwise reverse it on the next timer tick. **The plan-wide claim above therefore still holds unqualified — no unanswered [Daniel]-class question remains anywhere in this plan, Phase Ρ included — and no track in this plan is gated on a decision.** **Phase Λ (added 2026-08-02) breaks that unqualified claim, and it is corrected here rather than left to be discovered.** The claim as written in the three paragraphs above — *"no unanswered [Daniel]-class question remains anywhere in this plan"* and *"no track in this plan is gated on a decision"* — is now **scoped to Θ, Ξ, Γ, Ψ, Ε and Ρ.** It is still true of all six. It is **not** true of Λ. **Λ opened four [Daniel]-class forks and NONE of them is ruled** (Λ-F1 CI; Λ-F2 the dialog-resource route; Λ-F3 the copy-only drag-out invariant's wording; Λ-F4 the declared support floor). Each is stated with the evidence for both sides at `docs/product/linux-readiness.md` §"Open forks — Daniel's", and the Phase Λ section below carries them as a table. What they gate is narrow and is not a matter of judgement: - **Λ-F2 is the only one that gates a dispatch** — Λ-W2-T3 `panel-dialog-resource` cannot be briefed until the route is chosen, because the two routes own different files. Leaving it open does not stall the phase; it slips that one track to Λ-W4 and **splits Λ-W3's verification sweep into two Linux sessions**, which is the fork's actual price. - **Λ-F4 gates the ship wave, not a dispatch.** Λ-W2 can record a floor and widen it; Λ-W5 cannot ship an artifact that does not say what it runs on. Answerable any time before Λ-W5-T1 is briefed. - **Λ-F1 and Λ-F3 gate nothing at all.** Λ-F1 (CI) decides only which paragraph Λ-W5-T1 writes; Λ-F3 decides one sentence's wording inside an edit Λ-W4-T3 makes either way. - **The other eleven Λ tracks are dispatchable against open forks**, in the sense that none of them waits on a decision. What every Λ track waits on instead is a Linux box — a different kind of unknown, and the reason Λ-W3 exists. **Λ's six [Daniel] rulings of 2026-08-02 are settled** and are recorded in the phase section below in the same shape as Γ's: the instrument is in scope; SWELL is reached by `dlopen`ing REAPER's own `libSwell.so` rather than vendoring it; the editor is REAPER-only but every other host must degrade safely; macOS is out; the Linux artifact is shipped rather than developer-only; and a hard `unlink` prune is acceptable with a platform-aware confirmation. **Do not re-litigate them.** **Phase Ω (added 2026-08-03) opened six [Daniel]-class forks and ALL SIX ARE RULED**, the same day the phase was framed (Daniel, 2026-08-03): **Ω-F1** the marker-alignment scope — *"widen to the correct scope"*, the whole overlay↔waveform mapping rather than the START mark; **Ω-F2** the loop marks — hidden outright in Trigger, drawn **grey-disabled and still grabbable** in Gate with loop off; **Ω-F3** the single-button toggle rule extends to Filter, Loop and Mono|Stereo, the last as a **mode selector** with no off state; **Ω-F4** the active/focus colour is **`AccentPrimary`**, not `AccentHot`; **Ω-F5** the mode-switch symptom is *"nothing for 2 seconds, then everything"* on **both** entry surfaces; **Ω-F6** the FX parking **separates UI from processing** — visibility/routing/FX-enable apply synchronously, per-FX offline/online defers. **Phase Ω therefore carries ZERO unanswered [Daniel]-class questions and no Ω track is gated on a decision.** Two of the six moved the spec rather than merely confirming it: Ω-F2 reverses `docs/product/instrument-control-surface.md`'s "Disabled rather than hidden" marker principle in the Trigger case and retires its `LOOP — GATE ONLY` caption, and Ω-F6 amends the accepted FX-parking invariant in `src/shell/view/CLAUDE.md`. Both amendments are named track deliverables, not footnotes. **The Λ carve-out above is unchanged and is NOT weakened by this phase.** Λ remains the one phase in this plan with unanswered [Daniel]-class questions; the enumeration in Λ's paragraph predates Ω, so read *"scoped to Θ, Ξ, Γ, Ψ, Ε and Ρ"* as **Θ, Ξ, Γ, Ψ, Ε, Ρ and Ω** — seven phases, all of them fully ruled, and Λ alone excepted. **Ruling 3 (Daniel, 2026-08-01) — real units at the host boundary.** *"The parameter values exposed to the VST host should be in real units, such that the host automation lanes report usable values."* Satisfied through VST3's **plain-value layer**, not its wire format (which is normalized and cannot be otherwise): `toPlain`/`toNormalized`, `getParamStringByValue` and `ParameterInfo::units`. Specified at **`docs/product/parameter-automation.md` §6.7** — the per-category unit/precision table, the one-formatter invariant, the `stepCount` sweep, and the resolution of the apparent conflict with the filter's re-taper prohibition. It lands entirely in Γ-W4-T1 and changes no wave boundary. ### Flagged for awareness — not blocking, but decision-grade 1. **Item 15's cross-artifact seam is RESOLVED — this is no longer an unknown.** The instrument is a read-only bank consumer by invariant (`src/shell/instrument/CLAUDE.md`), and the one previous attempt at an instrument→extension relay (S13) closed with a **DEGRADED** spike verdict and was deferred (`docs/TODO.md`). Resample required that crossing. Ξ-W2-T1 ratified Decision 1 = (1b): the editor invokes the extension's bake action directly over the VST-host bridge (`NamedCommandLookup`/`Main_OnCommandEx`), no poller, no nonce — dissolving the S13 DEGRADED verdict rather than re-litigating it. The read-only bank invariant held: the crossing is a bridge call, not a shell-side bank write. See `docs/COMPLETED.md` for the full narrative. 2. **The "Γ before Ξ-W2" ordering is VIOLATED, it was never Daniel's choice, and Γ now owns the correction.** Daniel, 2026-08-01: *"xi was started before I spun you up, we'll have to correct phase xi inside gamma. wasn't a choice."* Ξ-W2-T1 (`resample-bake-chain`) ran ahead of this plan's sequencing claim, so the bake's settled reset scope — which enumerates parameters **by name** — ships incomplete: it cannot name rate, pitch offset or the limiter flag, none of which existed when it was written. **This is no longer a scheduling constraint to honour. It is a correction obligation with a named owner: Γ-W3-T2 `bake-reset-amendment`.** The classification costs no Daniel decision — `docs/product/instrument-control-surface.md` §3.4 pre-classifies all three against Ξ-W2's own ratified rule (all **reset**) — but the amendment must be written **against what Ξ-W2-T1 actually shipped, not against what this plan predicted it would ship.** Ruling 1 adds a second correction of the same shape, homed on Γ-W4-T1 rather than here: see item 3. 3. **The one-way doors are now IN-PHASE, and the sweep for them is a delivered artifact.** Ruling 1 (Daniel, 2026-08-01) schedules VST3 parameter reporting **inside Phase Γ**, as Γ-W4-T1. Everything that participates in a parameter's normalization therefore freezes at the end of this phase rather than at the start of some later one, and anything that ought to move must move first. - **The taper** (Γ-W1-T1) — known, and the reason this phase was ordered as it was. - **The stage-time ceiling 2.0 → 10.0 s** (Γ-W1-T1) — Γ-F3 **reversed** by Daniel's *"extend the stage lengths to 10s."* A range endpoint is normalization exactly as much as the curve between the endpoints is. - **Two further doors that need action, both new**, both landing on Γ-W1-T1: every default must have an **exact normalized preimage** (a host's reset-to-default has no `resetDeckParam` bypass to use), and the filter's four `*Norm` controls **must not be re-tapered** (their laws are already wire-frozen in payload v9). - **Six more constants freeze without needing to change**, and three are already frozen for unrelated reasons; the complete sweep, with dispositions and with what was checked, is `docs/product/parameter-automation.md` §8. **That doc is no longer scoping-only — §§6–10 are the specification Γ-W4-T1 is built from.** 4. **Single-cycle material and the wavetable direction is FRAMED but is NOT a phase, and deliberately so.** Daniel, 2026-08-03, after DAW-testing a one-cycle 60 Hz sine capture: *"supporting smoothness at this micro scale would turn this into a wavetable synth, which is desirable."* The product framing is `docs/product/single-cycle-and-wavetable.md`. **It framed EIGHT [Daniel]-class forks. ONE is now ruled and SEVEN remain open**, so it still gets a pointer here rather than a phase section: `PLAN.md` is the active on-deck specification list, and a phase whose specification rests on seven unruled forks is neither briefable nor consistent with the standing claim above that **Λ is the only phase in this plan with unanswered [Daniel]-class questions**. That claim is unaffected by this item — there is no phase here to except. When the remaining forks are ruled, the doc becomes a phase's backing product doc in the ordinary way (the Γ / Ε / Ρ / Λ pattern, not the doc-less Ψ / Ω one). **The ruling — Daniel, 2026-08-03:** *"I don't want actual frame-indexed wave tables yet, that is out of scope. just static captures right now."* It closes fork F5 (the wavetable-position knob does not ship now), confirms the multi-frame wavetable kind out by ruling rather than only by the doc's argument, and **narrows the near-term scope to the doc's W0 — loop-grid recalibration for micro material, which is defect-class work with no new parameter, no format change and no `process()` involvement.** *"Yet"* is recorded as a scope ruling, not a rejection; the doc keeps the deferred case whole and names the single trigger that would revive it. **The ruling does NOT settle band-limiting** — the question that decides it is still outstanding with Daniel, and nothing in the doc pre-rules it. Per the doc's §7.1. **Two things in it are decision-grade for work already in this plan, which is why it is flagged rather than merely filed:** - **The immediate defect it came from is NOT its subject.** `nearestZeroCrossing` (`src/core/instrument/ui/waveform_view.cpp:181-210`) fans out across the whole buffer with no radius bound; a snap-radius track is in flight against it. **But Phase Ω's own acceptance criteria name the zero-crossing snap as a thing that "must come out the other side identical", and Ω-W1-T5's outline entry repeats it.** A snap-radius change alters exactly that behaviour. **Whoever owns Ω's criteria must reconcile the two** — this item records the conflict, it does not resolve it. - **Sequencing, if any of it is ever dispatched.** Every candidate touches `waveform_view.{h,cpp}`, `editor_input_waveform.cpp` and `editor_controls.cpp`'s `waveMarksFor` / `grabbableMarks` — Ω-W1-T5's exact surface. Nothing from that doc should dispatch until Ω-W1-T5 and the snap-radius track have both landed. ## Phase-wide acceptance criteria These bind every track in this plan and are stated once here rather than repeated per track. **Phase Λ adds a set of its own**, stated in its phase header. ### Structural (root `CLAUDE.md`, Daniel 2026-07-28) - **More directories is a must; more files is good; ~600-line file ceiling.** The ceiling is the *bar*; a responsibility seam is the *method*. Bisection-to-hit-the- number is rejected. `sampler_core.cpp` (956 lines) is the standing documented hot-path exception — Θ-W1-T1 re-seams it, and any surviving over-ceiling TU must carry the same explicit justification. - **Templates where earned** — compile-time dedup with zero runtime cost, off the hot paths. Not for types that differ in name only. - **SOLID is great, but saved CPU is better.** No dispatch-stack blowouts anywhere; prefer static polymorphism where the types are compile-time-known. ### Performance guardrails Root `CLAUDE.md`'s five extension-side guardrails (peaks envelope compute, audition, realtime-capture tick, JSON, `FxBypassGuard`) are unchanged by this plan; no track here touches them. The instrument adds a sixth surface that binds every Θ track: - **`process()` — the per-voice-per-sample path — takes no new indirection.** The filter tick, the envelope evaluation (staged *and* spline), the pitch-shift read, and the loop read all sit on it. Concrete, inlineable types only: **no `IEnvelope`, no `IFilter`, no virtual per-voice `tick()`**. A filter with two modes is a branch-predictable switch or a compile-time-known dispatch, never a vtable. Spline evaluation is a binary search over a point array, not a polymorphic curve object. - **No allocation, no file I/O, no bridge call in `process()`.** The off-audio-thread `reloadInstrument` + atomic pointer swap stays the only way new state reaches the audio thread. Every new parameter this plan adds follows that path. - **A split that would add a hot-path indirection is out of scope — rework it or drop it.** ### Product invariants - **Capture and placement are separate acts.** No track here may place a timeline item. Item 15's bake explicitly must not, and one of its candidate architectures uses a *temporary* arrange mutation — that candidate must leave the arrange byte-identical. - **Prune is the single, exclusive file-deletion authority.** Item 15's "replace" never deletes bytes; item 17's consolidation may not weaken any protection prune has today. - **The instrument never writes the bank.** Item 15 is the first feature that needs to, and it resolves that by *asking the extension*, not by breaching the invariant. - **Migration bar: a project saved before a change reopens sounding identical.** Holds everywhere except item 16's genuinely-multi-zone case, where Daniel deliberately relaxed it. - **Every pure module gets a `_tests` target** that runs without REAPER or a DAW. New pure modules in this plan are not optional-test. --- ## Phase Ω — Responsiveness, and the control surface's second pass **Ships:** A mode switch that draws its result at once instead of two seconds later; a master meter that moves at 60 FPS instead of two; a deck whose two-state toggles are single buttons and whose envelope decks are their own overlay targets; loop marks that tell the truth about the mode they are in; and one coordinate mapping shared by everything that rides the waveform, so an overlay lands on the frame it names. **Consolidates: none of the seventeen.** Phase Ω came from a direct list of seven defects and refinements (Daniel, 2026-08-03) and has **no backing product doc of its own** — the Ψ precedent. The seven are its provenance and are recorded here verbatim in substance: - **Ω.1** — the ReaSampler 9000 master meter renders at 0.5–1 FPS. *(instrument)* - **Ω.2** — Filter Mod belongs on the filter's envelope deck, and the Trigger face's knob spacing is wrong. *(instrument)* - **Ω.3** — a two-state toggle should be ONE button, not two segments. *(instrument)* - **Ω.4** — the whole envelope deck should be the overlay's click target, not a 12 px corner square. *(instrument)* - **Ω.5** — the loop marks are meaningless in Trigger and unreadable in Gate with loop off. *(instrument)* - **Ω.6** — the START marker does not land on the sample position it names — **widened by Ω-F1 to the overlay↔waveform coordinate mapping as a whole.** *(instrument)* - **Ω.7** — the arrange⇄design switch takes 1–2 s and shows nothing until it completes. *(extension)* **This phase is small tidy work sequenced BEFORE Λ, and it is not another Λ.** Everything here is verifiable on the current Windows box except the DAW-observable half, which has one track of its own (Ω-W2-T3) rather than a wave. It **supersedes nothing** and adds no format, no wire version, no parameter id, and no new artifact. **Two invariant amendments are track deliverables** (the Ψ/Ρ precedent), because two settled statements become untrue the moment this phase lands: - `docs/product/instrument-control-surface.md` §6.3(d) and §6.4 — the marks' "Disabled rather than hidden" principle and the `LOOP — GATE ONLY` caption, reversed in the Trigger case by Ω-F2. **Owner: Ω-W1-T5.** *(Amended in that doc as part of this phase's authoring; the track verifies the code agrees with the amended text.)* - `src/shell/view/CLAUDE.md`'s FX-parking caveat — today it accepts a "possible load hitch" on the return to the active mode as the cost of the CPU reclaim. Ω-F6 splits that cost off the synchronous path entirely. **Owner: Ω-W1-T1.** ### Rulings — Daniel's, 2026-08-03. Six, settled. **Do not re-litigate these.** There is no product doc to index them in; this table is their one home, and a brief for any Ω track is written against it. | Ruling | What it settled | Bound into | |---|---|---| | **Ω-F1** | **Widen the marker-alignment work to the correct scope.** The defect is the overlay↔waveform coordinate mapping as a whole, not the START mark — a class defect with four independent causes, inherited by every overlay | Ω-W1-T5 | | **Ω-F2** | **Trigger ⇒ the loop marks are HIDDEN outright. Gate with loop off ⇒ they are still drawn and still grabbable, styled GREY DISABLED rather than dim teal** — *"that was not intuitive to me."* The drag-to-set-loop gesture survives; `LOOP — GATE ONLY` goes | Ω-W1-T5 | | **Ω-F3** | **The single-button toggle rule extends to `Filter` and chrome `Loop`** (Primary when active, **dim gray** when off) **and to `Mono\|Stereo`, which is a MODE SELECTOR** — its label reads the current mode and it has **no** dim-gray state | Ω-W1-T4 | | **Ω-F4** | **The active/focus colour is `AccentPrimary`** — both the active single-button state and the focused envelope deck's border. **Not `AccentHot`**, which stays the hover cue | Ω-W1-T4 | | **Ω-F5** | **The mode-switch symptom is both entry surfaces**, and it is *"nothing for 2 seconds, then everything switches"* — nothing renders until the whole synchronous body completes | Ω-W1-T1, Ω-W1-T2 | | **Ω-F6** | **ACCEPTED, and it is a spec amendment: separate the FX parking's UI half from its processing half.** Visibility / routing / FX-enable apply **synchronously**; per-FX offline/online **defers to a later idle tick** — *"the switch doesn't happen during playback anyway… the UI is the priority when toggling modes"* | Ω-W1-T1 | **Verified by Daniel, not a fork:** the deck knob-spacing defect is in the **Trigger** face. **Gate is correct and must stay pixel-identical** — an acceptance criterion of Ω-W1-T4, not a hope. ### Phase-Ω acceptance criteria These bind every track in this phase, in addition to the plan-wide set above. - **No behaviour changes outside the named defect.** Every track carries that criterion explicitly. The Gate deck face, the zero-crossing snap, the ballistics constants, the 500 ms bank/bake poll body and the audio thread are each named below as things that must come out the other side identical. - **Nothing is added to any hot path.** No track here touches `process()`, the `peaks` envelope compute, audition, or the realtime-capture tick. Ω-W1-T3's work is entirely UI-thread paint cost; Ω-W1-T1's is entirely main-thread REAPER API cost. - **`reasampler_editor.h` is at 564 lines against the ~600 ceiling** (`docs/TODO.md`, "The editor's drag state machine has no seam"), and **three W1 tracks add state to it.** The budget is ~36 lines for the whole wave. **The drag-state seam that TODO entry names is OUT OF SCOPE for Ω** — a track that finds itself over the ceiling stops and flags rather than bisecting the header under time pressure, which is exactly the failure that entry predicts. - **The shell layers this phase touches have no CTest coverage** — `editor_paint_*`, `editor_input_*` and `shell/view` have no test targets. Every track therefore states, as a criterion, **what it pushed down into a pure module and asserted there**; "verified by eye in the DAW" is the fallback, not the plan. - **Measurement before and after is a deliverable, not a nicety**, for the two performance items (Ω.1, Ω.7). A track that improves a number it never measured has no acceptance criterion, only an opinion. --- ### Ω-W1 — The five defects, in parallel **Depends on:** nothing. **Five tracks, disjoint at the file level.** Items 2, 3 and 4 are ONE track on purpose: they land in the same deck modules and the same pinned-geometry test files, so splitting them means re-deriving `test_deck_groups_measured.cpp` three times. Items 5 and 6 are one track because they overlap inside `paintWaveform`'s mark-draw loop — one deletes and gates it, the other changes the `mx` it computes. | Track | Owns | |---|---| | **T1** `mode-switch-responsiveness` | `shell/view/view.{h,cpp}` + one new deferred-park TU + its drain in `app/main.cpp` | | **T2** `mode-switch-persist` | `shell/actions/design_view_actions.cpp` + `shell/persist/ext_state_io.cpp` — dirty-key gating | | **T3** `meter-rate` | the editor's timer/paint path + the hero-envelope cache | | **T4** `deck-reflow-focus-toggles` | Ω.2 + Ω.3 + Ω.4 — the deck's composition, layout, paint, input and pinned tests | | **T5** `overlay-mapping` | Ω.5 + Ω.6 — the waveform overlay's marks and its one coordinate mapping | **Shared files in the wave, named rather than discovered at merge.** All textual adjacency unless marked otherwise. | File | Tracks | Nature | |---|---|---| | `src/shell/instrument/reasampler_editor.h` | T3 (meter clock + timer members), T4 (deck focus state), T5 (nothing new expected) | **The one to watch.** Additive member declarations in different sections of a header with ~36 lines of ceiling margin between them. Whichever lands second rebases AND re-checks the line count | | `src/shell/instrument/editor_paint_waveform.cpp` | T3 (the envelope-cache call site at `paintWaveform`'s bin/`computeEnvelope` block), T5 (the mark, caption and mapping regions below it) | Disjoint regions of one function's body; whichever lands second rebases | | `src/shell/instrument/editor_controls.cpp` | T4 (`faceLayout`'s `deckDescs` build), T5 (`waveMarksFor` / `grabbableMarks`) | Different functions, no shared state | | `src/app/CMakeLists.txt` | T1 (one `target_sources` line for the new park-queue TU) | Ω's only CMake edit. **Cross-phase adjacency:** Λ-W2-T2, Λ-W2-T3, Λ-W4-T3 and Λ-W5-T1 all own regions of this file. Ω lands first; Λ rebases | | `src/app/main.cpp` | T1 (the drain call inside `OnTimer`) | **Cross-phase adjacency:** Λ-W2-T1 owns this file's `REAPERAPI_LoadAPI` failure branch — a different region | #### Ω-W1-T1 — `mode-switch-responsiveness` **Goal.** A mode switch redraws immediately. The expensive half of the switch still happens; it just stops being the thing the user waits for. **Spec:** Ω.7 and rulings Ω-F5, Ω-F6 above; no product doc. **Surface boundary — owns:** `src/shell/view/view.cpp` (the `applyMode` body — the undo block, the park/restore loops, the parent-visibility loop, `resolve`), `src/shell/view/view.h`, ONE new TU pair beside them for the deferred queue, one `target_sources` line in `src/app/CMakeLists.txt`, the drain call in `src/app/main.cpp`'s `OnTimer`, and `src/shell/view/CLAUDE.md`'s FX-parking caveat. **Does not own:** `core/view` (that is Ω-W2-T1's), `design_view_actions.cpp` or `ext_state_io.cpp` (T2's), `panel_input.cpp` (Ω-W2-T2's), or `view_solo.cpp`. **The solo surface index is EXONERATED.** It costs ~2T extra API calls, sub-millisecond. Say so in the brief so it is not re-litigated: the magnitude is two older O(project) costs that are invisible on a scratch project and quadratic-feeling on a real one. **Behavior.** - **The undo mask (`view.cpp:563`).** `Undo_EndBlock2(proj, undoLabel.c_str(), -1)` — `-1` is `UNDO_STATE_ALL` (`reaper_plugin.h:1540`), which includes `UNDO_STATE_FX` (`:1542`). REAPER marshals **every plugin's state chunk in the project** into the undo record, on every switch **and** every tag/untag reapply. The honest mask is `UNDO_STATE_TRACKCFG|UNDO_STATE_ITEMS|UNDO_STATE_MISCCFG`, with `UNDO_STATE_FX` OR'd in only when the park/restore loop actually toggled an offline state this apply. The second `-1` at `:607` (`mintManagedLanes`) is a separate call site — assess it, do not assume it. - **Unconditional `TrackFX_SetOffline`.** `parkFxOffline` (`:190-195`) calls it on **every FX of every parked track**, including tracks already parked and already offline — unloading and re-instantiating plugins for no state change. `restoreFxOffline` (`:200-204`) writes verbatim from the snapshot the same way. Both compare before writing. - **`PreventUIRefresh` is used NOWHERE in `src/`** (verified: zero matches across the tree) though the SDK exports it (`reaper_plugin_functions.h:5583`). ~10T flag writes land with UI refresh live. Bracket the write phase, and keep the existing `TrackList_AdjustWindows(false)` + `UpdateArrange()` as the one deliberate rebuild after the bracket closes. **Balanced pairing is mandatory** — an early return between the two calls leaves REAPER's UI frozen. - **Compare before write.** `applyFlags` (`:182-186`) and the parent-visibility loop (`:521-531`) write unconditionally, so a reapply that changed nothing still pays every write. - **`resolve` is a linear scan** (`:122-128`) called once per parked / restored / parent / lane track — O(T²). `readFolderEntries` (`:103-120`) already builds `handleByGuid` and can hand back a hash map instead of a vector of pairs. - **Ω-F6, and it is the headline.** Split the synchronous apply from the deferred park: visibility, routing and FX-enable apply **inside** `applyMode`; per-FX offline/online is enqueued and drained on a later idle tick. **The spec must state what the queue does when a second switch arrives before the first's park has run** — the answer is that a queue entry is keyed by track GUID and the LATEST intent wins (an enqueued park superseded by a restore cancels, not stacks), because the two are inverses and replaying both is both slower and observably wrong. A project close, a track deletion, or a project switch drains the queue without applying it. - **Where the queue lives.** `src/shell/view/view.cpp` measures **642 lines** today and is already over the ~600 ceiling with its seam blocked (`docs/TODO.md`). The queue is therefore a **new TU beside it**, not more of `view.cpp` — `src/shell/view/` has no `CMakeLists.txt` of its own, so this costs exactly one `target_sources` line in `src/app/CMakeLists.txt` (where `view.cpp` and `view_solo.cpp` are already listed). This is the one structural decision in the track and it moves `view.cpp` toward the ceiling rather than further past it. - **The invariant amendment.** `src/shell/view/CLAUDE.md`'s "Documented caveat" bullet accepts a load hitch on the return to the active mode. After this track the hitch is still real but no longer sits on the switch's synchronous path — the caveat must say so, and must say what the deferral does NOT change (the plugin still re-instantiates; un-persisted internal state is still lost). **Acceptance criteria.** - On a project with enough tracks and FX to reproduce it, the mode switch **paints its new state before the park work runs**, and the wall-clock to first paint is recorded before and after. `[verify — DAW]`, discharged by Ω-W2-T3. - A tag/untag reapply on an unchanged project performs **zero** `TrackFX_SetOffline` calls and **zero** redundant flag writes. - The undo record for one mode switch no longer carries FX state when no offline state changed; **one switch is still one Ctrl-Z**, and Ctrl-Z still restores what it restored before. - The park/restore round trip — the phase's trust anchor — still returns every driven flag to its captured value, including when the deferral means the park lands on a later tick. - Two switches in rapid succession leave every track in the state the SECOND switch specifies, with no track left offline that should be online. - `PreventUIRefresh` is balanced on every exit path from `applyMode`, including the two early returns already there. - `view.cpp` is no longer than it is today. **Prerequisites.** None; concurrent with T2–T5. **Discharges:** Ω.7 (the structural half), Ω-F5, Ω-F6. #### Ω-W1-T2 — `mode-switch-persist` **Goal.** A mode switch stops rewriting the whole session to record that one key changed. **Spec:** Ω.7's lower-priority half; ruling Ω-F5 (both entry surfaces). **Surface boundary — owns:** `src/shell/actions/design_view_actions.cpp` (`persistViewState` and the two callers `doActivateMode` / `doToggleMode`) and `src/shell/persist/ext_state_io.cpp` (`saveToActiveProject`). **Does not own:** `view.cpp`, `session.cpp`, or any panel TU. **Behavior.** - The panel-click path routes through `Main_OnCommand` → `doActivateMode` (`panel_input.cpp:330-334`), which calls `persistViewState()` → `saveToActiveProject()` — a **seven-key** ext-state write (`ext_state_io.cpp:140-190`: banks, the legacy-key clear, view, tail, owned_files, version stamp, bank generation) plus a `MarkProjectDirty`, every switch, when only `view_state` changed. Serializing the bank book and the tracking ledger is the expensive part and neither moved. - **Gate by dirty key, not by call site.** A per-key dirty flag on the session, or a `saveViewStateOnly` entry point beside the full save — the track picks, and states why. The full save must remain the default for every other caller. - **`saveToActiveProject`'s return contract must not change.** `shell/persist/CLAUDE.md` is explicit: `false` means *no active/saved project and NOTHING was written*, and it may never grow an observational third failure mode. A narrowed write returns on the same rule. - The degraded-ledger suppression (`tracking::ledgerDegraded`) and the version stamp both stay exactly where they are on the full path. **Acceptance criteria.** - A mode switch on a saved project writes `view_state` and nothing else; a capture, a bank op and a project save each still write everything they wrote before. - `MarkProjectDirty` still fires on the narrowed path — the state DID change. - Both entry surfaces (the footer segment and the bindable action) take the same path. - The write-count difference is measured and recorded. **Prerequisites.** None; concurrent with T1. **Discharges:** Ω.7 (the persist half). #### Ω-W1-T3 — `meter-rate` **Goal.** The master meter moves at 60 FPS, and a paint costs what one frame should. **Spec:** Ω.1; no product doc. `src/shell/instrument/CLAUDE.md` already records that "a meter-rate timer remains a separate change and is not in" — this is that change. **Surface boundary — owns:** `src/shell/instrument/editor_platform.cpp` (the timer set-up and the `WM_PAINT`/`invalidate` path), `editor_session.cpp` (the meter block currently inside `onSyncTimer`), `editor_paint.cpp` (the paint entry's bitmap and dirty-rect handling), the hero-envelope cache's accessor in `editor_paint_waveform.cpp`, and `reasampler_editor.h`'s meter/timer/cache members. **Does not own:** `meter_ballistics.h`, `master_meter`, the processor, any deck or overlay module. **Ballistics need no rework.** Every constant in `core/instrument/engine/meter_ballistics.h` is per-second, and elapsed time is measured rather than assumed — the rate is a UI-cadence problem, not a ballistics one. **Nothing changes on the audio thread.** **Behavior.** - **The cause.** There is exactly ONE timer in the tree: `kSyncTimerIntervalMs = 500` (`editor_platform.cpp:33`), the bank/bake change-detection poll. The meter's advance + `invalidate` sit inside `onSyncTimer` (`editor_session.cpp:113-134`), so 2 FPS is the ceiling, and `WM_TIMER` coalescing under host load takes it below that. - **The recommended shape: a SECOND timer id on the same child window**, running only the meter drain, advance and invalidate. **The 500 ms poll body is untouched** — `pollBankSync`, `bakeAvailable`, `resolveBakeHoldNeeded` and the bake run must not speed up (a bridge read plus a bank parse each), and the tick-counted UI decays (`kBakeMessageTicks`, `dropHintTicks_`) keep their meaning for free. **The rejected alternative** is raising the shared interval and converting those decays to elapsed time: it is one fewer timer and it silently shortens two user-visible banners by ~15× if the conversion is missed anywhere. - **`masterBusMeter()` is a CONSUMING drain** (exchange-to-identity) and must keep **exactly ONE caller**, or the two clocks steal windows from each other. Moving it to the meter timer keeps that; leaving a copy behind in `onSyncTimer` breaks it. - **The meter block must stay AHEAD of the in-flight-drag guard.** On its own timer this is structural rather than positional — state it, because the reason (a drag suppresses the reload poll but the bus keeps sounding) survives the move. - **The clock resolution is a real hazard at 16 ms.** Today the elapsed measure is `GetTickCount64` (`editor_session.cpp:114`), whose granularity is ~15.6 ms — at 60 FPS the measured delta quantizes to 0 or 16 and a zero-delta advance does nothing but burn a frame. Move the meter clock to a high-resolution source off the audio thread, and skip the advance on a zero delta rather than passing it in. - **Three paint costs, all of which the higher rate exposes:** 1. `invalidate()` (`editor_platform.cpp:65-66`) dirties the **whole client area** (`InvalidateRect(hwnd, nullptr, FALSE)`) and `paint` never reads `ps.rcPaint` (`:149-155`). The meter needs its own rect invalidated and the paint needs to honour the dirty rect. 2. `paint` allocates a **full-size `LICE_SysBitmap` per paint** (`editor_paint.cpp:28`). Hoist it to a member resized on `WM_SIZE`, where `thumbCache_.clear()` already lives. 3. `paintWaveform` recomputes `computeEnvelope` over the **entire decoded PCM every paint** (`editor_paint_waveform.cpp:204-212`). The decode is cached (`channelPcmFor` / `monoPcmFor`); the hero envelope is not. Cache it keyed on sample id + bin count + lane count, busted by `refreshFromBank` and by a resize — the same discipline `marksCache_` and `thumbCache_` already use. - **Target 60 FPS.** 30 is the fallback **only** if the paint-cost work cannot get there, and choosing it requires the measurement that justifies it. **Acceptance criteria.** - The meter visibly tracks transients at the shipped rate; the measured frame interval is recorded, with the rate chosen and why. `[verify — DAW]`, discharged by Ω-W2-T3. - A meter-only frame does **not** repaint the deck, chrome or waveform bands. - No allocation in the steady-state paint path. - With no editor open, nothing changed: the meter timer is bound to the child window exactly as the sync timer is. - The bake affordance, the Hold applicability, the bake message and the drop-hint banner all behave exactly as before — same cadence, same durations. - `masterBusMeter()` has exactly one call site. - The first meter read is still DISCARDED (session history, not a window) and the clip latch is still NOT discarded with it. **Prerequisites.** None; concurrent with T1, T2, T4, T5. **Discharges:** Ω.1. #### Ω-W1-T4 — `deck-reflow-focus-toggles` **Goal.** Filter Mod sits with the envelope it modulates, the Trigger face's knobs sit at their natural pitch, a two-state toggle is one button, and clicking an envelope deck anywhere selects its overlay. **Spec:** Ω.2, Ω.3, Ω.4 and rulings Ω-F3, Ω-F4. Deck composition law: `src/core/instrument/ui/CLAUDE.md`'s `knob_deck` and `deck_groups` bullets. **Surface boundary — owns:** `src/core/instrument/ui/deck_groups.{h,cpp}`, `knob_deck.{h,cpp}`, `deck_values.cpp` (the toggle commit seam only), `src/core/instrument/ui/sample_chrome.{h,cpp}`, `src/shell/instrument/editor_paint_deck.cpp`, `editor_input_deck.cpp`, `editor_paint_chrome.cpp`, `editor_input_chrome.cpp`, `editor_input.cpp` (the focus-clear arm of the mouse-down dispatch only), `editor_controls.cpp` (`faceLayout`'s `deckDescs` build only), and `tests/test_deck_groups.cpp`, `tests/test_deck_groups_measured.cpp`, `tests/test_knob_deck.cpp`. **Does not own:** `waveform_view`, `envelope_overlay`, `spline_edit`, or any `editor_*_waveform` TU (T5's), and **no `param/` file whatsoever**. **Behavior — Ω.2, the move.** - `sampleDeckGroups` (`deck_groups.cpp`) is the sole deck-composition authority. Move `kFilterModAmt` out of FILTER's `cellIds` (`:62-68`) into FILTER ENV's (`:80-89`), **in both faces**, landing **last in the run** — the `kPitchEnvDepth` precedent (`:50-53`). - **NO VST3 parameter-id change.** `kFilterModAmt` keeps its frozen id (`kParamFilterModAmount` = 1240) and its unit; ids are keyed on `DeckParam`, never on deck group. `param_id.h`'s header comment says the blocks run "per deck group in SIGNAL-FLOW order" (`:34`) — that becomes untrue for exactly one row. **Annotate it; never renumber.** The frozen-table rule is the whole reason the annotation is the correct fix. - **This move costs the filter tie-line, and that cost is stated rather than discovered.** Today `test_deck_groups_measured.cpp` pins both rows' filter groups to one right edge (x=640 block-relative) — the only reason the row block was widened 1020→1028. After the move the Sound row loses 60 px and the Contour row gains 60, and with the two filter groups then equal in width the tie is **unreachable at any block width** under the space-between law. The alignment goes; the ruling stands. Re-derive that fixture from the new contract, record the loss in it, and **do not change `kDeckRowBlockW` or the editor floor to chase it** — revisiting the floor is a separate proposal, not this track's to make. **Behavior — Ω.2, the spacing.** - The defect is a **layout-algorithm artifact, not a constant**. A group's cell run is a reserved width, and a `-1` reserve donates its slot to the surviving cells (`knob_deck.cpp:123-138`), so Trigger's FILTER ENV gets ~100 px cells and AMP ENV ~75 against the standard 60. Gate is uniform at 60 because it has no reserves. - **The law: cells stay at `kDeckCellW`; the run is CENTRED in the reserve.** It is the only candidate satisfying both of Daniel's constraints — group width stays stable across a mode flip (which is why the reserves exist) and knobs keep their natural pitch. Gate is then pixel-identical by construction, because with no reserves the centred run and the divided run are the same run. - **The two rejected laws, recorded so they are not re-proposed:** *(a) drop the reserves and let the group narrow in Trigger* — reflows every neighbour on a mode flip, which is the thing the reserves were introduced to prevent; *(b) keep the division but cap the cell width* — leaves an asymmetric residue at one end and re-introduces the dead-slot look the division law replaced. - A consequence worth stating: once the run is centred, **where a `-1` sits in `cellIds` stops mattering** — only the count does. The reserves become pure width accounting. **Behavior — Ω.3, single buttons.** - Today every deck toggle is structurally two segments (`DeckToggleDesc` `knob_deck.h:69-72`, `placeToggle` `knob_deck.cpp:82-94`, `hitTestDeck`'s seg0/seg1 `:259-268`, `drawToggle` `editor_paint_deck.cpp:117-137`). **Two variants replace it:** - **Enable** — one button, label names what it controls, **`AccentPrimary` when active and dim gray when off** (Ω-F3, Ω-F4). Applies to: the envelope enable (`kPitchEnvEnable`, today `Off|On`) → **"Envelope"**; `kFilterEnable` → **"Filter"**; `kLimiterEnable` → **"Limiter"** (it currently names nothing it controls); and the chrome row's `Loop Off|Loop On` → **"Loop"**. - **Mode selector** — one button whose **label reads the current mode**, with **no dim-gray state** (Ω-F3). Applies to: the three `Stg|Spl` env-mode toggles → **"Stage"/"Spline"**, and the chrome row's `Mono|Stereo`. - **A third visual state survives and must not be collapsed into "off":** a control refused by the mode (the chrome Loop enable in Trigger, `kFilterLaw` while the filter is off) draws **Disabled and inert**. Off is live and clickable; Disabled is not. - **Explicitly NOT converted, and that is a boundary rather than an oversight:** `kPlayMode` (`Gate|Trigger`), `kPitchEngine` (`Varisp|Presrv`), `kVoiceMode` (`Poly|Mono`), `kFilterLaw` (`Band|Notch`) and `kMonoTrigger` (`Retrig|Legato`) keep their two-segment form. Daniel named a set; this track ships that set. The mode-selector variant generalizes to them the day he asks. - **Three load-bearing risks the implementation must answer:** 1. **A single button carries no `segment`**, so the commit must derive the NEXT state from the current one. `setDeckParam`'s `segment == 1` contract (`deck_values.cpp:135-162`) is the seam; `nextOverlaySelection` (`deck_groups.cpp:298-302`) is the existing precedent for computing a next discrete state purely. 2. **`kLimiterEnable`'s split commit must survive VERBATIM** (`editor_input_deck.cpp:67-79`): it commits the parameter set and calls `setLimiterEnabled`, which only ARMS the host's `restartComponent(kLatencyChanged)` for the sync tick to deliver. Nothing on that path may start calling into the host from inside a mouse handler. 3. **MASTER has ZERO caption slack** — its caption row (46 + toggle + radio) and its knob row (`kDeckCellW` + `kDeckColumnGap` + `kMeterColumnW`) both measure 130, which is exactly `kDeckSpanningW` − 2·`kDeckGroupPadX`. **So the "Limiter" button must be ≤ 64 px** (what the two 32 px segments occupy today). Wider and `kDeckSpanningW` grows, which pushes `kEditorMinWidth` into the 82 px budget against `kEditorCeilingWidth`. **Every other group absorbs a wider button free** — verified: each is knob-row-bound with 50+ px of caption headroom. - **`hitTestKnobFace` runs no toggle-precedence pass** (`knob_deck.h:217-224`) and is only correct while no toggle rect overlaps a knob circle. Any button that grows must be re-checked against that, or given the same precedence order. **Behavior — Ω.4, the deck as the overlay target.** - Remove the three overlay radios from the env groups' descriptors. **`DeckRadioDesc` itself must SURVIVE** — MASTER uses a passive one for the GR lamp (`deck_groups.cpp:140`), and the `passive` skip in `hitTestDeck` (`:255-258`) is what keeps a readout from growing a gesture. - Clicking anywhere in an envelope deck — panel background, knob, or button — focuses that envelope's overlay. The focused deck takes an **`AccentPrimary`** border (Ω-F4). Focus clears when the user clicks a control surface outside it; **the overlay itself is excluded from stealing or clearing focus**, so editing the thing you selected does not deselect it. - **Five design constraints, each of which breaks something if missed:** 1. The focus write must land **before** the `hit.kind` switch in `mouseDownDeck` (`editor_input_deck.cpp:31-37`) and must **not consume the click**, or every knob grab and every toggle commit on a focused deck dies. 2. **`DeckHit` carries no group id** (`knob_deck.h:190-195`). Either the shell scans `dl.groups` for containment or `DeckHit` gains one — the latter is the smaller lie. 3. **`hitTestDeck` returns a miss for a click on group padding** (`knob_deck.cpp:252-278`), which is exactly the "deck panel background" Daniel wants to hit. Containment against `g.box` is the test, not the hit kind. 4. **A deck click SETS focus idempotently; it does not toggle to none.** Otherwise every knob tweak on the focused deck unfocuses it. This retires `nextOverlaySelection`'s re-click-clears branch (`deck_groups.cpp:301`) and replaces the radio-id map with a group-id one. 5. **Refuse focus changes while a drag is in flight**, and keep `overlayEnv_` transient view state with **no persistence path** — it is what is drawn, not what is played. **Acceptance criteria.** - **The Gate deck face is pixel-identical to today**, group boxes and cell rects both, asserted in `test_deck_groups_measured.cpp` — except for FILTER and FILTER ENV, whose widths move by exactly one cell in opposite directions. - Trigger's FILTER ENV and AMP ENV cells measure `kDeckCellW`, centred in their reserves, and each group's box width is **identical in both modes**. - `kFilterModAmt`'s `ParamId` is unchanged; `exposedParams()` returns the same list in the same order; no `param/` file is edited. - Every converted control's commit produces the same audible result as the segment it replaced, including the limiter's latency-restart arm. - `kEditorMinWidth` and `kEditorCeilingWidth` are unchanged, and the 82 px budget is unspent. - Clicking a knob, a button and the bare panel of an envelope deck all focus that overlay; the first two still perform their own action; clicking the overlay does not change focus. - The deck's pure fixtures cover the new composition, the centred run and the focus map — the shell layer is not where any of the three is asserted. **Prerequisites.** None; concurrent with T1, T2, T3, T5. **Discharges:** Ω.2, Ω.3, Ω.4, Ω-F3, Ω-F4. #### Ω-W1-T5 — `overlay-mapping` **Goal.** The loop marks say what the mode means, and every overlay that rides the waveform lands on the frame it names. **Spec:** Ω.5, Ω.6 and rulings Ω-F1, Ω-F2; `docs/product/instrument-control-surface.md` §6.3(d) and §6.4 **as amended by Ω-F2**. **Surface boundary — owns:** `src/core/instrument/ui/waveform_view.{h,cpp}`, `src/shell/instrument/editor_paint_waveform.cpp` (the mark, caption and overlay regions), `editor_input_waveform.cpp`, `editor_controls.cpp` (`waveMarksFor` / `grabbableMarks` only), `src/core/instrument/ui/spline_edit.h`'s `splineOverlayBox` and `envelope_overlay`, and `tests/test_waveform_view.cpp` + `tests/test_component_geometry.cpp`. **Does not own:** any deck module or `editor_*_deck` TU (T4's), the envelope-cache region of `editor_paint_waveform.cpp` (T3's), or — **without an explicit proposal** — `src/shell/panel/draw_kit.cpp` and `src/core/ui/component_geometry.cpp`. **Behavior — Ω.5, the marks.** - **Trigger ⇒ the loop pair and the crossfade mark are HIDDEN entirely.** Not Disabled, not dim: absent. **Gate with loop off ⇒ they are still drawn and still grabbable**, styled **grey disabled** rather than today's dim teal at `kMarkAlphaDisabled` (`editor_paint_waveform.cpp:51-53, 285-305`). The drag-to-set-loop gesture survives, which is the whole reason they stay grabbable. - **The `"LOOP \xe2\x80\x94 GATE ONLY"` caption (`editor_paint_waveform.cpp:254`) is removed unconditionally** — in Trigger there is no span left to centre it in. - **The two sibling captions SURVIVE** (`:255`): `DRAG TO SET LOOP` when the pair is parked and `LOOP OFF` when a span is retained. Their box derives from the marks' span, which still exists in Gate, and they carry the one thing the grey styling cannot — *which* off state you are in, and that dragging will fix it. **This is a [propose]-class call made here rather than left open**; reversing it means deleting information, not styling. - **The seams.** `waveMarksFor`'s `present[]` (`editor_controls.cpp:145`) is the correct place to suppress — it also skips label layout. `grabbableMarks` (`:162`) is the separate grabbability fold and must NOT follow it into hiding for the Gate-off case. - **Warning to the track:** `resolveWaveformClaim`'s smallest-area arbitration and `capAtPoint`'s reverse-order resolve (`editor_input_waveform.cpp:65, 89, 111, 212`) are both sensitive to marks entering and leaving the set — `capAtPoint`'s reverse order exists precisely so a coincident pair stays separable and the crossfade (the one mark with no column) can never be shadowed (`core/instrument/CLAUDE.md`, `waveform_view`). Changing which marks are in the set changes what those two resolve, which is the regression risk in this half of the track. **Behavior — Ω.6, the mapping. This is a CLASS defect, not a START-marker instance.** Two chains that do not agree, verified this pass: | | Marker / overlay chain | Waveform draw chain | |---|---|---| | Area | `waveformOverlayArea` returns the **FULL band, no inset** (`waveform_view.cpp:23-25`) | insets **2 px each side**; `waveformColumnCount` = `width − 4` (`component_geometry.cpp:95-98`), columns start at `box.x + 2` (`draw_kit.cpp:292, 311`) | | Domain | `[0, N]` **closed** (`clampFrame`, `waveform_view.cpp:15-19`) | `[0, N)` **half-open** (`columnMinMax`, `peaks.cpp:69-71`) | | Rounding | **round-half-up** (`frameToX`, `:51-58`) | **truncation** | | Phase | a 2 px marker line centred on an integer (`editor_paint_waveform.cpp:298, :301`) | a 1 px column centred at +0.5 | Four independent discrepancies. The 2 px inset dominates — frame 0's marker sits 2 px left of the first column and the error reverses sign toward the end — and the closed domain puts the last frame one pixel outside the band. **Everything riding the overlay inherits all four:** the loop span fill, the crossfade wedges, the staged envelope polyline, and the spline contour — whose `spline_edit.h` guarantee that it uses "the FULL area, no inset, so the drawn contour stays 1:1 with the sample's time axis" **is currently false by 2 px.** - **Mandate ONE mapping both chains read**, so the drift cannot recur. Two routes: - **Inset the overlay** (recommended) — instrument-only, and the overlay is already the distinct `OverlayArea` type, so the change has one construction site. - **De-inset the waveform** — `[propose]`, **requires justification before it is taken**: it touches the shared `draw_kit`, which also serves the docked bank panel and the browse-card thumbnails, so its blast radius is three surfaces rather than one. - **The zero-crossing snap must be preserved EXACTLY.** `nearestZeroCrossing` (`waveform_view.cpp:170-199`) fans out symmetrically, probing `t−d` before `t+d` so an equidistant tie resolves to the lower frame; it runs on the mono cache at drag time and is exempt for the crossfade handle. **Behaviour-identical after the change** is an acceptance criterion, not an assumption. - **One question the track must ANSWER, not dodge:** what "sample-accurate" means when `frames < columns` and one frame spans many pixel columns. A marker must land somewhere inside its own frame's column span; which end, and whether the inverse `xToFrame` (`:60-70`) still round-trips, are the track's to specify. - **The two pinned fixtures are re-DERIVED from the new contract, never re-baselined.** `test_waveform_view.cpp` and `test_component_geometry.cpp` pin the current mapping by construction; a test updated to match whatever the code now prints has stopped being a test. **Acceptance criteria.** - One mapping function serves both chains; a second one anywhere in the tree fails review. - Every mark, the loop span fill, both crossfade wedges, the staged polyline and the spline contour land on the same pixel as the waveform column for the frame they name — at frame 0, at the last frame, and at an interior frame, asserted in the pure fixtures. - `frames < columns` and `frames > columns` are both covered. - The zero-crossing snap returns the identical frame for the identical input, ties included. - Trigger shows no loop marks and no loop caption; Gate-with-loop-off shows grey marks that still drag, and dragging one still turns the enable on. - `docs/product/instrument-control-surface.md` §6.3(d) and §6.4 agree with the shipped behaviour. **Prerequisites.** None; concurrent with T1–T4. **Discharges:** Ω.5, Ω.6, Ω-F1, Ω-F2. --- ### Ω-W2 — What the measurements say, and what only a DAW can **Depends on:** Ω-W1 in full. **Three tracks.** T1 is **measurement-gated** — it may turn out not to be worth doing, and that is a legitimate outcome. T3's deliverable is a **RECORD, not a fix** (the Λ-W3 precedent). | Track | Owns | |---|---| | **T1** `view-topology-cache` | `src/core/view/` — tree / `parentOf` / handle-map memoization | | **T2** `content-detect-throttle` | `src/shell/panel/panel_input.cpp` — the 30 Hz detector's cadence | | **T3** `omega-verification-sweep` | `docs/VERIFICATION.md` — the DAW record for all of Ω-W1 | **Shared files in the wave:** none. The three tracks touch three disjoint directories. #### Ω-W2-T1 — `view-topology-cache` *(measurement-gated on Ω-W1-T1's result)* **Goal.** Stop rebuilding the project's folder topology from scratch on every apply — if, after Ω-W1-T1, that is still a cost worth paying for. **Spec:** Ω.7's residual. **Surface boundary — owns:** `src/core/view/` (the folder tree, `parentOf`, and the GUID→handle map's shape). **Does not own:** `shell/view/view.cpp` — Ω-W1-T1 owns it, and this track lands after. **Behavior.** `readFolderEntries` + `buildFolderTree` run on every apply, and `visibleTracks` walks the tree per parent. **The gate:** Ω-W1-T1 removes the undo-mask cost, the redundant FX writes and the O(T²) resolve; if the remaining apply time is already inside the frame budget on the measured project, **this track is not dispatched** and its finding is recorded instead. `docs/TODO.md` already carries `view_mode_model.cpp` at 815 lines with a named codec seam — do not take that seam here, and do not grow that file. **Acceptance criteria.** - A before/after measurement on the same project decides the track, and the number is recorded either way. - If taken: membership, reconcile and the park/restore round trip are all unchanged, and the cache is invalidated by every path that can change track topology. - `core/view` stays pure and CTest-covered. **Prerequisites.** Ω-W1-T1 landed and measured. **Discharges:** Ω.7's residual, or records it as not worth taking. #### Ω-W2-T2 — `content-detect-throttle` **Goal.** The auto-tag detector stops spending main-thread time 30 times a second on exactly the projects where the switch is slowest. **Spec:** Ω.7's third cost. **Surface boundary — owns:** `src/shell/panel/panel_input.cpp` (`detectNewContent` / `enumerateLiveGuids` and their cadence inside `bankPanelRefresh`). **Does not own:** any other panel TU, `view.cpp`, or the auto-tag RULES — only how often they are asked. **Behavior.** `detectNewContent` (`panel_input.cpp:146`) runs `enumerateLiveGuids` (`:106-135`) every tick of REAPER's `"timer"` registration (`main.cpp:341`, ~30/sec), regardless of panel state — O(T + I) REAPER string calls plus ~4(T+I) allocations per tick, on every project. Throttle the ENUMERATION, not the detection semantics: the same GUIDs must still be detected, the first-poll and `reloadPending` re-baseline guards must still fire before any diff, and the lane-minting pass must still run only on a tick that tagged. A slower cadence is a latency change to an invisible background tag, and the track states the chosen interval and why. **Acceptance criteria.** - New tracks and items are still auto-tagged into the active mode, and a project reload still re-baselines rather than mass-tagging. - The Ρ-W1-T1 rule survives: an added GUID that already carries a membership record is still dropped, so a render-in-place result is not re-tagged to Design. - The per-tick allocation count is measured before and after. - The panel's own repaint cadence is unaffected. **Prerequisites.** None beyond Ω-W1. **Discharges:** Ω.7's polling cost. #### Ω-W2-T3 — `omega-verification-sweep` **Goal.** The DAW-observable half of this phase is recorded rather than assumed. **Spec:** every `[verify — DAW]` criterion in Ω-W1. **Surface boundary — owns:** `docs/VERIFICATION.md` (a Phase-Ω section). **Does not own:** any source file. **A fix found here opens a track; it is not applied inside this one.** **Behavior.** One session, on a project big enough to reproduce Ω.7 (the track records how big that turned out to be, because that number is itself a finding). Covers: switch-to-first-paint before and after; two switches in rapid succession; the park/restore round trip after deferral; Ctrl-Z after a switch; the meter's frame interval and its behaviour under host load; the Gate deck face against a pre-Ω screenshot; the Trigger face's spacing; every converted button in all three states; deck-click overlay focus; the marks in Trigger and in Gate-with-loop-off; and a marker aligned against the waveform at both ends of a sample. **Acceptance criteria.** - Every `[verify — DAW]` mark in Ω-W1 is discharged by a recorded observation or explicitly carried forward as unrun. **Inspection is not an observation.** - The two performance numbers are recorded with the machine and project they were measured on. **Prerequisites.** All of Ω-W1 landed. **Discharges:** Ω's DAW criteria. ### Shared files across Ω's waves, named rather than discovered at merge All textual adjacency unless marked otherwise. Within Ω-W1 the wave's own table above is the authority; this one covers the cross-wave and cross-phase cases. | File | Tracks | Nature | |---|---|---| | `src/shell/view/view.cpp` | Ω-W1-T1 alone | **Deliberately not split.** Already 642 lines and over the ceiling with its seam blocked; T1's new code goes in a new TU beside it rather than into it | | `src/core/view/*` | Ω-W2-T1 alone | Different wave from T1's shell edits, and gated on them | | `src/app/CMakeLists.txt` / `src/app/main.cpp` | Ω-W1-T1 | **Cross-phase:** Λ owns other regions of both (Λ-W2-T1/T2/T3, Λ-W4-T3, Λ-W5-T1). Ω lands first; Λ rebases | | `src/shell/panel/panel_input.cpp` | Ω-W2-T2 | **Cross-phase:** Ρ-W1-T1's `detectNewContent` edit is landed and is the same function — read what shipped, not what was specced | | `docs/VERIFICATION.md` | Ω-W2-T3 | Λ-W3-T1 writes its own Linux record into a different section | --- ## Phase Λ — ReaSampler on Linux: both artifacts, shipped **Ships:** `reaper_reasampler.so` and `reasampler_9000.vst3` built, installed and documented on Linux. The extension compiles under GCC/Clang, builds optimized by default, docks its panel, writes a bank index that survives a comma-decimal locale, and prunes without lying about what it reclaimed. The instrument loads, scans, instantiates and processes audio in **any** Linux host, and opens its editor under **REAPER only** — a stated contract, not a shortfall. macOS is out. **Four named exceptions to platform parity**, stated up front rather than discovered: prune deletes permanently (no trash); the OS drag-out's copy-only guarantee is conventional rather than structural, because SWELL's file-list drag takes no effect mask; the type faces are DejaVu rather than Segoe UI and Consolas; and the ReaSampler 9000 editor comes up under REAPER only, with every other host getting its generic parameter UI. **Consolidates: none of the seventeen.** Phase Λ came from a direct request (Daniel, 2026-08-02) and is scoped in `docs/product/linux-readiness.md`, which is itself downstream of two landed audits: `docs/product/audit-notes/lambda-w1-t1-build-toolchain.md` (findings Λ-01…Λ-10, verify items V1…V10, forks D1…D7 — build system, toolchain, vendored deps, resources, test harness, packaging) and `docs/product/audit-notes/lambda-w1-t2-source-runtime.md` (L2-01…L2-12 — source portability and runtime behaviour). **Grep those for a cited finding number; do not read either whole.** This phase **supersedes nothing**, and corrects one standing promise rather than inheriting it: `docs/product/versioning-and-release.md` already commits in writing to "three platform artifacts per channel per release" (T1 §1e), which Λ-D4 makes **two** for now, with macOS named as deferred rather than silently dropped. **Nothing in this phase has been verified on a Linux machine.** Every `[verify — Linux]` mark below is load-bearing and none may be laundered into settled voice. The structural consequence is stated once here because it shapes the whole wave order: **Λ-W2's edits are authored blind from the audits' citations, and Λ-W3 is the session that discharges Λ-W2's acceptance criteria.** Λ-W2 is not "done" in the usual sense until Λ-W3 runs, and a track that reports otherwise has laundered unverified work into landed work — which is exactly what the audits' `[verify]` discipline exists to prevent. Both audits state their own effort bands as provisional until the sweep runs; this plan does not restate a band the audits did not give. ### Rulings — Daniel's, 2026-08-02. Six, settled. Full statements with the reasoning Daniel gave: `docs/product/linux-readiness.md` §"Settled decisions (Daniel, 2026-08-02)". **Do not re-litigate these.** | Ruling | What it settled | Specified in | Bound into | |---|---|---|---| | **Λ-D1** | **The VST3 instrument is in scope** (audit fork D1 = **Fork B**). Both artifacts ship, not just the extension. **Reverses D5's platform clause in writing** — "Windows-only, VST3-only, REAPER-only" becomes "Windows and Linux; the editor is REAPER-hosted"; the VST3-only and REAPER-only clauses survive verbatim | §Λ-D1 | Λ-W6…Λ-W8; the D5 rewrite is Λ-W6-T1's first act | | **Λ-D2** | **SWELL is reached by `dlopen`ing REAPER's own `libSwell.so`** (route **B3a**). Route B3b — building SWELL into the module, and with it GDK/GTK3, FreeType, Fontconfig, OpenGL — is **rejected**, not deferred | §Λ-D2 | Λ-W7-T1 | | **Λ-D3** | **Non-REAPER hosts must be protected** — *"REAPER-only is good for now, but we need to protext other hosts."* A first-class acceptance criterion of two tracks, not a footnote on one | §"The non-REAPER-host safety criterion" | Λ-W6-T2 (contract), Λ-W7-T1 (runtime), Λ-W8-T1 (regression) | | **Λ-D4** | **macOS is out** (D3 = **out**) — *"I don't have a mac to compile on."* Shared macOS/Linux edits are still made **in their shared form and noted as shared**; what is out is macOS as a deliverable, any macOS verification, signing/notarization, and L2-11's APFS half. **Do not "fix macOS while you're in there"** — an unverifiable edit to the APPLE branch is worse than none, because it looks tested | §Λ-D4 | phase-wide | | **Λ-D5** | **The Linux artifact is shipped, not developer-only** (D6 = **shipped**), which promotes Λ-02 (a real optimized build) and Λ-08 (a documented install path) to must-fix. **And a hard `unlink` prune is acceptable**, with a **platform-aware confirmation string** saying so | §Λ-D5 | Λ-W2-T2, Λ-W4-T1, Λ-W5-T1 | | **Λ-D6** | **The X11 editor is real work and is sequenced last.** Not a separate ruling — the direct consequence of D1+D2+D3. Everything before it ships something; halting Λ-W8 still leaves a shipped extension and a loading, processing, generic-UI instrument | §Λ-D6 | the wave order itself | ### Open forks — four, NONE ruled Stated with the evidence for both sides at `docs/product/linux-readiness.md` §"Open forks — Daniel's". This is the one phase in this plan with unanswered [Daniel]-class questions; see "Decision state" above for how that reconciles against the plan-wide claim. | Fork | Question | Recommendation | What it blocks | |---|---|---|---| | **Λ-F1** | Does CI get built in this phase, and on what runner? (audit D4) | **No recommendation — genuinely a resourcing call.** For: two toolchains and one is not on the developer's machine, so every Windows-only commit becomes a latent Linux regression. Against: CI is infrastructure, its value is highest *after* the first Linux build works, and a runner forces Λ-F4 immediately | **Nothing.** Decides only whether Λ-W5-T1 writes a pipeline paragraph or a "deferred to dev-ops" one | | **Λ-F2** | The dialog-resource route: **resource-id-0** (SWELL's documented escape hatch, `swell-functions.h:606–608`; deletes the whole resgen pipeline from non-Windows builds) or **wire up resgen properly** (PHP, `add_custom_command`, an include-shim TU — the M end of Λ-01's band)? (audit D7) | **No clean recommendation; the asymmetry neither audit stated is the reason it is worth ruling.** Route A is cheaper and structurally simpler and stakes the panel's file-drop ingest on an *unverified* `ChildWindowFromPoint` descent, because with no resource there is no `WS_EX_ACCEPTFILES` bit to set (L2-07). Route B can set that bit explicitly. **A third option, if the cheap route is wanted without the exposure:** take Route A and make the file-drop check a hard gate in Λ-W3's sweep, with Route B as an additive follow-up track rather than a rewrite — it spends a second Linux session in the bad case | **Λ-W2-T3's dispatch — the ONLY fork here that gates a track.** Unruled, that track slips to Λ-W4 and Λ-W3's sweep splits into two Linux sessions | | **Λ-F3** | Does "copy-only is structural" survive as a shipped invariant? (T2 [Daniel] 4) | **Keep the invariant, scope the *enforcement* claim to Windows** — "structurally enforced on Windows via `DoDragDrop`'s copy-only mask; advertised, not enforced, on SWELL, which takes no effect mask." The real counter: an invariant one platform cannot enforce is arguably not an invariant, and weakening the global wording would also correctly warn a Windows reader off relying on it in shared code | **Nothing.** Λ-W4-T3 makes the edit either way; only the sentence changes. Answerable at implementation review | | **Λ-F4** | What is the declared support floor? (new — the audits raised the inputs Λ-07, V8, V10, not the decision) | **Declare a narrow floor — current-stable-distro glibc, GCC ≥ 9, `x86_64-linux` only — and widen it on request.** Counter: REAPER's Linux reputation is partly built on modest and non-x86 hardware, `aarch64` is no longer exotic, and adding it later means a second bundle directory and a second verification pass on every release | **The ship wave, not a dispatch.** Non-gating for Λ-W2 (a recorded floor can be widened); **gating for Λ-W5-T1** — a shipped artifact has to say what it runs on. Also feeds Λ-W6-T1's bundle directory set and Λ-F1's runner image | ### Phase-Λ acceptance criteria These bind every track in this phase, in addition to the plan-wide set above. - **Windows behaviour does not change. Every track carries that criterion explicitly.** This is a port; a port that improves Windows by accident has also changed Windows by accident. - **`[verify — Linux]` is a status, not a decoration.** A criterion carrying it is discharged by a recorded observation in Λ-W3's verification record — never by inspection, never by "should work". A track whose Linux criteria are all unrun is not landed; it is authored. - **No vendored file is patched.** `git status` under `vendor/` stays clean through the whole phase, including the SWELL bootstrap — which is precisely the constraint that shapes it. - **No new third-party dependency surface.** Λ-D2 rejected route B3b for this reason: no vendored SWELL build, no GDK/GTK3, no FreeType, no Fontconfig, no OpenGL, no `pkg-config` in this tree. **A track that finds itself reaching for one of those has drifted and stops.** - **Shared macOS/Linux edits are made in shared form and labelled shared** (Λ-D4). Λ-01, Λ-03, Λ-04, Λ-07, Λ-09 and L2-09 are all shared by the audits' own marking; an `#ifdef _WIN32` / `#else` that is right for both costs nothing and needs no mac. No macOS verification is claimed for any of them. - **`core/` stays pure and stays platform-neutral.** Every `#include` under `src/core/**` is a `core/` sibling, one of 26 standard headers, or the generated `version_generated.h` (T2 §1.1) — the audits verified this rather than assuming it, and no Λ track may be the one that breaks it. The one platform fork in the whole directory (`capture_paths.cpp:18–20`, the Windows case-fold) is already correct for Linux with both branches asserted by `tests/test_capture_paths.cpp`. - **Every pure module gets a `_tests` target** that runs without REAPER or a DAW. Λ-W2-T4 is the phase's proof that this is not ceremonial: it is a `core/`-only fix, fully testable on the current Windows box, and it lands before any Linux session. - **The ~600-line ceiling and the structural heuristics bind unchanged.** Λ-W8-T1 is the track most likely to strain them; its own criteria name the seam vocabulary to reuse. **Performance posture.** No named hot path is touched by any track in this phase — not the `peaks` envelope compute, not audition, not the realtime-capture tick's single-pointer-test idle fast path, not the instrument's `process()`. Two consequences are stated as criteria rather than left implicit: **nothing is added to `process()`** (no `dlopen`, no probe, no platform branch on the per-voice-per-sample path — the SWELL availability probe is computed once, lazily, off the audio thread), and **Λ-W2-T4's locale fix stays on the JSON/persist path**, which root `CLAUDE.md` already declares off all hot paths. Λ-02 is the phase's one genuine performance item and it runs the other way: today a Linux build carries **no `-O` flag at all**, on a tree whose `peaks` path is documented as presuming an optimizing build. ### The non-REAPER-host safety contract Λ-D3 stated as something a person can check. **This is an observable contract, and it is the specification the two instrument waves are graded against** — the full argument is `docs/product/linux-readiness.md` §"The non-REAPER-host safety criterion", which a brief for Λ-W6-T2, Λ-W7-T1 or Λ-W8-T1 must be written against. **The finding that makes it urgent, and it is a fact rather than a risk.** The vendored `vendor/WDL/WDL/swell/swell-modstub-generic.cpp` declares a **file-scope static** at `:125`, so its constructor runs when our `.so` is `dlopen`ed — i.e. **during the host's plugin scan** — and `:102` calls **`exit(2)`** when `dlopen` of `libSwell.so` fails, `:117` **`exit(1)`** on an incomplete API table. Compiling the vendored `SWELL_LOAD_SWELL_DYLIB` path unmodified therefore means **a Bitwig or Ardour plugin scan on a machine without `libSwell.so` terminates the host process** — a killed DAW mid-scan, with the user's session. The mitigating detail that shapes the fix: `doinit` substitutes a zero-returning `dummyFunc` for each unresolved name, so a *partial* table degrades rather than crashes. A partial load is survivable; `exit()` is not. **In any Linux host, with or without `libSwell.so`:** the module scans and enumerates its one class with no crash, hang, process exit or blacklist entry; it instantiates, produces audio, plays MIDI, and round-trips component state byte-identically with the Windows build; when no usable platform surface exists `isPlatformTypeSupported` returns `kResultFalse` for **every** type — including `kPlatformTypeX11EmbedWindowID` — and `createView(kEditor)` returns **`nullptr`**, never a view that then fails to attach and never one that draws nothing; the host falls back to its generic parameter UI; exactly **one** diagnostic line per process names why the editor is unavailable, through the SDK's logging or stderr, never a modal; teardown crashes nothing and leaves no partially-initialised SWELL table reachable. **Verified with a harness that is already on disk and that neither audit named**, because neither swept the SDK's `samples/` tree: `vendor/vst3sdk/public.sdk/samples/vst-hosting/validator/` is a scriptable, headless, REAPER-free host, under `public.sdk/` and therefore pulled by the documented narrow submodule init. The four checks, all `[verify — Linux]`: `validator` completes with **no `libSwell.so` on the filesystem** (the direct negation of the `exit(2)` finding, and the load-bearing one); a real Ardour or Bitwig scan reaching browser, instantiation and generic UI; a REAPER-on-Linux load where the editor opens; and a **negative control** — rename `libSwell.so` beside a Linux REAPER and confirm the generic-UI fallback plus the single diagnostic line. ### Cross-phase boundary — Γ-W4-T1, and the frozen parameter table **Λ must never register a VST3 parameter.** Γ-W4-T1's `ParamID` table is FOREVER-FROZEN from the moment it ships, on the same footing as the command-id strings and the class UIDs. A parameter minted in Λ to make a Linux fallback look better would collide with a table Λ does not own. **This is a hard boundary and it is the only Γ↔Λ interaction that could actually go wrong.** Neither phase blocks the other. The host's fallback for a plug-in reporting no usable editor is its generic parameter UI, and today the plugin registers zero parameters — so "no editor" currently degrades to *nothing* rather than to *controls*. **Λ-W6-T2's acceptance criteria are written to pass with zero parameters registered**: an empty generic UI is a pass. Once Γ-W4-T1 lands its 44 derived parameters the identical Λ code path degrades to a usable generic UI instead — a better result, not a different criterion. **Λ assumes no schedule for Γ**; a Λ track needing a Γ fact reads `dev` at dispatch time. **Concurrency.** Λ is the widest phase in this plan by file surface, and it is disjoint from the others by *kind* rather than by directory: its edits are platform guards, CMake, docs and one new `shell/instrument/` TU. Γ owns `core/instrument/` + `shell/instrument/` **sources** (Λ-W6…Λ-W8 own that directory's CMake, its `CLAUDE.md` files, and `editor_platform`'s non-Windows branch); Ε lands in the new `core/package/` + `shell/package/`; Ρ touches `core/capture`, one new `shell/capture` TU, `panel_input.cpp` and `main.cpp`. **The genuine adjacency to watch is CMake**: Ε appends two `add_subdirectory` lines to the root `CMakeLists.txt` while Λ-W2-T2 owns that file's language and target settings — append-only lines against property blocks, textual adjacency rather than semantic contention. Λ's own shared files are named in the wave sections below. --- ### Λ-W1 — The audits *(complete)* Two tracks, both landed: **T1 `build-toolchain-audit`** (Λ-01…Λ-10, V1…V10, D1…D7) and **T2 `source-runtime-audit`** (L2-01…L2-12). Recorded so the wave numbering matches the audit filenames and branch names. Nothing to re-spec; nothing to dispatch. --- ### Λ-W2 — Make it buildable, and make it honest **Depends on:** nothing. **Four tracks, disjoint at the file level.** Three are authored blind against the audits and verified in Λ-W3; **T4 is the one track fully verifiable on the current Windows box.** | Track | Owns | |---|---| | **T1** `linux-compile-blockers` | two source blockers + `main.cpp`'s API-load failure branch | | **T2** `toolchain-floor` | the CMake files' language, property and platform blocks | | **T3** `panel-dialog-resource` | `panel_window.cpp`'s dialog creation, `resource.rc` / `resource.h` — **gated on Λ-F2** | | **T4** `locale-independent-numerics` | the number codec in four `core/` TUs | **Shared file in the wave, named rather than discovered at merge:** `src/app/CMakeLists.txt` — T2 takes the property and platform blocks, T3 takes **one `target_sources` line under the resgen route only**. Disjoint regions; whichever lands second rebases. The wave's severity question is dissolved rather than adjudicated: T2 grades L2-03 a Blocker on failure-mode quality with no direct evidence and L2-04 a Major with a confirmed mechanism, and flags its own inconsistency — both land here, in different tracks, so the relative grade never has to be settled. #### Λ-W2-T1 — `linux-compile-blockers` **Goal.** The extension compiles and links under GCC/Clang, and when it refuses to load it says why instead of vanishing. **Spec:** `docs/product/linux-readiness.md` §Λ-W2-T1; findings L2-01, L2-02, L2-03. **Surface boundary — owns:** `src/shell/panel/draw_kit.cpp` (`loadFont`, `:70–77`), `src/shell/actions/instrument_drop_win.cpp` (`writeTempPreset`, `:50–61`), `src/app/main.cpp` (**the `REAPERAPI_LoadAPI` failure branch only**). **Does not own:** any `CMakeLists.txt`, `panel_window.cpp`, or any `core/` file. **Behavior.** - **`FF_DONTCARE` (L2-01).** `draw_kit.cpp:73` passes `DEFAULT_PITCH | FF_DONTCARE` to `CreateFont`; the symbol has **zero occurrences anywhere in `vendor/WDL/`**, and the file is not platform-guarded, only its include is. `draw_kit` links into **both** modules, so nothing builds until this is fixed. Drop the term or define it locally in the non-Windows include branch. **Do not add `windows.h`** — L2-01's stated direction. The family bits are advisory to Windows' font mapper and meaningless to fontconfig. - **`GetCurrentProcessId()` (L2-02).** `instrument_drop_win.cpp:59` calls it with no platform branch anywhere in the TU; SWELL exports `GetCurrentThreadId` and not this. The PID exists only to keep two concurrent REAPER instances from colliding in the shared temp dir, and the atomic counter at `:52` already carries the intra-process half. Replace with a platform-neutral uniqueness source behind a guard. - **The silent load failure (L2-03).** Either switch `main.cpp` to `REAPERAPI_MINIMAL` plus an explicit `WANT` list — the pattern `panel_window.cpp` and `panel_audition.cpp` already use, and the honest inventory of what this extension actually needs — or keep the full load and print the failure count via `rec->GetFunc("ShowConsoleMsg")` before returning 0. **Acceptance criteria.** - `cmake -B build -S . -G Ninja && cmake --build build` produces `build/reaper_reasampler.so` with no errors. `[verify — Linux]` = **V1**. - `ctest --test-dir build --output-on-failure` passes all 91 test targets, **no `-C` flag needed** on a single-config generator. `[verify — Linux]` = **V2**. - A deliberately misspelled `WANT` entry (or a forced non-zero `failcnt`) produces a visible REAPER console line naming the count, not a silent refusal. - Windows build and `ctest` unchanged. **Prerequisites.** None. **Discharges:** L2-01, L2-02, L2-03; enables V1, V2, V3. #### Λ-W2-T2 — `toolchain-floor` **Goal.** The Linux build is optimized when asked, links what it uses, hides what it does not export, and reports diagnostics no one has seen yet. **Spec:** `docs/product/linux-readiness.md` §Λ-W2-T2; findings Λ-02, Λ-03, Λ-04, Λ-05, Λ-07, Λ-09, Λ-10. **Surface boundary — owns:** root `CMakeLists.txt`, `src/app/CMakeLists.txt` (**the target property and platform blocks; NOT the source list — that is Λ-W4-T3's**), `cmake/reasampler_targets.cmake`, `src/shell/instrument/CMakeLists.txt` (**thread linkage only; the `WIN32` gate is Λ-W6-T1's**), `README.md`, and root `CLAUDE.md` §"Build and test" / §"Install / reload" / §"One-time submodule setup". **Does not own:** any `.cpp` or `.h`. **Behavior.** - **Λ-02** — root `CMakeLists.txt:28–30` is the *complete* list of language settings: no `CMAKE_BUILD_TYPE`, no `CMAKE_CXX_FLAGS`, no IPO/LTO, no `target_compile_options` anywhere in the tree. Default `CMAKE_BUILD_TYPE` when neither it nor `CMAKE_CONFIGURATION_TYPES` is set, and correct the docs' ship instruction: `--config Release` is *accepted and ignored* by Ninja and Make, and the README currently sends a Linux user to `build/Release/`, which does not exist there — the module lands at `build/reaper_reasampler.so`. - **Λ-03** — `CXX_VISIBILITY_PRESET hidden` + `VISIBILITY_INLINES_HIDDEN` on both module targets. SWELL's own build already uses `-fvisibility=hidden`, and the symbols that must stay exported carry their own `visibility("default")` attributes (`reaper_plugin.h`'s `REAPER_PLUGIN_DLL_EXPORT`, `fplatform.h`'s `SMTG_EXPORT_SYMBOL`, `swell-modstub-generic.cpp:135`'s `SWELL_dllMain`). - **Λ-04** — `find_package(Threads REQUIRED)` + `Threads::Threads`. Correct on all three platforms, costs nothing on Windows. - **Λ-09** — `-Wall -Wextra` and `CMAKE_CXX_EXTENSIONS OFF`. **No `-Werror` in this change** (the audit is explicit): the diagnostic-set size over this tree is not estimable from Windows. Separately `-Wl,--no-undefined` on the module targets, restoring the fail-at-link-time behaviour MSVC gives and GNU `ld` does not — **or** a recorded reason why `SWELL_PROVIDED_BY_APP`'s function-pointer design makes the gap moot. - **Λ-05** — pin the `reaper_plugin.h` → `../WDL/swell/swell.h` include coincidence with a comment or an `INTERFACE` target carrying both include dirs as one unit. Invisible on Windows, load-bearing off it. - **Λ-07** — record the compiler floor (**Λ-F4's input**), or add `-lstdc++fs` and document why. - **Λ-10** — one sentence in the platform-support docs: on Linux `vendor/vst3sdk` is optional until Λ-W6, so `git submodule update --init vendor/reaper-sdk vendor/WDL` is the complete extension-only prerequisite. **Acceptance criteria.** - `compile_commands.json` or a verbose build log shows an explicit `-O` flag on a bare `cmake --build build`. `[verify — Linux]`. - `nm -D --defined-only reaper_reasampler.so | grep -E 'ReaperPluginEntry|SWELL_dllMain'` finds **both** after the visibility preset. `[verify — Linux]` = **V6**. - The extension links with `Threads::Threads` **removed** — proving the include-only pthread dependency needs no flag — or the symbol forcing it is named. `[verify — Linux]` = **V5**. - `capture_paths_tests` links without an explicit `-lstdc++fs` on the declared floor, or the flag is added and the floor documented. `[verify — Linux]` = **V8**. - **The warning count from the first `-Wall -Wextra` build is recorded, not fixed**, and handed to Λ-W4 as an input. - Windows build unchanged; `--config Release` still behaves as documented there. **Prerequisites.** None; concurrent with T1, T3, T4. **Discharges:** Λ-02, Λ-03, Λ-04, Λ-05, Λ-07, Λ-09, Λ-10; enables V5, V6, V8. #### Λ-W2-T3 — `panel-dialog-resource` *(GATED on Λ-F2 — do not dispatch until it is ruled)* **Goal.** The docked bank panel opens on Linux, and if it ever fails to, it says so. **Spec:** `docs/product/linux-readiness.md` §Λ-W2-T3 and §Λ-F2; findings Λ-01, L2-06, L2-07. **The defect.** `panel_window.cpp:169` is `CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_BANK_PANEL), …)`, which SWELL resolves out of a per-module registry populated by a **resgen-generated source file that is not in the Linux target**: `src/app/CMakeLists.txt:109` has the `target_sources` line commented out (`:98` for macOS). The registry head stays null, `SWELL_CreateDialog` returns null, `:171` returns, and the toggle action is a **silent no-op** — no console line, no Actions-list checkmark. Three defects stack inside the commented-out instructions themselves: the script named at `:108` (`mac_resgen.php`) does not exist, the output filename is wrong, and the output is an `#include`-only artifact that cannot be a `target_sources` entry at all. **Surface boundary — owns:** `src/shell/panel/panel_window.cpp` (the `CreateDialogParam` call at `:169–171`, the dialog proc's platform contract, the drop-accept opt-in at `:179–184`), `src/resource.rc`, `src/resource.h`, and — **under the resgen route only** — one `target_sources` line in `src/app/CMakeLists.txt`'s `else()` branch plus a new include-shim TU. **Does not own:** any other panel TU, `draw_kit`, or any CMake target property. `panel_window.cpp` is **deliberately not split** across tracks: the L2-06 diagnostic and the Λ-01 resource route are the same function, and under the id-0 route the same *line*. **Behavior.** Whichever route Λ-F2 picks, plus — **unconditionally, and on both platforms rather than behind a guard** — a one-line `ShowConsoleMsg` on the `!g_panel.hwnd` path naming the missing dialog resource (L2-06). That single line converts a mystery into a two-minute diagnosis and is worth having on Windows too. The fix is shared macOS/Linux either way (Λ-D4: made in shared form, verified on Linux only). **Acceptance criteria.** - The panel toggle action docks a visible, LICE-drawn bank panel in a Linux REAPER. `[verify — Linux]` = **V7**. - With the resource deliberately unavailable, the toggle prints one console line rather than doing nothing. **Verifiable on Windows by forcing the branch.** - Dragging a WAV from the file manager onto the docked panel ingests it — or, if it does not, the failure is understood rather than mysterious. `[verify — Linux]`, T2 §5 item 3. **This criterion's difficulty depends on the Λ-F2 route** and is the fork's substance. - Windows panel behaviour byte-for-byte unchanged. **Prerequisites.** **Λ-F2 must be ruled before dispatch.** If it is not, this track slips to Λ-W4 and Λ-W3's sweep splits into a panel-independent half (run early) and a panel-dependent half (run after this lands) — a second Linux session, which is the cost of leaving the fork open. **Discharges:** Λ-01, L2-06, L2-07; enables V7. #### Λ-W2-T4 — `locale-independent-numerics` **Goal.** A persisted float round-trips identically regardless of process locale, so a bank index written on a comma-decimal machine is not written unparseable. **Spec:** `docs/product/linux-readiness.md` §Λ-W2-T4; finding L2-04. **Why it is pulled forward, ahead of everything it looks like it should follow:** it is the phase's one substantial fix that **needs no Linux box at all** — two writers and three readers, all in `core/`, all unit-testable on Windows today — and its failure mode is that the bank index is written unparseable and **the project's whole bank is lost on reload.** It should land before any Linux user saves a project. **Surface boundary — owns:** `src/core/json/json.cpp` (the `%.17g` writer and the `strtod` reader), `src/core/model/provenance.cpp` (its own `%.17g`), `src/core/wire/wire.cpp` (`Cursor::fieldDouble`), `src/core/capture/render_settings.cpp` (the `std::stod` over REAPER's `P_RAZOREDITS`), and the corresponding `tests/`. **Does not own:** any shell TU, any CMake file, `wav_codec` (its byte-order handling is already explicit and correct). **Behavior.** Make the number codec locale-independent at its two writers and three readers — `std::to_chars`/`std::from_chars`, or a `std::locale::classic()`-bound stream. **Do not "fix" this by calling `setlocale`**; an extension must not mutate the host's locale. Why it is not paranoia: on Windows the CRT starts in the `"C"` locale and nothing here calls `setlocale`, which is why it has never fired; on Linux SWELL's GDK backend calls `gtk_init_check`/`gdk_init_check` and never calls `gtk_disable_setlocale`, and any GTK or Qt plugin in the same process can do the same. The readers are honestly fail-closed — they require whole-token consumption — so the failure is "the field disappears", not "the field is silently wrong". **Acceptance criteria.** - New pure tests pass **on Windows today**, under a forced comma-decimal `LC_NUMERIC` set inside the test. **This is the one Λ-W2 track that does not wait for Λ-W3.** - The bank index, view model, tracking ledger, provenance blob and tail setting all round-trip a fractional value under that forced locale. - **Byte-for-byte identical output to today's writer under the `"C"` locale** — this is a persisted format, and a changed representation is a compatibility event. - Nothing on a hot path is touched; the JSON/persist path is declared off all hot paths in root `CLAUDE.md` and stays there. **Prerequisites.** None. **Discharges:** L2-04. **Note:** T2 §5 item 1 (read `LC_NUMERIC` inside a running REAPER-Linux process) stays in the Λ-W3 sweep, but only to record how urgent this *was* — the work is not gated on it. --- ### Λ-W3 — First light, and the verification sweep **Depends on:** Λ-W2. **One track, deliberately** — this is a person at a Linux box working a checklist where each answer reprices the next; splitting it across specialists buys no concurrency and loses the thread. Precedent: Ρ-W1 and Γ-W4 are both single-track waves for the same reason. #### Λ-W3-T1 — `linux-verification-sweep` **Goal.** Discharge every acceptance criterion Λ-W2 could not check from Windows, answer every `[verify — Linux]` item in both audits, and reprice the remaining waves against what is actually true. **Spec:** `docs/product/linux-readiness.md` §Λ-W3-T1 and §"Why this sequence". **Surface boundary — owns: no source file and no CMake file.** Its deliverable is a **verification record** at `docs/product/audit-notes/lambda-w3-verification.md`, one entry per item: the exact check run, the observed result, and what it changes. **Any fix the sweep motivates is filed to Λ-W4, not made here.** The one exception: a defect that blocks further sweeping (the build does not link at all) is fixed in place and recorded as a deviation, because the alternative is a wasted session. **Behavior — the sweep set.** | From | Items | |---|---| | T1 | **V1** compile, **V2** ctest, **V3** does REAPER's Linux build call `SWELL_dllMain` and populate the API table, **V4** where `UserPlugins/` actually is and whether `reaper_*.so` is the right glob, **V5** pthread link flag, **V6** visibility vs. the two exported symbols, **V7** the docked panel, **V8** `-lstdc++fs`, **V9** does `libSwell.so` sit beside REAPER's executable, **V10** which `uname -m` values the VST3 bundle must carry | | T2 | process `LC_NUMERIC`; `REAPERAPI_LoadAPI`'s actual return value and the name of any gap; panel file-drop routing; which SWELL GDI/locale build REAPER ships; fontconfig's substitution for "Consolas"; `SWELL_InitiateDragDropOfFileList` acceptance by common targets, and whether its 500 ms no-motion timeout cancels a slow gesture; prune against an in-use file | | New | the `-Wall -Wextra` diagnostic set, counted and categorised; whether `-Wl,--no-undefined` links clean or names an undefined set; whether the four-TU LICE slice links without `lice_colorspace.cpp` (T1 §3 leaves this an unreconciled inference) | **Three items are load-bearing beyond their own answer and must be run FIRST:** **V1** (nothing else is observable until it passes); **V9** (a negative answer **voids Λ-D2's route and needs a Daniel re-ruling** before Λ-W6 is dispatched — the entire `dlopen`-REAPER's-SWELL design rests on it, and it is a one-line `ls`); and the `REAPERAPI_LoadAPI` count (a non-zero result promotes L2-03 from a diagnostic to a real Blocker and names the gap). **Acceptance criteria.** - Every item above has a recorded answer or an explicit "could not determine, because X". **A blank is a failure of this track, not a deferral.** - Λ-W2's four tracks each have their `[verify — Linux]` criteria marked discharged or failed, **by name**. - Λ-W4 and Λ-W5's scope is restated against the answers, with any effort band that moved called out. **The audits' bands are provisional by their own statement; this is where they stop being.** - Λ-W6's prerequisites (V9, V10) are answered, or Λ-W6 is explicitly blocked pending a Daniel re-ruling on Λ-D2. **Prerequisites.** Λ-W2-T1, T2, T4. **T3 if Λ-F2 was ruled** — otherwise the panel-dependent items (V7, file-drop routing, any font check needing a rendered panel) defer to a second session and that deferral is recorded. **Discharges:** V1–V10 and T2 §5's seven items, **as answers rather than as fixes**. --- ### Λ-W4 — Correctness and safety, repriced **Depends on:** Λ-W3-T1. **Three tracks, disjoint by directory.** Everything here is known work whose *size* the sweep may have moved. #### Λ-W4-T1 — `prune-deletion-safety` **Goal.** Prune on Linux deletes only what it means to, tells the truth about what it reclaimed, and tells the user the deletion is permanent. **Spec:** `docs/product/linux-readiness.md` §Λ-W4-T1; finding L2-05 (both halves) plus the symlink row of T2 §4. **Surface boundary — owns:** `src/shell/persist/prune_fs.cpp` (the deletion authority's non-Windows branch and the reclaim scan) and the prune confirmation text wherever it is composed (`shell/actions/prune_action` / `shell/panel/panel_bank_ops`). **Does not own:** `core/reclaim/`'s orphan computation (pure, portable, unaffected), or any other persist TU. **Behavior.** - **The confirmation string becomes platform-aware** (Λ-D5). On Linux it states the deletion is permanent and there is no Recycle Bin; on Windows it says what it says today. **Same code path, one platform-dependent phrase — not a second dialog.** - **The dead "locked" branch is reckoned with.** `if (ec) return false; // real failure (locked/permission) -> skip` encodes Windows file-sharing semantics; on Linux `unlink` of an open file **succeeds**, the audio keeps playing from the open fd, and the bytes are gone when it closes — so the branch never fires. Either it is documented as Windows-only in place, or the Linux path acquires an equivalent guard. **What it must not do is stay silently asymmetric**: the recovery floor root `CLAUDE.md` §"The resample bake" relies on ("the superseded file survives on disk until a prune reclaims it") otherwise has nothing under it on Linux. **This half is not covered by Λ-D5's trash ruling** — it is not a trash question. - **The symlink hazard is fixed.** `fs::directory_iterator` + `is_regular_file()` follows symlinks under C++17; size is read from the target via `file_size()` but `fs::remove` deletes the **link**, not the target — so prune reports N bytes reclaimed and reclaims zero. Symlinked media folders are far more idiomatic on Linux than on Windows. **This is a reporting lie, not a cosmetic issue.** **Acceptance criteria.** - A Linux prune of a bank file currently playing behaves as recorded in Λ-W3's sweep, and the behaviour matches what the confirmation promised. `[verify — Linux]`. - A symlinked bank file is either skipped or deleted with its target, and the reclaimed-byte figure matches what actually left the disk in **both** cases. Unit-testable for the computation; `[verify — Linux]` for the filesystem half. - The Windows path — `SHFileOperationW` + `FOF_ALLOWUNDO`, Recycle-Bin recoverable — is bit-for-bit unchanged. - **Prune remains the only file-deletion path in the system** (plan-wide product invariant). **Prerequisites.** Λ-W3-T1 (the in-use-file check). **Discharges:** L2-05 both halves, and the symlink row of T2 §4. #### Λ-W4-T2 — `linux-font-faces` **Goal.** The kit asks for faces that exist on a stock Linux distro, so type is **chosen** rather than substituted. **Spec:** `docs/product/linux-readiness.md` §Λ-W4-T2 and §"Calls made here"; finding L2-09. **This is a product-designer call, not a fork:** silent fontconfig substitution is not acceptable because the kit's type is part of a deliberate visual identity (`docs/product/visual-design-language.md`), and the palette-role discipline exists precisely so a visual direction is a single-file change rather than an emergent property of the host. The cost is five string literals. Contradict it in review with an argument. **Surface boundary — owns:** `src/shell/panel/draw_kit.cpp`'s five `loadFont` call sites (`:154–158`) and the face constants in `draw_kit.h`. **Does not own:** `loadFont` itself beyond the literals, the palette, any geometry, or the two WCAG `static_assert`s — which are on pixel height and weight, not on the face, and hold regardless. **Behavior.** A platform face list at the five call sites: **DejaVu Sans / DejaVu Sans Mono** as the Linux defaults, following SWELL's own no-fontconfig fallback list (LiberationSans/DejaVuSans, LiberationMono/DejaVuSansMono) as precedent. **One code path**, shared with macOS's eventual San Francisco/Menlo (Λ-D4: made in shared form, not verified). The subtlety to carry into the work: `draw_kit.cpp:74`'s `if (!hf) return` guard does **not** catch this failure mode — SWELL's `CreateFont` always returns a non-null handle even when the face never resolved, recording the failure as a null `typedata` internally. **A wrong or missing face is not observable at the call site, only in the rendering.** **Acceptance criteria.** - Every kit string renders in the intended face on a stock distro, and the numeric readouts are tabular. `[verify — Linux]`. - Windows renders Segoe UI and Consolas exactly as today. - Row heights, ellipsis points and label truncation are unchanged on Windows; on Linux they are **measured** rather than assumed correct. **Prerequisites.** Λ-W3-T1's fontconfig answer (T2 §5 item 5), which decides whether this is cosmetic or a readability regression. **Discharges:** L2-09. #### Λ-W4-T3 — `source-partition-and-invariants` **Goal.** The build's source list stops relying on every TU's own `#ifdef` discipline, and the invariants Linux weakens are stated where a reviewer will read them. **Spec:** `docs/product/linux-readiness.md` §Λ-W4-T3 and §Λ-F3; findings Λ-06, L2-10, L2-11. **Surface boundary — owns:** the `target_sources` list in `src/app/CMakeLists.txt:8–55` (**the list; the property blocks are Λ-W2-T2's**), any new platform-sibling TU the sweep showed was needed, `src/shell/actions/drag_out_win.h`'s invariant comment, and the corresponding `src/shell/**/CLAUDE.md` invariant passages. **Does not own:** any behaviour change in a shipped code path. **Behavior.** - **Λ-06** — partition the source list where the Λ-W3 diagnostic set says a TU needs it. `drag_out_win.cpp` is the model (a real `#ifdef _WIN32` / `#else` split); `arrange_drop_win.cpp` and `instrument_drop_win.cpp` are `_win`-suffixed for the **surface** they serve, not for a platform dependency, and the audits found them portable by inspection — **confirm against the actual compile rather than re-inspecting.** - **L2-10 — the copy-only invariant. Λ-F3 rules the wording; this track makes the edit either way.** Make `drag_out_win.h:7–11` the doc a Linux reviewer is pointed at, and treat "MOVE is structurally impossible" as a Windows-scoped claim: SWELL's file-list drag takes no effect mask, so nothing at the API level forbids a target from treating the drag as a move. - **L2-11 — no Linux action.** If the predicate is touched at all it becomes "case-insensitive filesystem", not "Windows" — but Λ-D4 puts macOS out, so the right move is **a comment recording the known macOS defect, not a speculative fix.** **Acceptance criteria.** - No TU compiles on Linux only because of an `#ifdef` that happens to be complete; every platform-specific TU is either partitioned in CMake or carries a deliberate, commented guard. - The copy-only invariant's text says **the same thing** in `drag_out_win.h`, the owning `CLAUDE.md`, and any spec text that cites it. - Zero behaviour change on Windows. **Prerequisites.** Λ-W3-T1. **Λ-F3 for the L2-10 wording only — non-gating:** the track can land the partition and leave the sentence for a follow-up. **Discharges:** Λ-06, L2-10, L2-11's Linux half. --- ### Λ-W5 — Ship the extension **Depends on:** Λ-W3-T1 (V4) and **all three Λ-W4 tracks** — shipping means the correctness fixes are in. **One track. Gated on Λ-F4** (a shipped artifact has to say what it runs on). #### Λ-W5-T1 — `linux-packaging-and-install` **Goal.** A Linux user can install a correctly-built `reaper_reasampler.so` by following a document, and a pipeline can install it by following a rule. **Spec:** `docs/product/linux-readiness.md` §Λ-W5-T1; finding Λ-08 plus Λ-02's shipped half. **Surface boundary — owns:** an `install()` rule in `src/app/CMakeLists.txt` — **the first in the tree** — `README.md`'s platform-support and install sections, root `CLAUDE.md` §"Install / reload", and `docs/product/versioning-and-release.md`'s artifact and pipeline paragraphs. **Does not own:** any source file, the toolchain properties (Λ-W2-T2's), or CI (Λ-F1). **Behavior.** - Document the Linux `UserPlugins/` root **as V4 actually found it**, and the single-config-generator output path (`build/reaper_reasampler.so`) that no doc currently names. - Add the `install()` rule for **both channels**. The channel fork is platform-independent by construction — `REASAMPLER_CHANNEL` threads through `configure_file` into names only — so `reaper_reasampler_beta.so` needs no separate mechanism, only a separate destination check. - **Amend `versioning-and-release.md`**, which already promises three platform artifacts per channel per release and mentions no Linux install path, no signing and no CI. Λ-D4 removes macOS from the near-term promise: that document should say **two** platform artifacts per channel for now, **with macOS named as deferred rather than silently dropped.** - **The beta channel on Linux is uncosted in both audits.** Almost certainly free — the fork is name-only — but "almost certainly" is not a ship criterion. Build and install both channels side by side once. - **Λ-F1 decides only which paragraph gets written here** — a pipeline paragraph, or an explicit "deferred to dev-ops". Until it is ruled, **Λ ships nothing that presumes a runner.** **Acceptance criteria.** - A clean-machine walkthrough **following only the README**: clone, narrow submodule init, configure, build, install, restart REAPER, actions present, panel docks. `[verify — Linux]`. - `reaper_reasampler.so` and `reaper_reasampler_beta.so` coexist in one REAPER with separate ext-state namespaces, command ids and dock idents. `[verify — Linux]`. - The **installed** binary shows an explicit `-O` flag in its build log — Λ-02's criterion, re-checked on the artifact that actually ships. - `versioning-and-release.md` no longer promises an artifact this phase does not produce. - The declared support floor (Λ-F4) appears in the README and matches what was built. **Prerequisites.** Λ-W3-T1 (V4), Λ-W4 (all three tracks), **Λ-F4 ruled**. **Discharges:** Λ-08, and Λ-02's shipped half. --- ### Λ-W6 — The instrument module, and the host-safety contract **Depends on:** Λ-W2-T2 (thread linkage, visibility, warnings) and Λ-W3-T1 (V10, plus V9 if Λ-D2 is to survive). **Λ-W6 may run CONCURRENTLY with Λ-W4 and Λ-W5** — it touches `src/shell/instrument/` and that directory's CMake and `CLAUDE.md` files plus `src/core/instrument/CLAUDE.md`, none of which Λ-W4 or Λ-W5 opens; the only reason to serialise is attention, not contention. **Two tracks — one build + docs, one source — and they are `T1 before T2 — serial`**, the same shape this plan already records for Γ-W3. They are file-disjoint but not order-free: there must be a module before it can refuse to show a view. **At the end of this wave the Linux instrument loads in any host, processes audio, and has NO EDITOR AT ALL** — a defined, verifiable, shippable state, not a half-done one. #### Λ-W6-T1 — `vst-linux-module` **Goal.** `reasampler_9000.vst3` configures, builds and installs on Linux as the directory bundle a Linux host expects, and the decision that forbade it is reversed in writing. **Spec:** `docs/product/linux-readiness.md` §Λ-W6-T1; audit findings B1, B2, B5. **Surface boundary — owns:** `src/shell/instrument/CMakeLists.txt` (the gate, the entry-point source selection, the bundle POST_BUILD and install rule, `SWELL_PROVIDED_BY_APP`, the modstub TU) and the three D5 passages in `src/core/instrument/CLAUDE.md`, `src/shell/instrument/CLAUDE.md` and `src/shell/panel/CLAUDE.md`. **Does not own:** any `.cpp` or `.h` under `shell/instrument/`. **Behavior.** - **B5 first, as a documentation act.** Rewrite the three D5 passages: the platform clause becomes "Windows and Linux; the editor is REAPER-hosted"; **VST3-only and REAPER-only survive verbatim.** `src/core/instrument/CLAUDE.md` carries it twice (Invariants and Non-goals), `src/shell/instrument/CLAUDE.md` once (Non-goals), and `src/shell/panel/CLAUDE.md` once (the font/GDI clause). **Nothing else in this wave may land before this does** — the invariant files are what a future implementer reads. - **B1** — swap `public.sdk/source/main/dllmain.cpp` for `linuxmain.cpp` on Linux. `dllmain` includes `` with no `SMTG_OS_*` guard; `linuxmain.cpp` exports `ModuleEntry` and `ModuleExit`, **both mandatory** — the SDK's own loader refuses the module without either. Both files are already vendored; this is a source swap plus a platform `if()`. - **Split the `WIN32 AND EXISTS` conjunction** at `src/shell/instrument/CMakeLists.txt:9`. Today it is a conjunction, so a Linux configure silently omits `reasampler_vst` **even with the submodule slice fully initialised.** The `EXISTS` half stays — a fresh clone with no VST3 slice must still configure — and the `WIN32` half becomes a Windows-or-Linux predicate. - **B2** — the artifact becomes a **directory bundle**: `reasampler_9000.vst3/Contents/-linux/reasampler_9000.so`, per V10's answer and Λ-F4's floor. `Contents/Resources/moduleinfo.json` is **optional** — the SDK's `getModuleInfoPath` returns empty when absent rather than failing — **so do not author one.** Install roots: `$HOME/.vst3/`, `/usr/lib/vst3/`, `/usr/local/lib/vst3/`, `$APPFOLDER/vst3/`. - **`SWELL_PROVIDED_BY_APP` + the modstub TU** are added to this target in the **default (non-`SWELL_LOAD_SWELL_DYLIB`) branch** — the same branch the extension already uses (`src/app/CMakeLists.txt:103–104`). The whole modstub file is inside `#ifdef SWELL_PROVIDED_BY_APP`, so the VST3 target must define that symbol; today it does not. **The `dlopen` itself is Λ-W7's**; what this track lands is the compiled-in, inert table plus the exported `SWELL_dllMain`. - **The channel fork applies:** `reasampler_9000_beta.vst3` with its own class UID. **The UID pair is FOREVER-FROZEN and must not change** — a Linux build is a new platform, not a new identity, and a saved project rebinds by UID. **Acceptance criteria.** - A Linux configure produces the `reasampler_vst` target; a Windows configure is unchanged. - The built bundle's directory shape matches what the SDK's `module_linux.cpp` opens, and the SDK's own `validator` loads it. `[verify — Linux]`. - Both channels build and install side by side, and **the two class UIDs are byte-identical to the Windows build's.** - The three `CLAUDE.md` files no longer claim Windows-only, and **still** claim VST3-only and REAPER-only. - **No VST3 parameter is registered by this track or any other in this phase** — see the Γ-W4-T1 boundary above. **Prerequisites.** Λ-W2-T2, Λ-W3-T1 (V10; V9 if Λ-D2 is to survive). **Discharges:** B1, B2, B5; and Λ-10's "vst3sdk is optional on Linux" caveat becomes conditional. #### Λ-W6-T2 — `vst-host-safety-contract` **Goal.** The plugin's editor surface refuses **cleanly** on every Linux host, so that adding a real editor later is a change of branch taken and not a change of contract. **Spec:** `docs/product/linux-readiness.md` §Λ-W6-T2 and §"The non-REAPER-host safety criterion"; Λ-D3, L2-08's fallback half. **Surface boundary — owns:** `src/shell/instrument/editor_platform.cpp` (the `isPlatformTypeSupported` / `createView` decision and the non-Windows stubs), `reasampler_processor.cpp`'s `createView` site, `reasampler_embed.cpp`'s `REAPER_FXEMBED_WM_IS_SUPPORTED`, and a new availability-probe seam under `shell/instrument/`. **Does not own:** any CMake file, the paint or input families (untouched — they stay whole-region-guarded), or any pure `core/instrument/` module. **Behavior.** - The editor's availability becomes a **tri-state probe** — untried / available / unavailable — computed **once, lazily, off the audio thread**. In this wave the Linux answer is unconditionally *unavailable*; **Λ-W7 gives it a real computation without changing a single call site.** - `isPlatformTypeSupported` returns `kResultFalse` for **every** type when unavailable; `createView(kEditor)` returns `nullptr`. **Never a view that fails to attach.** - **One** diagnostic line per process, naming the reason. Never a modal. - `reasampler_embed`'s `REAPER_FXEMBED_WM_IS_SUPPORTED` continues to return 0 off Windows. That is correct and stays correct — the TCP/MCP embed strip on Linux is an explicit non-goal of this phase. - **The Windows path is not restructured to accommodate this.** The probe is a Linux branch on an existing decision, **not a new abstraction over both.** **Acceptance criteria.** The full observable contract above, verified by **all four** of its checks: `validator` with no `libSwell.so` present; a real Ardour or Bitwig scan; a REAPER-on-Linux load; and the renamed-`libSwell.so` negative control. `[verify — Linux]`. Plus: - Windows editor behaviour bit-for-bit unchanged — same window class, same `wndProc`, same `CS_DBLCLKS` fall-through. - **Nothing added to `process()`** — no `dlopen`, no probe, no branch on the per-voice-per-sample path. - **The criteria pass with ZERO VST3 parameters registered.** An empty generic UI is a pass; Γ-W4-T1 improves it and Λ must not. **Prerequisites.** Λ-W6-T1 (there must be a module to load). **Discharges:** Λ-D3's Λ-W6 half; L2-08's fallback half. --- ### Λ-W7 — The SWELL bootstrap **Depends on:** Λ-W6-T1, Λ-W6-T2, and **V9 answered affirmatively.** If V9 is negative, **this track does not exist and Λ-D2 needs a re-ruling** — which is why V9 runs first in Λ-W3 rather than being discovered here. #### Λ-W7-T1 — `swell-dylib-bootstrap` **Goal.** The plugin acquires a working SWELL function table under REAPER on Linux, without the vendored stub's `exit()` behaviour and without vendoring SWELL. **Spec:** `docs/product/linux-readiness.md` §Λ-W7-T1 and §"The implementation shape this obliges"; Λ-D2 (route B3a), audit finding B3. **Surface boundary — owns:** a new `src/shell/instrument/swell_bootstrap.{h,cpp}`, the availability-probe computation Λ-W6-T2 left stubbed, and the `SWELLAppMain`-shaped callback. **Does not own:** any vendored file (**nothing under `vendor/` is patched**), the CMake source list beyond adding one TU, or the editor. **Behavior — exactly the four-point shape in the product doc, and it is not a re-litigation of Λ-D2 but the specification of it.** 1. **Never define `SWELL_LOAD_SWELL_DYLIB`.** Compile `swell-modstub-generic.cpp` in its default branch, which exports `SWELL_dllMain(hInst, callMode, GetFunc)` and calls `doinit` on the pointer it is handed. 2. **Own the load.** Resolve the host executable's directory, `dlopen` `libSwell.so`, `dlsym` `SWELLAPI_GetFunc`, `dlsym` and call `SWELL_set_app_main`, then call our own exported `SWELL_dllMain(hinst, DLL_PROCESS_ATTACH, getfunc)` — **using only exported surface, with every failure returning a recorded state instead of exiting.** This also dissolves the `SWELLAppMain`-as-a-link-requirement noted in T1 B3a: we pass an app-main because we choose to, not because the linker demands one. `[verify — Linux]` whether a minimal app-main suffices for a plugin that only ever creates child windows inside a host-supplied X11 window. 3. **Probe the host, not just the library.** A non-REAPER host has no SWELL message loop — REAPER's own is what pumps SWELL windows — so even a successful `dlopen` in Ardour would produce a window nothing drives. **Gate on the host name via `IHostApplication::getName()` AND on the `dlopen` succeeding**; either failing means no editor. 4. **Probe once, lazily, off the audio thread.** Tri-state, computed on first `createView` and never recomputed. No `dlopen` from `process()`, no per-`createView` retry, **no static-constructor work.** **The failure taxonomy the probe must distinguish**, because they need different messages: host is not REAPER / executable path unresolvable / `libSwell.so` not present at the resolved path / `SWELLAPI_GetFunc` missing or version-mismatched (the stub checks `SWELLAPI_GetFunc(NULL)==(void*)0x100`) / API table incomplete. **The last is not fatal** — `doinit` substitutes a zero-returning `dummyFunc` per miss — and whether to accept a partial table or refuse is **[propose at review]**, with *"refuse if any name the editor actually calls is missing"* as the starting proposal. **Acceptance criteria.** - Under REAPER on Linux the probe reports *available* and the SWELL table resolves with **zero misses**. `[verify — Linux]`. - Under `validator` with **no `libSwell.so` anywhere**, the probe reports *unavailable*, the process **exits normally**, and one diagnostic line is printed. **This is the direct negation of the `exit(2)` finding and is the track's headline criterion.** `[verify — Linux]`. - Under Ardour or Bitwig **with** a `libSwell.so` reachable, the host-name gate **still refuses** — proving the gate is on the host, not only on the library. `[verify — Linux]`. - No vendored file is modified; `git status` under `vendor/` is clean. - Windows build unaffected — the whole TU is behind a platform guard. **Prerequisites.** Λ-W6-T1, Λ-W6-T2, V9 affirmative. **Discharges:** B3 via route B3a; Λ-D3's runtime half. --- ### Λ-W8 — The X11 editor **Depends on:** Λ-W7-T1. **One track, and it is the phase's only L-band source item** (L2-08; T1 B4 costs it "build S, source L"). Λ-D6 sequences it last on purpose: **halting this wave still leaves a shipped extension and a loading, processing, generic-UI instrument** — a real product, not a stub. #### Λ-W8-T1 — `x11-embed-view` **Goal.** The ReaSampler 9000 editor opens, draws and responds under REAPER on Linux. **Spec:** `docs/product/linux-readiness.md` §Λ-W8-T1; audit findings B4, L2-08. **Surface boundary — owns:** a **Linux sibling** to `src/shell/instrument/editor_platform.cpp` (window creation and parenting, the run-loop timer, the event-driven input path), and the `#ifdef _WIN32` region **boundaries** in `reasampler_editor.h` and the eight `editor_input_*` / `editor_paint*` TUs — **boundaries only, not their contents.** **Does not own:** `draw_kit`, any pure `core/instrument/` module, any painter's drawing logic, `instrument_bake`, or the processor. **Behavior.** The audits call this **a new competence rather than a port.** The build-side cost is nil — the Linux IIDs are already in the vendored slice (`commoniids.cpp` defines `Linux::IEventHandler`, `Linux::ITimerHandler`, `Linux::IRunLoop` under `#if SMTG_OS_LINUX`, and that file is already in the `vst3_sdk` source list). What must be written: - `kPlatformTypeX11EmbedWindowID` instead of `kPlatformTypeHWND`; `attachedToParent` receiving an X11 window id. - A `Linux::IRunLoop`-driven timer replacing `SetTimer`/`WM_TIMER` — **including the editor's sync tick, which the bake's arm-then-run discipline rides on.** - An **event-driven input path** replacing the `wndProc` switch. `RegisterClassW` / `CreateWindowExW` / `DefWindowProcW` have **no SWELL analogue at all** — SWELL has no window-class model, only `SWELL_CreateDialog` and raw `HWND__` construction. - Substitutions the audit already resolved by name: `MoveWindow` → `SetWindowPos`; `GetWindowLongPtr`/`SetWindowLongPtr` → the non-`Ptr` forms returning `LONG_PTR`; `GetKeyState` → `GetAsyncKeyState`; `TrackMouseEvent`/`WM_MOUSELEAVE` → **nothing**, so hover-leave needs its own derivation (the panel layer already documents the same gap); `DragAcceptFiles`/`DragQueryFileW` → the drop path Λ-W2-T3 settled for the panel. **A worked reference implementation is already on disk and neither audit named it.** `vendor/vst3sdk/public.sdk/samples/vst-hosting/editorhost/source/platform/linux/` contains `window.cpp` (returning `{kPlatformTypeX11EmbedWindowID, …}` and answering `Linux::IRunLoop::iid` from `queryInterface`), `runloop.cpp`, and `irunloopimpl.h` (a `RunLoopImpl` implementing `registerEventHandler` / `registerTimer` and their unregisters). It is the **host** side of the contract rather than the plug-in side, which makes it a precise specification of what our plug-in side must satisfy, and the documented narrow submodule init already pulls it. **This does not shrink the L band** — it is a reading input, not a library; it removes the "we are guessing at the contract" risk from the estimate. **Acceptance criteria.** - The editor opens, draws every face identically to the Windows build (compare screenshots at the same window size), and every drag, click, wheel and keyboard interaction behaves the same. `[verify — Linux]`. - Hover-leave is correct on every hover surface, without `TrackMouseEvent`. - **The bake's arm-then-run tick fires under `IRunLoop`, and a bake completes end-to-end on Linux:** staged file, extension action invoked over the VST3 host bridge, outcome read back, adopt and reset. `[verify — Linux]`. - **Λ-W6-T2's contract still holds** — a non-REAPER host still gets `nullptr` from `createView`. **Re-run all four checks; this is a regression criterion, not a new one.** - **Nothing added to `process()`**; no dispatch added to any per-sample path. - Every new file lands under the ~600-line ceiling **with a responsibility seam, not a bisection.** The editor's existing band-axis split (`_chrome` / `_waveform` / `_deck` / `_browse` / `_curve`) is the seam vocabulary to reuse — the Linux platform TU is a **sibling to `editor_platform`**, per L2-08's own direction: *"it needs a sibling, not a rewrite."* - Windows editor behaviour bit-for-bit unchanged. **Prerequisites.** Λ-W7-T1. **Discharges:** B4, L2-08. --- ### Shared files across Λ's waves, named rather than discovered at merge All textual adjacency, not semantic contention, unless marked otherwise. | File | Tracks | Nature | |---|---|---| | `src/app/CMakeLists.txt` | Λ-W2-T2 (property + platform blocks), Λ-W2-T3 (one `target_sources` line, **resgen route only**), Λ-W4-T3 (the source list), Λ-W5-T1 (the `install()` rule) | Four disjoint regions of one file. Λ-W2-T2 and Λ-W2-T3 are the only pair in the same wave; one line each | | `src/shell/panel/draw_kit.cpp` | Λ-W2-T1 (`loadFont`'s `CreateFont` args, `:73`), Λ-W4-T2 (the five call sites, `:154–158`) | Different waves | | `src/shell/instrument/CMakeLists.txt` | Λ-W2-T2 (thread linkage), Λ-W6-T1 (gate, entry point, bundle, install), Λ-W7-T1 (one added TU) | Different waves | | `src/shell/panel/panel_window.cpp` | Λ-W2-T3 alone | **Deliberately not split.** The L2-06 diagnostic and the Λ-01 resource route are the same function — under the id-0 route the same *line*. Splitting them would be semantic contention | | `src/shell/instrument/editor_platform.cpp` | Λ-W6-T2 (the refusal branch), Λ-W8-T1 (the real branch) | Different waves; the second replaces the first's computation **without touching its call sites** | --- ## Traceability — all seventeen items The check that nothing was dropped. Every row points at a track. All seventeen items are Θ or Ξ work, and both phases have since landed in full — their track sections are retired from this file; see `docs/COMPLETED.md` for each track's full narrative. | # | Item (short) | Phase-Wave-Track | Worktree slug | |---|---|---|---| | 1 | Envelope editor: radio switch, curve dials, overlay recolor | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` | | 2 | MM preamp Filter — resonant HP/LP stage | Θ-W1-T3 (DSP) **+** Θ-W2-T1 (integration) | `pth-w1-t3-filter-dsp-port`, `pth-w2-t1-filter-voice-path` | | 3 | Alternative Spline EGs | Θ-W5-T1 | `pth-w5-t1-spline-egs` | | 4 | Bug: end-of-sample click, Trigger × Preserve | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` | | 5 | Bug: drag-out sometimes lands without audio | Θ-W1-T2 | `pth-w1-t2-capture-handoff-bugs` | | 6 | Bug: FX-container drop loses the capture | Θ-W1-T2 | `pth-w1-t2-capture-handoff-bugs` | | 7 | Stereo waveform shows both channels | Θ-W2-T2 | `pth-w2-t2-stereo-waveform-lanes` | | 8 | Release anchoring; the Pitch AD becomes AHD | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` | | 9 | Loop points — regression + Gate loop-sustain | Θ-W4-T1 | `pth-w4-t1-gate-loop-sustain` | | 10 | Knob/label sizing, ms units, double-click reset | Θ-W6-T1 | `pth-w6-t1-legibility-and-antialiasing` | | 11 | Preview glyph; VELOCITY deck; bipolar curves | Θ-W4-T2 | `pth-w4-t2-velocity-deck-and-bipolar-curves` | | 12 | Toolbar cleanup; full-width piano strip; tooltips | Θ-W2-T3 | `pth-w2-t3-toolbar-and-piano-strip` | | 13 | Antialiased rendering audit for high-DPI | Θ-W6-T1 | `pth-w6-t1-legibility-and-antialiasing` | | 14 | Trigger amp/filter fade → AHD consolidation | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` | | 15 | One-click in-sampler resample | Ξ-W1-T2 (note model) **+** Ξ-W2-T1 (bake chain) **+** Ξ-W3-T1 (popup abandoned by ruling; window derives itself) | `pxi-w1-t2-note-program-model`, `pxi-w2-t1-resample-bake-chain`, `pxi-w3-t1-capture-signal-popup` | | 16 | Retire the zone mapping system | Θ-W1-T1 | `pth-w1-t1-zone-retirement` | | 17 | Consolidate provenance/usage tracking | Ξ-W1-T1 | `pxi-w1-t1-tracking-consolidation` | ### Work in this plan that is not one of the seventeen The table above is a completeness proof over `TODO-1.0.md` — every row points at a track, so nothing from the source was dropped. It is deliberately **not** an index of the plan: work that did not come from the source doc has no row, and inventing one would weaken the proof it exists to give. - **Θ-W3-T1 — `live-parameter-delivery`** (`pth-w3-t1-live-parameter-delivery`). Arose from Θ-W2-T1's implementation review, not from `TODO-1.0.md`. The first such track in this plan; see Θ-W7-T1 below for the second. - **Θ-W7-T1 — `arc-and-spline-aa`** (`pth-w7-t1-arc-and-spline-aa`). Opened after Θ-W6-T1 shipped, when Daniel found two rendering defects by eye in the editor — not from `TODO-1.0.md`, and not a track this plan originally scoped. The second such track in this plan today; if others appear, they belong on this list rather than in the table. - **All of Phase Γ** (`pg-*`). **Thirteen tracks across four waves** (W1 seven, W2 two, W3 three, W4 one), from a direct interview with Daniel (2026-08-01) and his four later rulings the same day, not from `TODO-1.0.md`. Listed here as a block rather than per track, because the whole phase is outside the source doc; the product reasoning lives in `docs/product/instrument-control-surface.md` and the parameter system's in `docs/product/parameter-automation.md` §§6–10. **Two `docs/TODO.md` entries are discharged by this phase, not deferred again:** Γ-W3-T1 discharges the 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-*`). **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). Phase Ψ has since landed in full and its `PLAN.md` section is retired; the design content and the Ψ.1–Ψ.7 list now live in `docs/COMPLETED.md`. The seventh track, Ψ-W3-T1, is not one of the seven defects/refinements itself — it came from a review finding mid-phase; see `docs/COMPLETED.md` 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 than in the table — the table is a completeness proof over `TODO-1.0.md`, and a correction has no source row to point at. - **Γ-W3-T3 `bake-prints-limiter` is also a CORRECTION, not a feature**, and for the same reason as T2: it exists only because T2's own finding disproved the premise §3.4's reset classification rested on. Unlike the twelve above, it does not come from the 2026-08-01 interview or that day's four rulings — it is a separate ruling, one day later (2026-08-02), opened after T2 landed and found the gap. - **All of Phase Ε** (`pe-*`). **Six tracks across three waves**, from a direct request (Daniel, 2026-08-02), not from `TODO-1.0.md`. Listed here as a block, like Γ and Ψ; the product reasoning lives in `docs/product/bank-package.md`. It **supersedes nothing** — neither `docs/TODO.md` nor `docs/TODO-1.0.md` records export, import, or a package format, so there is no deferred entry to absorb or contradict. Its three [Daniel]-class forks (Ε-F1/F2/F3) were opened and ruled the same day it was framed, so no track here is gated on a decision; see "Decision state" above. - **All of Phase Ρ** (`pr-*`). **One track in one wave**, from a direct request (Daniel, 2026-08-02), not from `TODO-1.0.md`; the product reasoning lives in `docs/product/render-in-place.md`. It **supersedes nothing** — a sweep of `docs/TODO.md` and `docs/TODO-1.0.md` for `render.in.place|render to new track|preserve.source` returns nothing. The smallest phase in this plan, deliberately: it is a thin third verb composed out of machinery that already exists, and the burden was on any new machinery to prove it unavoidable. Its three [Daniel]-class forks (Ρ-F1/F2/F3) were opened and ruled the same day it was framed, so the track here is not gated on a decision; see "Decision state" above. **Ρ-F2 was the one that moved the spec** — the result track goes to Arrange unconditionally rather than following the active mode, which is also the only reason the phase touches `panel_input.cpp` at all. **Ρ-W1-T1 has landed**; see `docs/COMPLETED.md` for the full narrative. - **All of Phase Λ** (`pl-*`). **Thirteen pending tracks across seven waves** (Λ-W2…Λ-W8), plus Λ-W1's two audit tracks, which are landed. From a direct request (Daniel, 2026-08-02), not from `TODO-1.0.md`; the product reasoning lives in `docs/product/linux-readiness.md` and the evidence in the two audit notes it cites. It **supersedes nothing** and **corrects one standing promise** rather than inheriting it: `docs/product/versioning-and-release.md` commits to three platform artifacts per channel per release, and Λ-D4 (macOS out) makes that two for now, with macOS named as deferred. **Λ is the one phase in this plan with unanswered [Daniel]-class questions** — four forks, none ruled, of which only Λ-F2 gates a dispatch and only Λ-F4 gates a wave; see "Decision state" above. It is also the one phase whose acceptance criteria cannot be checked from the current box at all, which is why Λ-W3 exists as a wave rather than as a checklist at the end. - **All of Phase Ω** (`pom-*`). **Eight tracks across two waves** (W1 five, W2 three), from a direct list of seven defects and refinements (Daniel, 2026-08-03), not from `TODO-1.0.md`. Listed here as a block, like Γ, Ψ, Ε, Ρ and Λ; **like Ψ it has no backing product doc** — the seven are recorded in its phase header as Ω.1…Ω.7, and its six rulings are indexed in the table there rather than in `docs/product/`. It **supersedes nothing** but **amends two settled statements**, which is what distinguishes it from the other doc-less phase: Ω-F2 reverses the marker "Disabled rather than hidden" principle in `docs/product/instrument-control-surface.md` §6.3(d)/§6.4 for the Trigger case, and Ω-F6 amends `src/shell/view/CLAUDE.md`'s accepted FX-parking load hitch. Its six [Daniel]-class forks were opened and ruled the same day it was framed, so no track here is gated on a decision; see "Decision state" above. **Two `docs/TODO.md` entries constrain it without being discharged by it:** `reasampler_editor.h`'s ceiling margin (three W1 tracks add state to it, and the drag-state seam is explicitly out of scope) and `view.cpp`'s blocked split (Ω-W1-T1 adds a TU beside it rather than growing it). ### Deliberate compressions Recorded so a reader of `TODO-1.0.md` can see what this plan did to the source, rather than discovering it later: - **Item 2 is split across two waves.** The DSP port (Θ-W1-T3) is deliberately separated from the integration (Θ-W2-T1) so the external-input dependency on Daniel's Cortex-M4 code sits on a standalone, disjoint track instead of blocking a wave. Item 2's acceptance criteria are split accordingly — the range/resonance assertions land in W1-T3's tests, the audible/pipeline/deck criteria in W2-T1. - **Item 2's filter envelope ships twice.** W2-T1 ships it in the existing staged AHDSR shape; W3-T2 gives it the curve treatment and the mode-driven Gate→AHDSR / Trigger→AHD shape. This is deliberate: waiting would put the filter behind the whole envelope system. - **Item 4 lands in Θ-W3, not Θ-W1.** Its fix region is the region item 14 retires, and the only earlier instrument-side track owns the entire engine. Its acceptance gate is stated as the post-consolidation gate. It is not gated behind the *whole* editor chain — three waves of six — and the source doc itself requires re-verification under the surviving mechanism either way. - **Item 11's filter velocity curve is split from item 2's filter velocity modulation.** W2-T1 wires the modulation path (following the amp/pitch precedents); W4-T2 sets the curve's bipolar domain and default and homes its button. Neither track can do the other's half. - **Item 15's undo/recovery is carried as a note, not a criterion** — Daniel set it at exactly "a plus." The guaranteed recovery floor (the superseded file surviving until prune) is a criterion. - **Nothing else was compressed.** Every other item's behavior bullets and acceptance criteria are carried at full strength into the track that owns it. --- ## Outline at a glance ``` Phase Θ — ReaSampler 9000: one parameter set, filter, shapeable envelopes, legible editor [LANDED — all seven waves (W1-W7); see docs/COMPLETED.md; full detail section removed from this file] W1 Collapse and re-seam T1 zone-retirement ......................... 16 T2 capture-handoff-bugs .................... 5, 6 T3 filter-dsp-port ......................... 2 (DSP) [needs Daniel's M4 code] W2 Filter in the voice path; waveform and chrome bands T1 filter-voice-path ....................... 2 (integration) T2 stereo-waveform-lanes ................... 7 T3 toolbar-and-piano-strip ................. 12 W3 Live parameters, then the staged envelope system [T1 before T2 — serial] T1 live-parameter-delivery ................. (not one of the seventeen) T2 staged-envelope-curves .................. 1, 8, 14, 4 W4 Loop sustain and the velocity deck T1 gate-loop-sustain ....................... 9 T2 velocity-deck-and-bipolar-curves ........ 11 W5 Spline EGs T1 spline-egs .............................. 3 W6 Editor legibility pass T1 legibility-and-antialiasing ............. 10, 13 W7 Arc-and-spline antialiasing fix T1 arc-and-spline-aa ....................... (not one of the seventeen) Phase Ξ — The resample loop (W1 concurrency-safe with Θ from Θ-W2 onward) [LANDED — all three waves (W1-W3); see docs/COMPLETED.md; full detail section removed from this file] W1 Consolidated tracking, and the programmed-note model T1 tracking-consolidation .................. 17 T2 note-program-model ...................... 15 (model) W2 The bake chain [ran AHEAD of Γ; its reset list is corrected by Γ-W3-T2] T1 resample-bake-chain ..................... 15 (chain) W3 The capture-signal popup [popup abandoned by ruling; bake window derives itself] T1 capture-signal-popup .................... 15 (popup abandoned; window derives) Phase Γ — The instrument's control surface (none of the seventeen; ends with VST3 params) [LANDED — all four waves (W1-W4); see docs/COMPLETED.md; full detail section removed from this file] W1 Foundations — landed [seven tracks, disjoint by surface] T1 knob-interaction-law ....... modifiers + ONE taper module + reset bypass + 10 s ceiling + AHDSR schematic axis [Ruling 2] T2 master-bus-audio ........... limiter + meter ballistics + dynamic PDC [rung 1] T3 contour-trace-curves ....... staged traces draw curved, knot on its trace T4 editor-floor-and-row-law ... floor 1190x680 (W3-T1: 1198) + budget constants + row predicate T5 preserve-time-stretch ...... real stretcher [measure-and-report gate] T6 exhaustive-switch gate on pure libraries ... /we4062, -Werror=switch on pure libraries [no PLAN entry — see COMPLETED.md] T7 psola-preserve ............. PSOLA-aligned splice jump; period_detect [cadence closure — see COMPLETED.md] W2 New controls, and the overlay's marks — landed [2 tracks] T1 pitch-rate-deck ............ Rate + Pitch, Varisp/Presrv compounding [rung 2] T2 loop-crossfade-ux .......... four-mark grammar; fade painted where it is heard W3 The reflow, and the bake correction — landed [3 tracks] T1 deck-reflow ................ two rows + double-height MASTER, by construction T2 bake-reset-amendment ....... the Xi correction Gamma owns T3 bake-prints-limiter ........ landed — bake prints the limiter through the master stage W4 VST3 parameters — landed [1 track] T1 vst3-parameter-set ......... 44 derived params, frozen id table [Ruling 1] [rung 3 RESERVED, unspent] [Gamma-F7 RULED: signal flow order] Resequenced 2026-08-01, three times: the reflow split canvas (W1-T4) from arrangement (W3-T1); preserve-time-stretch moved W4 -> W1-T5, retiring Rate's interim stand-in; then Ruling 1 added W4 and Ruling 2 grew W1-T1. Payload rungs are RELATIVE, not absolute. Both are spent — kParamsPayloadVersion == 15 (Γ-W1-T2, landed, above) and v16 (Γ-W2-T1, stated there) — and the third remains RESERVED as v17, unspent: Γ-W4-T1's storage verification did not force it. Shared files, named: engine/CMakeLists.txt (W1-T2 | W1-T5), ui/CMakeLists.txt (W1-T1 | W1-T3), editor_session.cpp (W2-T1 | W2-T2) — all textual adjacency, not semantic contention. W3-T2's disjointness from W3-T1, once a CONDITIONAL risk to confirm against what Xi-W2-T1 shipped, is resolved: both tracks landed — see docs/COMPLETED.md. Phase Psi — The extension trust pass (none of the seventeen; a direct list of seven) [LANDED — all three waves (W1-W3); see docs/COMPLETED.md; full detail section removed from this file] 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:215). 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. Phase Epsilon — The bank package (none of the seventeen; a direct request 2026-08-02) [LANDED — all three waves (W1-W3); see docs/COMPLETED.md; full detail section removed from this file] W1 The contract, the filesystem, and the ledger's new kind [3 tracks, disjoint by dir] T1 package-format .............. core/package: framing + TWO version ints [E-F1 RULED: proprietary RSBK. No ZIP, no zlib] T2 package-fs-shell ............ shell/package: atomic write, streaming, pickers [ONE picker: REAPER GetUserFileName, mode 0/1] T3 import-origin-kind .......... OriginKind::PackageImport = 5, append-only W2 The two verbs [2 tracks; disjointness CONDITIONAL — see below] T1 bank-export ................. export_plan + verb + action; project untouched T2 bank-import ................. import_plan + verb + action + .rsbank drop [E-F2 RULED: always a NEW bank, auto-suffix, no merge. E-F3 RULED: REFUSE on degraded ledger] W3 The compatibility fixtures [1 track] T1 package-compat-fixtures ..... frozen bytes prove BOTH version directions Version policy, both directions: older package in newer build ALWAYS imports (additive fields default, unknown keys skipped); newer package in older build REFUSES WHOLE with a three-part message (package needs / this build reads / writer semver). The gate is minReaderVersion <= kPackageFormatVersion — formatVersion is for the message, not the gate. All three E-forks were ruled 2026-08-02, the day the phase was framed: NO track here is gated on a decision. Import name collision = first free of seed, "seed 2", "seed 3", ... seed never re-parsed, fold is BankBook's own. Shared files, named: root CMakeLists.txt add_subdirectory list (W1-T1 | W1-T2, one append-only line each); main.cpp + the panel bank menu (W2-T1 | W2-T2, one registration line and one menu row each); bank_book.{h,cpp} is W2-T2's alone (one additive public const member). W1's three tracks are unconditionally disjoint; W2's two are textually adjacent only — serialize T2 behind T1 if zero contention is wanted. Phase Rho — Render in place (none of the seventeen; a direct request 2026-08-02) [LANDED — Ρ-W1-T1; see docs/COMPLETED.md; full detail section removed from this file] W1 The verb [ONE track, deliberately] T1 render-in-place ............. render selected track -> new sibling track, item placed at the exact render position, colour + "Capture " name cloned, source -> Design, result track -> ARRANGE always. Never the bank. [R-F1 RULED: refuse multi-track, settled non-goal. R-F2 RULED: result track ALWAYS Arrange, never mode-following. R-F3 RULED: follow panel tail] The THIRD VERB. Capture = arrange -> bank (writes it). Placement = bank -> arrange (reads it). Render in place = arrange -> arrange (bank on neither side). That is why the load-bearing capture/placement rule survives it; the full argument is in the phase header and in docs/product/render-in-place.md. Structural boundary, not a convention: the render destination reaches the backend as a TWO-VALUED ENUM (Bank | ProjectMedia), never a caller-supplied path — so no Rho caller can name the bank folder. A renderDir string on CaptureRequest IS the drift. Genuinely new: ONE pure sibling-placement function (folder-parent and last-in-folder are both audibly wrong if inserted at srcIdx+1), an idempotent "Capture " prefix, a deriveRenderPaths sibling, and CaptureResult::absolutePath. Everything else composes. R-F2 fallout, the ruling's only cost: the result track AND its item are tagged kArrangeModeId explicitly, and detectNewContent must drop added GUIDs that already carry a membership record — the auto-tag detector otherwise re-tags a Design-fired result to Design on its next tick and reverses the ruling. Firing from Design is therefore deliberately invisible: the render waits in Arrange. No A/B-on-the-bench behaviour exists. Four invariant amendments are track deliverables (the Psi precedent): shell/capture and shell/actions CLAUDE.md placing-path claims, ONE sentence in root CLAUDE.md naming the third verb without softening the prohibition, and core/view CLAUDE.md's "new tracks are tagged to the active mode" made conditional on carrying no membership record. Shared files, named: capture.{h,cpp} (destination enum + result field), main.cpp (one ActionTableRow), and panel_input.cpp (detectNewContent only; Psi's two named regions in that file are landed and are other functions) — the only pre-existing shell files touched. Disjoint from Gamma (core+shell/instrument) and Epsilon (core+shell/package). Phase Omega — Responsiveness + control surface (none of the seventeen; a direct list of seven) W1 The five defects, in parallel [5 tracks, file-disjoint] T1 mode-switch-responsiveness .. Omega.7 structural: undo mask (-1 = UNDO_STATE_ALL, pulls every FX chunk), unconditional SetOffline, PreventUIRefresh (used NOWHERE in src today), compare-before-write, resolve() map, and the F6 split: UI/routing sync, per-FX park DEFERRED. New TU beside view.cpp (642 lines, over ceiling). T2 mode-switch-persist ......... Omega.7 persist: a switch rewrites all SEVEN ext-state keys when only view_state moved. T3 meter-rate .................. Omega.1: ONE 500ms timer today -> second timer id for the meter; 500ms poll body UNCHANGED. Paint: dirty rect, hoisted bitmap, hero-envelope cache. 60 FPS target, 30 the measured fallback. T4 deck-reflow-focus-toggles ... Omega.2 + Omega.3 + Omega.4. Filter Mod -> FILTER ENV last-in-run (id 1240 FROZEN, annotate only); run CENTRED at kDeckCellW; single buttons in two variants (enable = AccentPrimary|dim gray, mode selector = reads current mode, no off state); whole env deck = overlay target, radios gone but DeckRadioDesc survives for MASTER's passive lamp. [F3 + F4 RULED. Gate face stays PIXEL-IDENTICAL. COST: the filter tie-line is unreachable after the move — stated, not discovered] T5 overlay-mapping ............. Omega.5 + Omega.6. Trigger = marks HIDDEN; Gate loop-off = GREY DISABLED and still grabbable; GATE ONLY caption gone, the other two survive. ONE mapping for both chains — four discrepancies (2px inset, round vs truncate, closed vs half-open, half-pixel phase). Overlay-inset route recommended; draw_kit route is [propose]. Zero-crossing snap behaviour-identical. Fixtures RE-DERIVED. [F1 + F2 RULED] W2 Measurement, and what only a DAW can say [3 tracks] T1 view-topology-cache ......... core/view memoization [GATED on W1-T1's measurement; "not worth taking" is a legitimate outcome] T2 content-detect-throttle ..... the 30/sec enumerate, throttled not weakened T3 omega-verification-sweep .... the deliverable is a RECORD, not a fix Small tidy work sequenced BEFORE Lambda, and deliberately not another Lambda: no format, no wire version, no parameter id, no new artifact. All six D-rulings SETTLED 2026-08-03 (F1 widen the mapping scope; F2 hidden in Trigger / grey-disabled in Gate; F3 single button extends to Filter, Loop, Mono|Stereo; F4 AccentPrimary not AccentHot; F5 both entry surfaces; F6 separate UI from processing). ZERO open forks. Two invariant amendments are track deliverables: instrument-control-surface.md 6.3(d) + 6.4 (T5) and shell/view/CLAUDE.md's FX-parking load hitch (T1). The solo surface index is EXONERATED (~2T calls, sub-ms) — do not re-litigate it. reasampler_editor.h has ~36 lines of ceiling margin and THREE W1 tracks add state to it; the drag-state seam TODO.md names is OUT OF SCOPE — a track over the ceiling stops and flags. Shared files, named: reasampler_editor.h (T3|T4), editor_paint_waveform.cpp (T3 envelope cache | T5 marks+mapping), editor_controls.cpp (T4 deckDescs | T5 waveMarksFor), app/CMakeLists.txt + main.cpp (T1, cross-phase with Lambda). Phase Lambda — ReaSampler on Linux (none of the seventeen; a direct request 2026-08-02) W1 The audits [COMPLETE] T1 build-toolchain-audit ....... L-01..L-10, V1..V10, D1..D7 T2 source-runtime-audit ........ L2-01..L2-12 W2 Make it buildable, and make it honest [4 tracks, file-disjoint] T1 linux-compile-blockers ...... L2-01, L2-02, L2-03 -> V1, V2 T2 toolchain-floor ............. L-02..L-05, L-07, L-09, L-10 -> V5, V6, V8 T3 panel-dialog-resource ....... L-01, L2-06, L2-07 -> V7 [GATED: L-F2] T4 locale-independent-numerics . L2-04 [the one track verifiable on Windows] W3 First light, and the verification sweep [1 track, deliberately] T1 linux-verification-sweep .... V1..V10 + T2 section 5's seven; the deliverable is a RECORD, not a fix. V1 / V9 / the API-load count run FIRST — each reprices what follows. W4 Correctness and safety, repriced [3 tracks, disjoint by dir] T1 prune-deletion-safety ....... L2-05 both halves + the symlink reclaim lie T2 linux-font-faces ............ L2-09 T3 source-partition-and-invariants . L-06, L2-10, L2-11 (Linux half) W5 Ship the extension [1 track; GATED: L-F4] T1 linux-packaging-and-install . L-08 + L-02's shipped half + both channels W6 The instrument module + the host-safety contract [T1 before T2 — serial] T1 vst-linux-module ............ B5 (the D5 reversal, FIRST), B1, B2 T2 vst-host-safety-contract .... D3's observable contract; passes with ZERO params W7 The SWELL bootstrap [1 track] T1 swell-dylib-bootstrap ....... B3 via route B3a, WITHOUT the stub's exit(2) [prereq: V9 affirmative, else L-D2 re-rules] W8 The X11 editor [1 track] T1 x11-embed-view .............. B4, L2-08. The phase's one L-band source item. NOTHING here has been verified on a Linux machine. W2's edits are authored BLIND from the audits' citations and their acceptance criteria are discharged in W3 — W2 is not "done" until W3 runs, and this plan says so rather than pretending otherwise. W6 may run concurrently with W4 and W5 — file-disjoint; the only reason to serialise is attention. Six D-rulings are SETTLED (instrument in scope; SWELL via dlopen of REAPER's libSwell.so; REAPER-only editor but other hosts must degrade SAFELY; macOS out; shipped not developer-only; hard unlink acceptable with a platform-aware confirmation). OPEN, none ruled: L-F1 (CI — gates nothing), L-F2 (dialog-resource route — the ONLY fork gating a dispatch, W2-T3), L-F3 (copy-only invariant wording — gates nothing), L-F4 (declared support floor — gates W5, not a dispatch). Lambda registers NO VST3 parameter, ever — that table is Gamma-W4-T1's and is frozen. ```