diff --git a/docs/PLAN.md b/docs/PLAN.md index 0b43203..16cb96b 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -10,7 +10,9 @@ come from the seventeen: it came from a direct list of seven defects and refinem 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`. +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`. ## What this doc is, and how it relates to the others @@ -31,9 +33,11 @@ seventeen: it came from a direct request (Daniel, 2026-08-02) and is scoped in cited section rather than reading a file whole. **Worktree slug convention:** `p-w-t-`. Greek phase letters -transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**, **Ψ → `psi`**, **Ε → `e`**, **Ρ → `r`**. So -Θ-W1-T1 dispatches into `pth-w1-t1-zone-retirement`, Γ-W1-T1 into -`pg-w1-t1-knob-interaction-law`, and Ψ-W1-T1 into `ppsi-w1-t1-capture-range-exactness`. +transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**, **Ψ → `psi`**, **Ε → `e`**, **Ρ → `r`**, +**Λ → `l`**. 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`, and +Λ-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`). ## Decision state @@ -42,6 +46,12 @@ reading code or running the DAW) or **[propose]** (a design call made at impleme 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/ @@ -91,6 +101,36 @@ would otherwise reverse it on the next timer tick. **The plan-wide claim above t 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 and Ε'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.** + **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 @@ -3189,6 +3229,891 @@ rather than add a message for a case the existing path already handles correctly --- +## Phase Λ — ReaSampler on Linux: both artifacts, shipped + +**Ships:** `reaper_reasampler.so` and `reasampler_9000.vst3` built, installed and documented +on Linux. The extension compiles under GCC/Clang, builds optimized by default, docks its +panel, writes a bank index that survives a comma-decimal locale, and prunes without lying +about what it reclaimed. The instrument loads, scans, instantiates and processes audio in +**any** Linux host, and opens its editor under **REAPER only** — a stated contract, not a +shortfall. macOS is out. + +**Four named exceptions to platform parity**, stated up front rather than discovered: +prune deletes permanently (no trash); the OS drag-out's copy-only guarantee is conventional +rather than structural, because SWELL's file-list drag takes no effect mask; the type faces +are DejaVu rather than Segoe UI and Consolas; and the ReaSampler 9000 editor comes up under +REAPER only, with every other host getting its generic parameter UI. + +**Consolidates: none of the seventeen.** Phase Λ came from a direct request (Daniel, +2026-08-02) and is scoped in `docs/product/linux-readiness.md`, which is itself downstream of +two landed audits: `docs/product/audit-notes/lambda-w1-t1-build-toolchain.md` (findings +Λ-01…Λ-10, verify items V1…V10, forks D1…D7 — build system, toolchain, vendored deps, +resources, test harness, packaging) and +`docs/product/audit-notes/lambda-w1-t2-source-runtime.md` (L2-01…L2-12 — source portability +and runtime behaviour). **Grep those for a cited finding number; do not read either whole.** +This phase **supersedes nothing**, and corrects one standing promise rather than inheriting +it: `docs/product/versioning-and-release.md` already commits in writing to "three platform +artifacts per channel per release" (T1 §1e), which Λ-D4 makes **two** for now, with macOS +named as deferred rather than silently dropped. + +**Nothing in this phase has been verified on a Linux machine.** Every `[verify — Linux]` +mark below is load-bearing and none may be laundered into settled voice. The structural +consequence is stated once here because it shapes the whole wave order: **Λ-W2's edits are +authored blind from the audits' citations, and Λ-W3 is the session that discharges Λ-W2's +acceptance criteria.** Λ-W2 is not "done" in the usual sense until Λ-W3 runs, and a track +that reports otherwise has laundered unverified work into landed work — which is exactly +what the audits' `[verify]` discipline exists to prevent. Both audits state their own effort +bands as provisional until the sweep runs; this plan does not restate a band the audits did +not give. + +### Rulings — Daniel's, 2026-08-02. Six, settled. + +Full statements with the reasoning Daniel gave: `docs/product/linux-readiness.md` +§"Settled decisions (Daniel, 2026-08-02)". **Do not re-litigate these.** + +| Ruling | What it settled | Specified in | Bound into | +|---|---|---|---| +| **Λ-D1** | **The VST3 instrument is in scope** (audit fork D1 = **Fork B**). Both artifacts ship, not just the extension. **Reverses D5's platform clause in writing** — "Windows-only, VST3-only, REAPER-only" becomes "Windows and Linux; the editor is REAPER-hosted"; the VST3-only and REAPER-only clauses survive verbatim | §Λ-D1 | Λ-W6…Λ-W8; the D5 rewrite is Λ-W6-T1's first act | +| **Λ-D2** | **SWELL is reached by `dlopen`ing REAPER's own `libSwell.so`** (route **B3a**). Route B3b — building SWELL into the module, and with it GDK/GTK3, FreeType, Fontconfig, OpenGL — is **rejected**, not deferred | §Λ-D2 | Λ-W7-T1 | +| **Λ-D3** | **Non-REAPER hosts must be protected** — *"REAPER-only is good for now, but we need to protext other hosts."* A first-class acceptance criterion of two tracks, not a footnote on one | §"The non-REAPER-host safety criterion" | Λ-W6-T2 (contract), Λ-W7-T1 (runtime), Λ-W8-T1 (regression) | +| **Λ-D4** | **macOS is out** (D3 = **out**) — *"I don't have a mac to compile on."* Shared macOS/Linux edits are still made **in their shared form and noted as shared**; what is out is macOS as a deliverable, any macOS verification, signing/notarization, and L2-11's APFS half. **Do not "fix macOS while you're in there"** — an unverifiable edit to the APPLE branch is worse than none, because it looks tested | §Λ-D4 | phase-wide | +| **Λ-D5** | **The Linux artifact is shipped, not developer-only** (D6 = **shipped**), which promotes Λ-02 (a real optimized build) and Λ-08 (a documented install path) to must-fix. **And a hard `unlink` prune is acceptable**, with a **platform-aware confirmation string** saying so | §Λ-D5 | Λ-W2-T2, Λ-W4-T1, Λ-W5-T1 | +| **Λ-D6** | **The X11 editor is real work and is sequenced last.** Not a separate ruling — the direct consequence of D1+D2+D3. Everything before it ships something; halting Λ-W8 still leaves a shipped extension and a loading, processing, generic-UI instrument | §Λ-D6 | the wave order itself | + +### Open forks — four, NONE ruled + +Stated with the evidence for both sides at `docs/product/linux-readiness.md` §"Open forks — +Daniel's". This is the one phase in this plan with unanswered [Daniel]-class questions; see +"Decision state" above for how that reconciles against the plan-wide claim. + +| Fork | Question | Recommendation | What it blocks | +|---|---|---|---| +| **Λ-F1** | Does CI get built in this phase, and on what runner? (audit D4) | **No recommendation — genuinely a resourcing call.** For: two toolchains and one is not on the developer's machine, so every Windows-only commit becomes a latent Linux regression. Against: CI is infrastructure, its value is highest *after* the first Linux build works, and a runner forces Λ-F4 immediately | **Nothing.** Decides only whether Λ-W5-T1 writes a pipeline paragraph or a "deferred to dev-ops" one | +| **Λ-F2** | The dialog-resource route: **resource-id-0** (SWELL's documented escape hatch, `swell-functions.h:606–608`; deletes the whole resgen pipeline from non-Windows builds) or **wire up resgen properly** (PHP, `add_custom_command`, an include-shim TU — the M end of Λ-01's band)? (audit D7) | **No clean recommendation; the asymmetry neither audit stated is the reason it is worth ruling.** Route A is cheaper and structurally simpler and stakes the panel's file-drop ingest on an *unverified* `ChildWindowFromPoint` descent, because with no resource there is no `WS_EX_ACCEPTFILES` bit to set (L2-07). Route B can set that bit explicitly. **A third option, if the cheap route is wanted without the exposure:** take Route A and make the file-drop check a hard gate in Λ-W3's sweep, with Route B as an additive follow-up track rather than a rewrite — it spends a second Linux session in the bad case | **Λ-W2-T3's dispatch — the ONLY fork here that gates a track.** Unruled, that track slips to Λ-W4 and Λ-W3's sweep splits into two Linux sessions | +| **Λ-F3** | Does "copy-only is structural" survive as a shipped invariant? (T2 [Daniel] 4) | **Keep the invariant, scope the *enforcement* claim to Windows** — "structurally enforced on Windows via `DoDragDrop`'s copy-only mask; advertised, not enforced, on SWELL, which takes no effect mask." The real counter: an invariant one platform cannot enforce is arguably not an invariant, and weakening the global wording would also correctly warn a Windows reader off relying on it in shared code | **Nothing.** Λ-W4-T3 makes the edit either way; only the sentence changes. Answerable at implementation review | +| **Λ-F4** | What is the declared support floor? (new — the audits raised the inputs Λ-07, V8, V10, not the decision) | **Declare a narrow floor — current-stable-distro glibc, GCC ≥ 9, `x86_64-linux` only — and widen it on request.** Counter: REAPER's Linux reputation is partly built on modest and non-x86 hardware, `aarch64` is no longer exotic, and adding it later means a second bundle directory and a second verification pass on every release | **The ship wave, not a dispatch.** Non-gating for Λ-W2 (a recorded floor can be widened); **gating for Λ-W5-T1** — a shipped artifact has to say what it runs on. Also feeds Λ-W6-T1's bundle directory set and Λ-F1's runner image | + +### Phase-Λ acceptance criteria + +These bind every track in this phase, in addition to the plan-wide set above. + +- **Windows behaviour does not change. Every track carries that criterion explicitly.** This + is a port; a port that improves Windows by accident has also changed Windows by accident. +- **`[verify — Linux]` is a status, not a decoration.** A criterion carrying it is discharged + by a recorded observation in Λ-W3's verification record — never by inspection, never by + "should work". A track whose Linux criteria are all unrun is not landed; it is authored. +- **No vendored file is patched.** `git status` under `vendor/` stays clean through the whole + phase, including the SWELL bootstrap — which is precisely the constraint that shapes it. +- **No new third-party dependency surface.** Λ-D2 rejected route B3b for this reason: no + vendored SWELL build, no GDK/GTK3, no FreeType, no Fontconfig, no OpenGL, no `pkg-config` + in this tree. **A track that finds itself reaching for one of those has drifted and stops.** +- **Shared macOS/Linux edits are made in shared form and labelled shared** (Λ-D4). Λ-01, + Λ-03, Λ-04, Λ-07, Λ-09 and L2-09 are all shared by the audits' own marking; an + `#ifdef _WIN32` / `#else` that is right for both costs nothing and needs no mac. No macOS + verification is claimed for any of them. +- **`core/` stays pure and stays platform-neutral.** Every `#include` under `src/core/**` is + a `core/` sibling, one of 26 standard headers, or the generated `version_generated.h` (T2 + §1.1) — the audits verified this rather than assuming it, and no Λ track may be the one + that breaks it. The one platform fork in the whole directory + (`capture_paths.cpp:18–20`, the Windows case-fold) is already correct for Linux with both + branches asserted by `tests/test_capture_paths.cpp`. +- **Every pure module gets a `_tests` target** that runs without REAPER or a DAW. + Λ-W2-T4 is the phase's proof that this is not ceremonial: it is a `core/`-only fix, fully + testable on the current Windows box, and it lands before any Linux session. +- **The ~600-line ceiling and the structural heuristics bind unchanged.** Λ-W8-T1 is the + track most likely to strain them; its own criteria name the seam vocabulary to reuse. + +**Performance posture.** No named hot path is touched by any track in this phase — not the +`peaks` envelope compute, not audition, not the realtime-capture tick's single-pointer-test +idle fast path, not the instrument's `process()`. Two consequences are stated as criteria +rather than left implicit: **nothing is added to `process()`** (no `dlopen`, no probe, no +platform branch on the per-voice-per-sample path — the SWELL availability probe is computed +once, lazily, off the audio thread), and **Λ-W2-T4's locale fix stays on the JSON/persist +path**, which root `CLAUDE.md` already declares off all hot paths. Λ-02 is the phase's one +genuine performance item and it runs the other way: today a Linux build carries **no `-O` +flag at all**, on a tree whose `peaks` path is documented as presuming an optimizing build. + +### The non-REAPER-host safety contract + +Λ-D3 stated as something a person can check. **This is an observable contract, and it is the +specification the two instrument waves are graded against** — the full argument is +`docs/product/linux-readiness.md` §"The non-REAPER-host safety criterion", which a brief for +Λ-W6-T2, Λ-W7-T1 or Λ-W8-T1 must be written against. + +**The finding that makes it urgent, and it is a fact rather than a risk.** The vendored +`vendor/WDL/WDL/swell/swell-modstub-generic.cpp` declares a **file-scope static** at `:125`, +so its constructor runs when our `.so` is `dlopen`ed — i.e. **during the host's plugin +scan** — and `:102` calls **`exit(2)`** when `dlopen` of `libSwell.so` fails, `:117` +**`exit(1)`** on an incomplete API table. Compiling the vendored `SWELL_LOAD_SWELL_DYLIB` +path unmodified therefore means **a Bitwig or Ardour plugin scan on a machine without +`libSwell.so` terminates the host process** — a killed DAW mid-scan, with the user's +session. The mitigating detail that shapes the fix: `doinit` substitutes a zero-returning +`dummyFunc` for each unresolved name, so a *partial* table degrades rather than crashes. A +partial load is survivable; `exit()` is not. + +**In any Linux host, with or without `libSwell.so`:** the module scans and enumerates its one +class with no crash, hang, process exit or blacklist entry; it instantiates, produces audio, +plays MIDI, and round-trips component state byte-identically with the Windows build; when no +usable platform surface exists `isPlatformTypeSupported` returns `kResultFalse` for **every** +type — including `kPlatformTypeX11EmbedWindowID` — and `createView(kEditor)` returns +**`nullptr`**, never a view that then fails to attach and never one that draws nothing; the +host falls back to its generic parameter UI; exactly **one** diagnostic line per process +names why the editor is unavailable, through the SDK's logging or stderr, never a modal; +teardown crashes nothing and leaves no partially-initialised SWELL table reachable. + +**Verified with a harness that is already on disk and that neither audit named**, because +neither swept the SDK's `samples/` tree: +`vendor/vst3sdk/public.sdk/samples/vst-hosting/validator/` is a scriptable, headless, +REAPER-free host, under `public.sdk/` and therefore pulled by the documented narrow submodule +init. The four checks, all `[verify — Linux]`: `validator` completes with **no `libSwell.so` +on the filesystem** (the direct negation of the `exit(2)` finding, and the load-bearing one); +a real Ardour or Bitwig scan reaching browser, instantiation and generic UI; a REAPER-on-Linux +load where the editor opens; and a **negative control** — rename `libSwell.so` beside a Linux +REAPER and confirm the generic-UI fallback plus the single diagnostic line. + +### Cross-phase boundary — Γ-W4-T1, and the frozen parameter table + +**Λ must never register a VST3 parameter.** Γ-W4-T1's `ParamID` table is FOREVER-FROZEN from +the moment it ships, on the same footing as the command-id strings and the class UIDs. A +parameter minted in Λ to make a Linux fallback look better would collide with a table Λ does +not own. **This is a hard boundary and it is the only Γ↔Λ interaction that could actually go +wrong.** + +Neither phase blocks the other. The host's fallback for a plug-in reporting no usable editor +is its generic parameter UI, and today the plugin registers zero parameters — so "no editor" +currently degrades to *nothing* rather than to *controls*. **Λ-W6-T2's acceptance criteria are +written to pass with zero parameters registered**: an empty generic UI is a pass. Once +Γ-W4-T1 lands its 44 derived parameters the identical Λ code path degrades to a usable +generic UI instead — a better result, not a different criterion. **Λ assumes no schedule for +Γ**; a Λ track needing a Γ fact reads `dev` at dispatch time. + +**Concurrency.** Λ is the widest phase in this plan by file surface, and it is disjoint from +the others by *kind* rather than by directory: its edits are platform guards, CMake, docs and +one new `shell/instrument/` TU. Γ owns `core/instrument/` + `shell/instrument/` **sources** +(Λ-W6…Λ-W8 own that directory's CMake, its `CLAUDE.md` files, and `editor_platform`'s +non-Windows branch); Ε lands in the new `core/package/` + `shell/package/`; Ρ touches +`core/capture`, one new `shell/capture` TU, `panel_input.cpp` and `main.cpp`. **The genuine +adjacency to watch is CMake**: Ε appends two `add_subdirectory` lines to the root +`CMakeLists.txt` while Λ-W2-T2 owns that file's language and target settings — append-only +lines against property blocks, textual adjacency rather than semantic contention. Λ's own +shared files are named in the wave sections below. + +--- + +### Λ-W1 — The audits *(complete)* + +Two tracks, both landed: **T1 `build-toolchain-audit`** (Λ-01…Λ-10, V1…V10, D1…D7) and +**T2 `source-runtime-audit`** (L2-01…L2-12). Recorded so the wave numbering matches the audit +filenames and branch names. Nothing to re-spec; nothing to dispatch. + +--- + +### Λ-W2 — Make it buildable, and make it honest + +**Depends on:** nothing. **Four tracks, disjoint at the file level.** Three are authored +blind against the audits and verified in Λ-W3; **T4 is the one track fully verifiable on the +current Windows box.** + +| Track | Owns | +|---|---| +| **T1** `linux-compile-blockers` | two source blockers + `main.cpp`'s API-load failure branch | +| **T2** `toolchain-floor` | the CMake files' language, property and platform blocks | +| **T3** `panel-dialog-resource` | `panel_window.cpp`'s dialog creation, `resource.rc` / `resource.h` — **gated on Λ-F2** | +| **T4** `locale-independent-numerics` | the number codec in four `core/` TUs | + +**Shared file in the wave, named rather than discovered at merge:** +`src/app/CMakeLists.txt` — T2 takes the property and platform blocks, T3 takes **one +`target_sources` line under the resgen route only**. Disjoint regions; whichever lands second +rebases. The wave's severity question is dissolved rather than adjudicated: T2 grades L2-03 a +Blocker on failure-mode quality with no direct evidence and L2-04 a Major with a confirmed +mechanism, and flags its own inconsistency — both land here, in different tracks, so the +relative grade never has to be settled. + +#### Λ-W2-T1 — `linux-compile-blockers` + +**Goal.** The extension compiles and links under GCC/Clang, and when it refuses to load it +says why instead of vanishing. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W2-T1; findings L2-01, L2-02, L2-03. + +**Surface boundary — owns:** `src/shell/panel/draw_kit.cpp` (`loadFont`, `:70–77`), +`src/shell/actions/instrument_drop_win.cpp` (`writeTempPreset`, `:50–61`), `src/app/main.cpp` +(**the `REAPERAPI_LoadAPI` failure branch only**). **Does not own:** any `CMakeLists.txt`, +`panel_window.cpp`, or any `core/` file. + +**Behavior.** +- **`FF_DONTCARE` (L2-01).** `draw_kit.cpp:73` passes `DEFAULT_PITCH | FF_DONTCARE` to + `CreateFont`; the symbol has **zero occurrences anywhere in `vendor/WDL/`**, and the file is + not platform-guarded, only its include is. `draw_kit` links into **both** modules, so + nothing builds until this is fixed. Drop the term or define it locally in the non-Windows + include branch. **Do not add `windows.h`** — L2-01's stated direction. The family bits are + advisory to Windows' font mapper and meaningless to fontconfig. +- **`GetCurrentProcessId()` (L2-02).** `instrument_drop_win.cpp:59` calls it with no platform + branch anywhere in the TU; SWELL exports `GetCurrentThreadId` and not this. The PID exists + only to keep two concurrent REAPER instances from colliding in the shared temp dir, and the + atomic counter at `:52` already carries the intra-process half. Replace with a + platform-neutral uniqueness source behind a guard. +- **The silent load failure (L2-03).** Either switch `main.cpp` to `REAPERAPI_MINIMAL` plus an + explicit `WANT` list — the pattern `panel_window.cpp` and `panel_audition.cpp` already use, + and the honest inventory of what this extension actually needs — or keep the full load and + print the failure count via `rec->GetFunc("ShowConsoleMsg")` before returning 0. + +**Acceptance criteria.** +- `cmake -B build -S . -G Ninja && cmake --build build` produces `build/reaper_reasampler.so` + with no errors. `[verify — Linux]` = **V1**. +- `ctest --test-dir build --output-on-failure` passes all 91 test targets, **no `-C` flag + needed** on a single-config generator. `[verify — Linux]` = **V2**. +- A deliberately misspelled `WANT` entry (or a forced non-zero `failcnt`) produces a visible + REAPER console line naming the count, not a silent refusal. +- Windows build and `ctest` unchanged. + +**Prerequisites.** None. **Discharges:** L2-01, L2-02, L2-03; enables V1, V2, V3. + +#### Λ-W2-T2 — `toolchain-floor` + +**Goal.** The Linux build is optimized when asked, links what it uses, hides what it does not +export, and reports diagnostics no one has seen yet. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W2-T2; findings Λ-02, Λ-03, Λ-04, Λ-05, Λ-07, +Λ-09, Λ-10. + +**Surface boundary — owns:** root `CMakeLists.txt`, `src/app/CMakeLists.txt` (**the target +property and platform blocks; NOT the source list — that is Λ-W4-T3's**), +`cmake/reasampler_targets.cmake`, `src/shell/instrument/CMakeLists.txt` (**thread linkage +only; the `WIN32` gate is Λ-W6-T1's**), `README.md`, and root `CLAUDE.md` §"Build and test" / +§"Install / reload" / §"One-time submodule setup". **Does not own:** any `.cpp` or `.h`. + +**Behavior.** +- **Λ-02** — root `CMakeLists.txt:28–30` is the *complete* list of language settings: no + `CMAKE_BUILD_TYPE`, no `CMAKE_CXX_FLAGS`, no IPO/LTO, no `target_compile_options` anywhere + in the tree. Default `CMAKE_BUILD_TYPE` when neither it nor `CMAKE_CONFIGURATION_TYPES` is + set, and correct the docs' ship instruction: `--config Release` is *accepted and ignored* + by Ninja and Make, and the README currently sends a Linux user to `build/Release/`, which + does not exist there — the module lands at `build/reaper_reasampler.so`. +- **Λ-03** — `CXX_VISIBILITY_PRESET hidden` + `VISIBILITY_INLINES_HIDDEN` on both module + targets. SWELL's own build already uses `-fvisibility=hidden`, and the symbols that must + stay exported carry their own `visibility("default")` attributes + (`reaper_plugin.h`'s `REAPER_PLUGIN_DLL_EXPORT`, `fplatform.h`'s `SMTG_EXPORT_SYMBOL`, + `swell-modstub-generic.cpp:135`'s `SWELL_dllMain`). +- **Λ-04** — `find_package(Threads REQUIRED)` + `Threads::Threads`. Correct on all three + platforms, costs nothing on Windows. +- **Λ-09** — `-Wall -Wextra` and `CMAKE_CXX_EXTENSIONS OFF`. **No `-Werror` in this change** + (the audit is explicit): the diagnostic-set size over this tree is not estimable from + Windows. Separately `-Wl,--no-undefined` on the module targets, restoring the + fail-at-link-time behaviour MSVC gives and GNU `ld` does not — **or** a recorded reason why + `SWELL_PROVIDED_BY_APP`'s function-pointer design makes the gap moot. +- **Λ-05** — pin the `reaper_plugin.h` → `../WDL/swell/swell.h` include coincidence with a + comment or an `INTERFACE` target carrying both include dirs as one unit. Invisible on + Windows, load-bearing off it. +- **Λ-07** — record the compiler floor (**Λ-F4's input**), or add `-lstdc++fs` and document + why. +- **Λ-10** — one sentence in the platform-support docs: on Linux `vendor/vst3sdk` is optional + until Λ-W6, so `git submodule update --init vendor/reaper-sdk vendor/WDL` is the complete + extension-only prerequisite. + +**Acceptance criteria.** +- `compile_commands.json` or a verbose build log shows an explicit `-O` flag on a bare + `cmake --build build`. `[verify — Linux]`. +- `nm -D --defined-only reaper_reasampler.so | grep -E 'ReaperPluginEntry|SWELL_dllMain'` + finds **both** after the visibility preset. `[verify — Linux]` = **V6**. +- The extension links with `Threads::Threads` **removed** — proving the include-only pthread + dependency needs no flag — or the symbol forcing it is named. `[verify — Linux]` = **V5**. +- `capture_paths_tests` links without an explicit `-lstdc++fs` on the declared floor, or the + flag is added and the floor documented. `[verify — Linux]` = **V8**. +- **The warning count from the first `-Wall -Wextra` build is recorded, not fixed**, and + handed to Λ-W4 as an input. +- Windows build unchanged; `--config Release` still behaves as documented there. + +**Prerequisites.** None; concurrent with T1, T3, T4. **Discharges:** Λ-02, Λ-03, Λ-04, Λ-05, +Λ-07, Λ-09, Λ-10; enables V5, V6, V8. + +#### Λ-W2-T3 — `panel-dialog-resource` *(GATED on Λ-F2 — do not dispatch until it is ruled)* + +**Goal.** The docked bank panel opens on Linux, and if it ever fails to, it says so. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W2-T3 and §Λ-F2; findings Λ-01, L2-06, L2-07. + +**The defect.** `panel_window.cpp:135` 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:97` has the `target_sources` line commented out (`:86` for macOS). +The registry head stays null, `SWELL_CreateDialog` returns null, `:137` 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 `:96` +(`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 `:135–137`, the dialog proc's platform contract, the drop-accept opt-in at `:145–150`), +`src/resource.rc`, `src/resource.h`, and — **under the resgen route only** — one +`target_sources` line in `src/app/CMakeLists.txt`'s `else()` branch plus a new include-shim +TU. **Does not own:** any other panel TU, `draw_kit`, or any CMake target property. +`panel_window.cpp` is **deliberately not split** across tracks: the L2-06 diagnostic and the +Λ-01 resource route are the same function, and under the id-0 route the same *line*. + +**Behavior.** Whichever route Λ-F2 picks, plus — **unconditionally, and on both platforms +rather than behind a guard** — a one-line `ShowConsoleMsg` on the `!g_panel.hwnd` path naming +the missing dialog resource (L2-06). That single line converts a mystery into a two-minute +diagnosis and is worth having on Windows too. The fix is shared macOS/Linux either way +(Λ-D4: made in shared form, verified on Linux only). + +**Acceptance criteria.** +- The panel toggle action docks a visible, LICE-drawn bank panel in a Linux REAPER. + `[verify — Linux]` = **V7**. +- With the resource deliberately unavailable, the toggle prints one console line rather than + doing nothing. **Verifiable on Windows by forcing the branch.** +- Dragging a WAV from the file manager onto the docked panel ingests it — or, if it does not, + the failure is understood rather than mysterious. `[verify — Linux]`, T2 §5 item 3. + **This criterion's difficulty depends on the Λ-F2 route** and is the fork's substance. +- Windows panel behaviour byte-for-byte unchanged. + +**Prerequisites.** **Λ-F2 must be ruled before dispatch.** If it is not, this track slips to +Λ-W4 and Λ-W3's sweep splits into a panel-independent half (run early) and a panel-dependent +half (run after this lands) — a second Linux session, which is the cost of leaving the fork +open. **Discharges:** Λ-01, L2-06, L2-07; enables V7. + +#### Λ-W2-T4 — `locale-independent-numerics` + +**Goal.** A persisted float round-trips identically regardless of process locale, so a bank +index written on a comma-decimal machine is not written unparseable. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W2-T4; finding L2-04. + +**Why it is pulled forward, ahead of everything it looks like it should follow:** it is the +phase's one substantial fix that **needs no Linux box at all** — two writers and three +readers, all in `core/`, all unit-testable on Windows today — and its failure mode is that +the bank index is written unparseable and **the project's whole bank is lost on reload.** It +should land before any Linux user saves a project. + +**Surface boundary — owns:** `src/core/json/json.cpp` (the `%.17g` writer and the `strtod` +reader), `src/core/model/provenance.cpp` (its own `%.17g`), `src/core/wire/wire.cpp` +(`Cursor::fieldDouble`), `src/core/capture/render_settings.cpp` (the `std::stod` over +REAPER's `P_RAZOREDITS`), and the corresponding `tests/`. **Does not own:** any shell TU, any +CMake file, `wav_codec` (its byte-order handling is already explicit and correct). + +**Behavior.** Make the number codec locale-independent at its two writers and three readers — +`std::to_chars`/`std::from_chars`, or a `std::locale::classic()`-bound stream. **Do not "fix" +this by calling `setlocale`**; an extension must not mutate the host's locale. Why it is not +paranoia: on Windows the CRT starts in the `"C"` locale and nothing here calls `setlocale`, +which is why it has never fired; on Linux SWELL's GDK backend calls +`gtk_init_check`/`gdk_init_check` and never calls `gtk_disable_setlocale`, and any GTK or Qt +plugin in the same process can do the same. The readers are honestly fail-closed — they +require whole-token consumption — so the failure is "the field disappears", not "the field is +silently wrong". + +**Acceptance criteria.** +- New pure tests pass **on Windows today**, under a forced comma-decimal `LC_NUMERIC` set + inside the test. **This is the one Λ-W2 track that does not wait for Λ-W3.** +- The bank index, view model, tracking ledger, provenance blob and tail setting all round-trip + a fractional value under that forced locale. +- **Byte-for-byte identical output to today's writer under the `"C"` locale** — this is a + persisted format, and a changed representation is a compatibility event. +- Nothing on a hot path is touched; the JSON/persist path is declared off all hot paths in + root `CLAUDE.md` and stays there. + +**Prerequisites.** None. **Discharges:** L2-04. +**Note:** T2 §5 item 1 (read `LC_NUMERIC` inside a running REAPER-Linux process) stays in the +Λ-W3 sweep, but only to record how urgent this *was* — the work is not gated on it. + +--- + +### Λ-W3 — First light, and the verification sweep + +**Depends on:** Λ-W2. **One track, deliberately** — this is a person at a Linux box working a +checklist where each answer reprices the next; splitting it across specialists buys no +concurrency and loses the thread. Precedent: Ρ-W1 and Γ-W4 are both single-track waves for +the same reason. + +#### Λ-W3-T1 — `linux-verification-sweep` + +**Goal.** Discharge every acceptance criterion Λ-W2 could not check from Windows, answer every +`[verify — Linux]` item in both audits, and reprice the remaining waves against what is +actually true. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W3-T1 and §"Why this sequence". + +**Surface boundary — owns: no source file and no CMake file.** Its deliverable is a +**verification record** at `docs/product/audit-notes/lambda-w3-verification.md`, one entry per +item: the exact check run, the observed result, and what it changes. **Any fix the sweep +motivates is filed to Λ-W4, not made here.** The one exception: a defect that blocks further +sweeping (the build does not link at all) is fixed in place and recorded as a deviation, +because the alternative is a wasted session. + +**Behavior — the sweep set.** + +| From | Items | +|---|---| +| T1 | **V1** compile, **V2** ctest, **V3** does REAPER's Linux build call `SWELL_dllMain` and populate the API table, **V4** where `UserPlugins/` actually is and whether `reaper_*.so` is the right glob, **V5** pthread link flag, **V6** visibility vs. the two exported symbols, **V7** the docked panel, **V8** `-lstdc++fs`, **V9** does `libSwell.so` sit beside REAPER's executable, **V10** which `uname -m` values the VST3 bundle must carry | +| T2 | process `LC_NUMERIC`; `REAPERAPI_LoadAPI`'s actual return value and the name of any gap; panel file-drop routing; which SWELL GDI/locale build REAPER ships; fontconfig's substitution for "Consolas"; `SWELL_InitiateDragDropOfFileList` acceptance by common targets, and whether its 500 ms no-motion timeout cancels a slow gesture; prune against an in-use file | +| New | the `-Wall -Wextra` diagnostic set, counted and categorised; whether `-Wl,--no-undefined` links clean or names an undefined set; whether the four-TU LICE slice links without `lice_colorspace.cpp` (T1 §3 leaves this an unreconciled inference) | + +**Three items are load-bearing beyond their own answer and must be run FIRST:** **V1** +(nothing else is observable until it passes); **V9** (a negative answer **voids Λ-D2's route +and needs a Daniel re-ruling** before Λ-W6 is dispatched — the entire `dlopen`-REAPER's-SWELL +design rests on it, and it is a one-line `ls`); and the `REAPERAPI_LoadAPI` count (a non-zero +result promotes L2-03 from a diagnostic to a real Blocker and names the gap). + +**Acceptance criteria.** +- Every item above has a recorded answer or an explicit "could not determine, because X". + **A blank is a failure of this track, not a deferral.** +- Λ-W2's four tracks each have their `[verify — Linux]` criteria marked discharged or failed, + **by name**. +- Λ-W4 and Λ-W5's scope is restated against the answers, with any effort band that moved + called out. **The audits' bands are provisional by their own statement; this is where they + stop being.** +- Λ-W6's prerequisites (V9, V10) are answered, or Λ-W6 is explicitly blocked pending a Daniel + re-ruling on Λ-D2. + +**Prerequisites.** Λ-W2-T1, T2, T4. **T3 if Λ-F2 was ruled** — otherwise the panel-dependent +items (V7, file-drop routing, any font check needing a rendered panel) defer to a second +session and that deferral is recorded. **Discharges:** V1–V10 and T2 §5's seven items, **as +answers rather than as fixes**. + +--- + +### Λ-W4 — Correctness and safety, repriced + +**Depends on:** Λ-W3-T1. **Three tracks, disjoint by directory.** Everything here is known +work whose *size* the sweep may have moved. + +#### Λ-W4-T1 — `prune-deletion-safety` + +**Goal.** Prune on Linux deletes only what it means to, tells the truth about what it +reclaimed, and tells the user the deletion is permanent. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W4-T1; finding L2-05 (both halves) plus the +symlink row of T2 §4. + +**Surface boundary — owns:** `src/shell/persist/prune_fs.cpp` (the deletion authority's +non-Windows branch and the reclaim scan) and the prune confirmation text wherever it is +composed (`shell/actions/prune_action` / `shell/panel/panel_bank_ops`). **Does not own:** +`core/reclaim/`'s orphan computation (pure, portable, unaffected), or any other persist TU. + +**Behavior.** +- **The confirmation string becomes platform-aware** (Λ-D5). On Linux it states the deletion + is permanent and there is no Recycle Bin; on Windows it says what it says today. **Same code + path, one platform-dependent phrase — not a second dialog.** +- **The dead "locked" branch is reckoned with.** `if (ec) return false; // real failure + (locked/permission) -> skip` encodes Windows file-sharing semantics; on Linux `unlink` of an + open file **succeeds**, the audio keeps playing from the open fd, and the bytes are gone + when it closes — so the branch never fires. Either it is documented as Windows-only in + place, or the Linux path acquires an equivalent guard. **What it must not do is stay + silently asymmetric**: the recovery floor root `CLAUDE.md` §"The resample bake" relies on + ("the superseded file survives on disk until a prune reclaims it") otherwise has nothing + under it on Linux. **This half is not covered by Λ-D5's trash ruling** — it is not a trash + question. +- **The symlink hazard is fixed.** `fs::directory_iterator` + `is_regular_file()` follows + symlinks under C++17; size is read from the target via `file_size()` but `fs::remove` + deletes the **link**, not the target — so prune reports N bytes reclaimed and reclaims + zero. Symlinked media folders are far more idiomatic on Linux than on Windows. **This is a + reporting lie, not a cosmetic issue.** + +**Acceptance criteria.** +- A Linux prune of a bank file currently playing behaves as recorded in Λ-W3's sweep, and the + behaviour matches what the confirmation promised. `[verify — Linux]`. +- A symlinked bank file is either skipped or deleted with its target, and the reclaimed-byte + figure matches what actually left the disk in **both** cases. Unit-testable for the + computation; `[verify — Linux]` for the filesystem half. +- The Windows path — `SHFileOperationW` + `FOF_ALLOWUNDO`, Recycle-Bin recoverable — is + bit-for-bit unchanged. +- **Prune remains the only file-deletion path in the system** (plan-wide product invariant). + +**Prerequisites.** Λ-W3-T1 (the in-use-file check). **Discharges:** L2-05 both halves, and the +symlink row of T2 §4. + +#### Λ-W4-T2 — `linux-font-faces` + +**Goal.** The kit asks for faces that exist on a stock Linux distro, so type is **chosen** +rather than substituted. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W4-T2 and §"Calls made here"; finding L2-09. +**This is a product-designer call, not a fork:** silent fontconfig substitution is not +acceptable because the kit's type is part of a deliberate visual identity +(`docs/product/visual-design-language.md`), and the palette-role discipline exists precisely +so a visual direction is a single-file change rather than an emergent property of the host. +The cost is five string literals. Contradict it in review with an argument. + +**Surface boundary — owns:** `src/shell/panel/draw_kit.cpp`'s five `loadFont` call sites +(`:154–158`) and the face constants in `draw_kit.h`. **Does not own:** `loadFont` itself +beyond the literals, the palette, any geometry, or the two WCAG `static_assert`s — which are +on pixel height and weight, not on the face, and hold regardless. + +**Behavior.** A platform face list at the five call sites: **DejaVu Sans / DejaVu Sans Mono** +as the Linux defaults, following SWELL's own no-fontconfig fallback list +(LiberationSans/DejaVuSans, LiberationMono/DejaVuSansMono) as precedent. **One code path**, +shared with macOS's eventual San Francisco/Menlo (Λ-D4: made in shared form, not verified). +The subtlety to carry into the work: `draw_kit.cpp:74`'s `if (!hf) return` guard does **not** +catch this failure mode — SWELL's `CreateFont` always returns a non-null handle even when the +face never resolved, recording the failure as a null `typedata` internally. **A wrong or +missing face is not observable at the call site, only in the rendering.** + +**Acceptance criteria.** +- Every kit string renders in the intended face on a stock distro, and the numeric readouts + are tabular. `[verify — Linux]`. +- Windows renders Segoe UI and Consolas exactly as today. +- Row heights, ellipsis points and label truncation are unchanged on Windows; on Linux they + are **measured** rather than assumed correct. + +**Prerequisites.** Λ-W3-T1's fontconfig answer (T2 §5 item 5), which decides whether this is +cosmetic or a readability regression. **Discharges:** L2-09. + +#### Λ-W4-T3 — `source-partition-and-invariants` + +**Goal.** The build's source list stops relying on every TU's own `#ifdef` discipline, and the +invariants Linux weakens are stated where a reviewer will read them. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W4-T3 and §Λ-F3; findings Λ-06, L2-10, L2-11. + +**Surface boundary — owns:** the `target_sources` list in `src/app/CMakeLists.txt:8–51` (**the +list; the property blocks are Λ-W2-T2's**), any new platform-sibling TU the sweep showed was +needed, `src/shell/actions/drag_out_win.h`'s invariant comment, and the corresponding +`src/shell/**/CLAUDE.md` invariant passages. **Does not own:** any behaviour change in a +shipped code path. + +**Behavior.** +- **Λ-06** — partition the source list where the Λ-W3 diagnostic set says a TU needs it. + `drag_out_win.cpp` is the model (a real `#ifdef _WIN32` / `#else` split); + `arrange_drop_win.cpp` and `instrument_drop_win.cpp` are `_win`-suffixed for the **surface** + they serve, not for a platform dependency, and the audits found them portable by inspection + — **confirm against the actual compile rather than re-inspecting.** +- **L2-10 — the copy-only invariant. Λ-F3 rules the wording; this track makes the edit either + way.** Make `drag_out_win.h:7–11` the doc a Linux reviewer is pointed at, and treat "MOVE is + structurally impossible" as a Windows-scoped claim: SWELL's file-list drag takes no effect + mask, so nothing at the API level forbids a target from treating the drag as a move. +- **L2-11 — no Linux action.** If the predicate is touched at all it becomes + "case-insensitive filesystem", not "Windows" — but Λ-D4 puts macOS out, so the right move is + **a comment recording the known macOS defect, not a speculative fix.** + +**Acceptance criteria.** +- No TU compiles on Linux only because of an `#ifdef` that happens to be complete; every + platform-specific TU is either partitioned in CMake or carries a deliberate, commented + guard. +- The copy-only invariant's text says **the same thing** in `drag_out_win.h`, the owning + `CLAUDE.md`, and any spec text that cites it. +- Zero behaviour change on Windows. + +**Prerequisites.** Λ-W3-T1. **Λ-F3 for the L2-10 wording only — non-gating:** the track can +land the partition and leave the sentence for a follow-up. **Discharges:** Λ-06, L2-10, +L2-11's Linux half. + +--- + +### Λ-W5 — Ship the extension + +**Depends on:** Λ-W3-T1 (V4) and **all three Λ-W4 tracks** — shipping means the correctness +fixes are in. **One track. Gated on Λ-F4** (a shipped artifact has to say what it runs on). + +#### Λ-W5-T1 — `linux-packaging-and-install` + +**Goal.** A Linux user can install a correctly-built `reaper_reasampler.so` by following a +document, and a pipeline can install it by following a rule. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W5-T1; finding Λ-08 plus Λ-02's shipped half. + +**Surface boundary — owns:** an `install()` rule in `src/app/CMakeLists.txt` — **the first in +the tree** — `README.md`'s platform-support and install sections, root `CLAUDE.md` +§"Install / reload", and `docs/product/versioning-and-release.md`'s artifact and pipeline +paragraphs. **Does not own:** any source file, the toolchain properties (Λ-W2-T2's), or CI +(Λ-F1). + +**Behavior.** +- Document the Linux `UserPlugins/` root **as V4 actually found it**, and the + single-config-generator output path (`build/reaper_reasampler.so`) that no doc currently + names. +- Add the `install()` rule for **both channels**. The channel fork is platform-independent by + construction — `REASAMPLER_CHANNEL` threads through `configure_file` into names only — so + `reaper_reasampler_beta.so` needs no separate mechanism, only a separate destination check. +- **Amend `versioning-and-release.md`**, which already promises three platform artifacts per + channel per release and mentions no Linux install path, no signing and no CI. Λ-D4 removes + macOS from the near-term promise: that document should say **two** platform artifacts per + channel for now, **with macOS named as deferred rather than silently dropped.** +- **The beta channel on Linux is uncosted in both audits.** Almost certainly free — the fork + is name-only — but "almost certainly" is not a ship criterion. Build and install both + channels side by side once. +- **Λ-F1 decides only which paragraph gets written here** — a pipeline paragraph, or an + explicit "deferred to dev-ops". Until it is ruled, **Λ ships nothing that presumes a + runner.** + +**Acceptance criteria.** +- A clean-machine walkthrough **following only the README**: clone, narrow submodule init, + configure, build, install, restart REAPER, actions present, panel docks. `[verify — Linux]`. +- `reaper_reasampler.so` and `reaper_reasampler_beta.so` coexist in one REAPER with separate + ext-state namespaces, command ids and dock idents. `[verify — Linux]`. +- The **installed** binary shows an explicit `-O` flag in its build log — Λ-02's criterion, + re-checked on the artifact that actually ships. +- `versioning-and-release.md` no longer promises an artifact this phase does not produce. +- The declared support floor (Λ-F4) appears in the README and matches what was built. + +**Prerequisites.** Λ-W3-T1 (V4), Λ-W4 (all three tracks), **Λ-F4 ruled**. **Discharges:** +Λ-08, and Λ-02's shipped half. + +--- + +### Λ-W6 — The instrument module, and the host-safety contract + +**Depends on:** Λ-W2-T2 (thread linkage, visibility, warnings) and Λ-W3-T1 (V10, plus V9 if +Λ-D2 is to survive). **Λ-W6 may run CONCURRENTLY with Λ-W4 and Λ-W5** — it touches +`src/shell/instrument/` and that directory's CMake and `CLAUDE.md` files plus +`src/core/instrument/CLAUDE.md`, none of which Λ-W4 or Λ-W5 opens; the only reason to +serialise is attention, not contention. + +**Two tracks — one build + docs, one source — and they are `T1 before T2 — serial`**, the +same shape this plan already records for Γ-W3. They are file-disjoint but not order-free: +there must be a module before it can refuse to show a view. + +**At the end of this wave the Linux instrument loads in any host, processes audio, and has NO +EDITOR AT ALL** — a defined, verifiable, shippable state, not a half-done one. + +#### Λ-W6-T1 — `vst-linux-module` + +**Goal.** `reasampler_9000.vst3` configures, builds and installs on Linux as the directory +bundle a Linux host expects, and the decision that forbade it is reversed in writing. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W6-T1; audit findings B1, B2, B5. + +**Surface boundary — owns:** `src/shell/instrument/CMakeLists.txt` (the gate, the entry-point +source selection, the bundle POST_BUILD and install rule, `SWELL_PROVIDED_BY_APP`, the modstub +TU) and the three D5 passages in `src/core/instrument/CLAUDE.md`, +`src/shell/instrument/CLAUDE.md` and `src/shell/panel/CLAUDE.md`. **Does not own:** any `.cpp` +or `.h` under `shell/instrument/`. + +**Behavior.** +- **B5 first, as a documentation act.** Rewrite the three D5 passages: the platform clause + becomes "Windows and Linux; the editor is REAPER-hosted"; **VST3-only and REAPER-only + survive verbatim.** `src/core/instrument/CLAUDE.md` carries it twice (Invariants and + Non-goals), `src/shell/instrument/CLAUDE.md` once (Non-goals), and + `src/shell/panel/CLAUDE.md` once (the font/GDI clause). **Nothing else in this wave may land + before this does** — the invariant files are what a future implementer reads. +- **B1** — swap `public.sdk/source/main/dllmain.cpp` for `linuxmain.cpp` on Linux. `dllmain` + includes `` with no `SMTG_OS_*` guard; `linuxmain.cpp` exports `ModuleEntry` and + `ModuleExit`, **both mandatory** — the SDK's own loader refuses the module without either. + Both files are already vendored; this is a source swap plus a platform `if()`. +- **Split the `WIN32 AND EXISTS` conjunction** at `src/shell/instrument/CMakeLists.txt:9`. + Today it is a conjunction, so a Linux configure silently omits `reasampler_vst` **even with + the submodule slice fully initialised.** The `EXISTS` half stays — a fresh clone with no + VST3 slice must still configure — and the `WIN32` half becomes a Windows-or-Linux predicate. +- **B2** — the artifact becomes a **directory bundle**: + `reasampler_9000.vst3/Contents/-linux/reasampler_9000.so`, per V10's answer and + Λ-F4's floor. `Contents/Resources/moduleinfo.json` is **optional** — the SDK's + `getModuleInfoPath` returns empty when absent rather than failing — **so do not author + one.** Install roots: `$HOME/.vst3/`, `/usr/lib/vst3/`, `/usr/local/lib/vst3/`, + `$APPFOLDER/vst3/`. +- **`SWELL_PROVIDED_BY_APP` + the modstub TU** are added to this target in the **default + (non-`SWELL_LOAD_SWELL_DYLIB`) branch** — the same branch the extension already uses + (`src/app/CMakeLists.txt:91–92`). The whole modstub file is inside + `#ifdef SWELL_PROVIDED_BY_APP`, so the VST3 target must define that symbol; today it does + not. **The `dlopen` itself is Λ-W7's**; what this track lands is the compiled-in, inert + table plus the exported `SWELL_dllMain`. +- **The channel fork applies:** `reasampler_9000_beta.vst3` with its own class UID. **The UID + pair is FOREVER-FROZEN and must not change** — a Linux build is a new platform, not a new + identity, and a saved project rebinds by UID. + +**Acceptance criteria.** +- A Linux configure produces the `reasampler_vst` target; a Windows configure is unchanged. +- The built bundle's directory shape matches what the SDK's `module_linux.cpp` opens, and the + SDK's own `validator` loads it. `[verify — Linux]`. +- Both channels build and install side by side, and **the two class UIDs are byte-identical to + the Windows build's.** +- The three `CLAUDE.md` files no longer claim Windows-only, and **still** claim VST3-only and + REAPER-only. +- **No VST3 parameter is registered by this track or any other in this phase** — see the + Γ-W4-T1 boundary above. + +**Prerequisites.** Λ-W2-T2, Λ-W3-T1 (V10; V9 if Λ-D2 is to survive). **Discharges:** B1, B2, +B5; and Λ-10's "vst3sdk is optional on Linux" caveat becomes conditional. + +#### Λ-W6-T2 — `vst-host-safety-contract` + +**Goal.** The plugin's editor surface refuses **cleanly** on every Linux host, so that adding +a real editor later is a change of branch taken and not a change of contract. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W6-T2 and §"The non-REAPER-host safety +criterion"; Λ-D3, L2-08's fallback half. + +**Surface boundary — owns:** `src/shell/instrument/editor_platform.cpp` (the +`isPlatformTypeSupported` / `createView` decision and the non-Windows stubs), +`reasampler_processor.cpp`'s `createView` site, `reasampler_embed.cpp`'s +`REAPER_FXEMBED_WM_IS_SUPPORTED`, and a new availability-probe seam under +`shell/instrument/`. **Does not own:** any CMake file, the paint or input families (untouched +— they stay whole-region-guarded), or any pure `core/instrument/` module. + +**Behavior.** +- The editor's availability becomes a **tri-state probe** — untried / available / unavailable + — computed **once, lazily, off the audio thread**. In this wave the Linux answer is + unconditionally *unavailable*; **Λ-W7 gives it a real computation without changing a single + call site.** +- `isPlatformTypeSupported` returns `kResultFalse` for **every** type when unavailable; + `createView(kEditor)` returns `nullptr`. **Never a view that fails to attach.** +- **One** diagnostic line per process, naming the reason. Never a modal. +- `reasampler_embed`'s `REAPER_FXEMBED_WM_IS_SUPPORTED` continues to return 0 off Windows. + That is correct and stays correct — the TCP/MCP embed strip on Linux is an explicit + non-goal of this phase. +- **The Windows path is not restructured to accommodate this.** The probe is a Linux branch on + an existing decision, **not a new abstraction over both.** + +**Acceptance criteria.** The full observable contract above, verified by **all four** of its +checks: `validator` with no `libSwell.so` present; a real Ardour or Bitwig scan; a +REAPER-on-Linux load; and the renamed-`libSwell.so` negative control. `[verify — Linux]`. +Plus: +- Windows editor behaviour bit-for-bit unchanged — same window class, same `wndProc`, same + `CS_DBLCLKS` fall-through. +- **Nothing added to `process()`** — no `dlopen`, no probe, no branch on the + per-voice-per-sample path. +- **The criteria pass with ZERO VST3 parameters registered.** An empty generic UI is a pass; + Γ-W4-T1 improves it and Λ must not. + +**Prerequisites.** Λ-W6-T1 (there must be a module to load). **Discharges:** Λ-D3's Λ-W6 half; +L2-08's fallback half. + +--- + +### Λ-W7 — The SWELL bootstrap + +**Depends on:** Λ-W6-T1, Λ-W6-T2, and **V9 answered affirmatively.** If V9 is negative, **this +track does not exist and Λ-D2 needs a re-ruling** — which is why V9 runs first in Λ-W3 rather +than being discovered here. + +#### Λ-W7-T1 — `swell-dylib-bootstrap` + +**Goal.** The plugin acquires a working SWELL function table under REAPER on Linux, without +the vendored stub's `exit()` behaviour and without vendoring SWELL. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W7-T1 and §"The implementation shape this +obliges"; Λ-D2 (route B3a), audit finding B3. + +**Surface boundary — owns:** a new `src/shell/instrument/swell_bootstrap.{h,cpp}`, the +availability-probe computation Λ-W6-T2 left stubbed, and the `SWELLAppMain`-shaped callback. +**Does not own:** any vendored file (**nothing under `vendor/` is patched**), the CMake source +list beyond adding one TU, or the editor. + +**Behavior — exactly the four-point shape in the product doc, and it is not a re-litigation of +Λ-D2 but the specification of it.** +1. **Never define `SWELL_LOAD_SWELL_DYLIB`.** Compile `swell-modstub-generic.cpp` in its + default branch, which exports `SWELL_dllMain(hInst, callMode, GetFunc)` and calls `doinit` + on the pointer it is handed. +2. **Own the load.** Resolve the host executable's directory, `dlopen` `libSwell.so`, `dlsym` + `SWELLAPI_GetFunc`, `dlsym` and call `SWELL_set_app_main`, then call our own exported + `SWELL_dllMain(hinst, DLL_PROCESS_ATTACH, getfunc)` — **using only exported surface, with + every failure returning a recorded state instead of exiting.** This also dissolves the + `SWELLAppMain`-as-a-link-requirement noted in T1 B3a: we pass an app-main because we choose + to, not because the linker demands one. `[verify — Linux]` whether a minimal app-main + suffices for a plugin that only ever creates child windows inside a host-supplied X11 + window. +3. **Probe the host, not just the library.** A non-REAPER host has no SWELL message loop — + REAPER's own is what pumps SWELL windows — so even a successful `dlopen` in Ardour would + produce a window nothing drives. **Gate on the host name via `IHostApplication::getName()` + AND on the `dlopen` succeeding**; either failing means no editor. +4. **Probe once, lazily, off the audio thread.** Tri-state, computed on first `createView` and + never recomputed. No `dlopen` from `process()`, no per-`createView` retry, **no + static-constructor work.** + +**The failure taxonomy the probe must distinguish**, because they need different messages: +host is not REAPER / executable path unresolvable / `libSwell.so` not present at the resolved +path / `SWELLAPI_GetFunc` missing or version-mismatched (the stub checks +`SWELLAPI_GetFunc(NULL)==(void*)0x100`) / API table incomplete. **The last is not fatal** — +`doinit` substitutes a zero-returning `dummyFunc` per miss — and whether to accept a partial +table or refuse is **[propose at review]**, with *"refuse if any name the editor actually +calls is missing"* as the starting proposal. + +**Acceptance criteria.** +- Under REAPER on Linux the probe reports *available* and the SWELL table resolves with **zero + misses**. `[verify — Linux]`. +- Under `validator` with **no `libSwell.so` anywhere**, the probe reports *unavailable*, the + process **exits normally**, and one diagnostic line is printed. **This is the direct + negation of the `exit(2)` finding and is the track's headline criterion.** + `[verify — Linux]`. +- Under Ardour or Bitwig **with** a `libSwell.so` reachable, the host-name gate **still + refuses** — proving the gate is on the host, not only on the library. `[verify — Linux]`. +- No vendored file is modified; `git status` under `vendor/` is clean. +- Windows build unaffected — the whole TU is behind a platform guard. + +**Prerequisites.** Λ-W6-T1, Λ-W6-T2, V9 affirmative. **Discharges:** B3 via route B3a; Λ-D3's +runtime half. + +--- + +### Λ-W8 — The X11 editor + +**Depends on:** Λ-W7-T1. **One track, and it is the phase's only L-band source item** (L2-08; +T1 B4 costs it "build S, source L"). Λ-D6 sequences it last on purpose: **halting this wave +still leaves a shipped extension and a loading, processing, generic-UI instrument** — a real +product, not a stub. + +#### Λ-W8-T1 — `x11-embed-view` + +**Goal.** The ReaSampler 9000 editor opens, draws and responds under REAPER on Linux. + +**Spec:** `docs/product/linux-readiness.md` §Λ-W8-T1; audit findings B4, L2-08. + +**Surface boundary — owns:** a **Linux sibling** to `src/shell/instrument/editor_platform.cpp` +(window creation and parenting, the run-loop timer, the event-driven input path), and the +`#ifdef _WIN32` region **boundaries** in `reasampler_editor.h` and the eight +`editor_input_*` / `editor_paint*` TUs — **boundaries only, not their contents.** +**Does not own:** `draw_kit`, any pure `core/instrument/` module, any painter's drawing logic, +`instrument_bake`, or the processor. + +**Behavior.** The audits call this **a new competence rather than a port.** The build-side cost +is nil — the Linux IIDs are already in the vendored slice (`commoniids.cpp` defines +`Linux::IEventHandler`, `Linux::ITimerHandler`, `Linux::IRunLoop` under `#if SMTG_OS_LINUX`, +and that file is already in the `vst3_sdk` source list). What must be written: +- `kPlatformTypeX11EmbedWindowID` instead of `kPlatformTypeHWND`; `attachedToParent` receiving + an X11 window id. +- A `Linux::IRunLoop`-driven timer replacing `SetTimer`/`WM_TIMER` — **including the editor's + sync tick, which the bake's arm-then-run discipline rides on.** +- An **event-driven input path** replacing the `wndProc` switch. `RegisterClassW` / + `CreateWindowExW` / `DefWindowProcW` have **no SWELL analogue at all** — SWELL has no + window-class model, only `SWELL_CreateDialog` and raw `HWND__` construction. +- Substitutions the audit already resolved by name: `MoveWindow` → `SetWindowPos`; + `GetWindowLongPtr`/`SetWindowLongPtr` → the non-`Ptr` forms returning `LONG_PTR`; + `GetKeyState` → `GetAsyncKeyState`; `TrackMouseEvent`/`WM_MOUSELEAVE` → **nothing**, so + hover-leave needs its own derivation (the panel layer already documents the same gap); + `DragAcceptFiles`/`DragQueryFileW` → the drop path Λ-W2-T3 settled for the panel. + +**A worked reference implementation is already on disk and neither audit named it.** +`vendor/vst3sdk/public.sdk/samples/vst-hosting/editorhost/source/platform/linux/` contains +`window.cpp` (returning `{kPlatformTypeX11EmbedWindowID, …}` and answering +`Linux::IRunLoop::iid` from `queryInterface`), `runloop.cpp`, and `irunloopimpl.h` (a +`RunLoopImpl` implementing `registerEventHandler` / `registerTimer` and their unregisters). It +is the **host** side of the contract rather than the plug-in side, which makes it a precise +specification of what our plug-in side must satisfy, and the documented narrow submodule init +already pulls it. **This does not shrink the L band** — it is a reading input, not a library; +it removes the "we are guessing at the contract" risk from the estimate. + +**Acceptance criteria.** +- The editor opens, draws every face identically to the Windows build (compare screenshots at + the same window size), and every drag, click, wheel and keyboard interaction behaves the + same. `[verify — Linux]`. +- Hover-leave is correct on every hover surface, without `TrackMouseEvent`. +- **The bake's arm-then-run tick fires under `IRunLoop`, and a bake completes end-to-end on + Linux:** staged file, extension action invoked over the VST3 host bridge, outcome read back, + adopt and reset. `[verify — Linux]`. +- **Λ-W6-T2's contract still holds** — a non-REAPER host still gets `nullptr` from + `createView`. **Re-run all four checks; this is a regression criterion, not a new one.** +- **Nothing added to `process()`**; no dispatch added to any per-sample path. +- Every new file lands under the ~600-line ceiling **with a responsibility seam, not a + bisection.** The editor's existing band-axis split (`_chrome` / `_waveform` / `_deck` / + `_browse` / `_curve`) is the seam vocabulary to reuse — the Linux platform TU is a **sibling + to `editor_platform`**, per L2-08's own direction: *"it needs a sibling, not a rewrite."* +- Windows editor behaviour bit-for-bit unchanged. + +**Prerequisites.** Λ-W7-T1. **Discharges:** B4, L2-08. + +--- + +### Shared files across Λ's waves, named rather than discovered at merge + +All textual adjacency, not semantic contention, unless marked otherwise. + +| File | Tracks | Nature | +|---|---|---| +| `src/app/CMakeLists.txt` | Λ-W2-T2 (property + platform blocks), Λ-W2-T3 (one `target_sources` line, **resgen route only**), Λ-W4-T3 (the source list), Λ-W5-T1 (the `install()` rule) | Four disjoint regions of one file. Λ-W2-T2 and Λ-W2-T3 are the only pair in the same wave; one line each | +| `src/shell/panel/draw_kit.cpp` | Λ-W2-T1 (`loadFont`'s `CreateFont` args, `:73`), Λ-W4-T2 (the five call sites, `:154–158`) | Different waves | +| `src/shell/instrument/CMakeLists.txt` | Λ-W2-T2 (thread linkage), Λ-W6-T1 (gate, entry point, bundle, install), Λ-W7-T1 (one added TU) | Different waves | +| `src/shell/panel/panel_window.cpp` | Λ-W2-T3 alone | **Deliberately not split.** The L2-06 diagnostic and the Λ-01 resource route are the same function — under the id-0 route the same *line*. Splitting them would be semantic contention | +| `src/shell/instrument/editor_platform.cpp` | Λ-W6-T2 (the refusal branch), Λ-W8-T1 (the real branch) | Different waves; the second replaces the first's computation **without touching its call sites** | + +--- + ## Traceability — all seventeen items The check that nothing was dropped. Every row points at a track that exists above. @@ -3267,6 +4192,17 @@ proof it exists to give. 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. +- **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. ### Deliberate compressions @@ -3441,4 +4377,45 @@ Phase Rho — Render in place (none of the seventeen; a direct reque 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 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. ```