Files
reasampler/docs/PLAN.md
T
daniel cca7d1a538 docs: frame the single-cycle / wavetable direction, eight forks open
Where the instrument's loop grid, crossfade and pitch handling stop making
sense as a capture shrinks toward one cycle, and what "wavetable synth" would
mean here. Two structural results: the pre-seam crossfade is the identity map
at one-period loop lengths, and YIN cannot detect a single cycle, so the period
is declared rather than detected there. Four candidate shapes, W3 recommended
out. PLAN.md gets a pointer under "Flagged for awareness" only.
2026-08-03 15:08:11 -04:00

2218 lines
153 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<phase>-w<wave>-t<track>-<slug>`. 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 —
§§610 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 carries EIGHT open [Daniel]-class forks and none is ruled**, so it gets a pointer
here rather than a phase section: `PLAN.md` is the active on-deck specification list, and
a phase with eight 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 forks are ruled, the doc becomes a phase's backing product doc in the ordinary
way (the Γ / Ε / Ρ / Λ pattern, not the doc-less Ψ / Ω one).
**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 `<module>_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.51 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 12 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 T2T5. **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` `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 `td` 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 T1T4. **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:606608`; 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:1820`, the Windows case-fold) is already correct for Linux with both
branches asserted by `tests/test_capture_paths.cpp`.
- **Every pure module gets a `<module>_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`, `:7077`),
`src/shell/actions/instrument_drop_win.cpp` (`writeTempPreset`, `:5061`), `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:2830` 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 `:169171`, the dialog proc's platform contract, the drop-accept opt-in at `:179184`),
`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:** V1V10 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
(`:154158`) 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:855` (**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:711` 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 `<windows.h>` 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/<uname -m>-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:103104`). 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, `:154158`) | 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` §§610. **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.
```