Files
reasampler/docs/product/linux-readiness.md
T
daniel b584fa4da9 docs: anchor the product-doc citations to symbols, not line numbers
Ten more were already pointing at unrelated code. Vendor SDK and SWELL citations left alone — those files don't move.
2026-08-02 20:46:28 -04:00

1306 lines
79 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.
# Linux readiness — product notes
Framing, rationale, and design-direction calls behind **Phase Λ — ReaSampler on Linux:
the `reaper_reasampler` extension and the ReaSampler 9000 VST3 instrument, both
shipped.** The tickable spec belongs in `docs/PLAN.md` (§Phase Λ); the architecture
detail belongs in the affected `src/**/CLAUDE.md` files once tracks land. This doc holds
the *why* — what "Linux-ready" is defined to mean here, what the tree's actual posture
is, the six decisions Daniel settled on 2026-08-02, the sequencing argument, and the four
forks still open.
**This doc is downstream of two audits and does not repeat them.**
`docs/product/audit-notes/lambda-w1-t1-build-toolchain.md` (findings Λ-01…Λ-10, verify
items V1…V10) owns the build system, toolchain, vendored dependencies, resources, test
harness and packaging. `docs/product/audit-notes/lambda-w1-t2-source-runtime.md` (findings
L2-01…L2-12) owns source portability and runtime behaviour. Both were code-reviewed against
the tree and remediated in place. **Everything below cites them by finding number rather
than re-deriving.** Grep the audits for a cited number; do not read either whole.
Status: framed by product-designer (2026-08-02) after Daniel's rulings the same day.
**Four forks are open** — Λ-F1 (CI), Λ-F2 (the dialog-resource route), Λ-F3 (the
copy-only drag-out invariant), Λ-F4 (the declared support floor) — each stated with the
evidence for both sides in §"Open forks". Λ-F2 gates one track and nothing else;
the other three gate nothing. Everything not marked as a fork is a product-designer call
with its reasoning stated; contradict it in review with an argument, not a preference.
**Slug convention.** `Λ → l`, so tracks dispatch into `pl-w<wave>-t<track>-<slug>`
Λ-W2-T1 into `pl-w2-t1-linux-compile-blockers`. The two audits already ran under
`pl-w1-t1-build-toolchain-audit` and `pl-w1-t2-source-runtime-audit`. **`docs/PLAN.md`'s
"Worktree slug convention" paragraph
lists the transliterations for Θ / Ξ / Γ / Ψ / Ε / Ρ and does not yet carry Λ — adding it
is a plan-doc edit this phase's PLAN.md entry must make.**
---
## Why now, and what "Linux-ready" means here
The reason to do this now is not demand — it is that **the cost is already sunk and the
window for it to stay sunk is closing.** Two facts from the audits:
- `docs/product/versioning-and-release.md` already commits, in writing, to
"three platform artifacts per channel per release" (T1 §1e). Linux is not a new promise;
it is an unredeemed one.
- The panel layer — the product's primary surface — was written against SWELL's
vocabulary throughout and is portable **call-for-call**, verified name-by-name against
the vendored headers (T2 §1.5, §"Surfaces checked"). That is an asset the tree acquired
by discipline, not by luck, and it decays every time someone writes a Win32 call without
a `#else`. Phase Ψ, Phase Ε and Phase Ρ are all adding extension-side surface right now.
Compiling on Linux once converts that discipline from a convention nobody can check into
a build that fails when it is broken.
**What "Linux-ready" is defined to mean in this phase.** A Linux REAPER user installs
`reaper_reasampler.so` and `reasampler_9000.vst3` and gets the same product a Windows user
gets, with four named exceptions:
1. **Prune deletes permanently.** No Recycle Bin, no trash. The confirmation says so.
(Λ-D5.)
2. **The OS drag-out's copy-only guarantee is conventional, not structural.** 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-10). Λ-F3 decides how the invariant is worded.
3. **Fonts are not Segoe UI and Consolas.** The kit gets an explicit Linux face list
(see §"Calls made here"); the faces are different, the layout is not.
4. **The ReaSampler 9000 editor comes up under REAPER only.** In any other Linux host the
plugin loads, scans, instantiates, and processes audio — and shows the host's generic
parameter UI instead of its own editor. This is a *contract*, not a shortfall; §"The
non-REAPER-host safety criterion" specifies it as an observable.
**What a Linux user explicitly does not get:** an XDG-trash prune, a macOS build, the
TCP/MCP embed strip, or an editor in Bitwig/Ardour. §"What Phase Λ does not do" states
each with its reason.
---
## The posture as found
Compressed from the two audits. Each claim below is theirs; the citations are the ones
this doc re-read against the tree while writing.
**The extension is close — two one-line compile blockers stand between the tree and a
GCC/Clang build.**
- `src/shell/panel/draw_kit.cpp`'s `loadFont` passes `DEFAULT_PITCH | FF_DONTCARE` to `CreateFont`.
`FF_DONTCARE` has zero occurrences anywhere in `vendor/WDL/` (L2-01); the file is not
platform-guarded, only its include is (`loadFont`'s whole body). `draw_kit` links into both modules, so
nothing builds.
- `src/shell/actions/instrument_drop_win.cpp`'s `writeTempPreset` calls `GetCurrentProcessId()`
with no platform branch anywhere in the TU. SWELL exports
`GetCurrentThreadId` and not this (L2-02). The PID exists only to keep two concurrent
REAPER instances from colliding in the shared temp dir; the atomic counter in the same function
already carries the intra-process half.
**`core/` is genuinely pure, and it was verified rather than assumed.** Every `#include`
under `src/core/**` is a `core/` sibling, one of 26 standard headers, or the generated
`version_generated.h` — zero REAPER, SWELL, WDL, LICE, VST3 or `windows.h` (T2 §1.1). The
whole directory contains nine preprocessor conditional lines, exactly one of which is a
platform fork, and that one (`capture_paths.cpp`'s `normalizeSlashes`, the Windows case-fold) is *correct*
for Linux with both branches already asserted by `tests/test_capture_paths.cpp`. All 91
test TUs under `tests/` are platform-neutral.
**The panel layer is already SWELL-portable, call for call.** Dialog lifecycle, docking,
double-buffered LICE `WM_PAINT`, mouse/wheel/capture, `WM_CAPTURECHANGED` rollback, all
seven cursors, menus, the keyboard accelerator path, modifier reads, tooltips, drag-out and
`DragQueryFile`/`DragFinish` were each checked by name against `swell-functions.h` /
`swell-types.h` and are present (T2 §1.5). The Windows-only escapes are three:
`DragAcceptFiles` (`panel_window.cpp`'s `openPanel`, under `#ifdef _WIN32`), `SHFileOperationW`
(prune), and OLE `DoDragDrop` (drag-out) — each already carrying a non-Windows branch or a
documented reason it does not. **This is the audits' single most load-bearing finding.**
**The dock panel will not appear until the dialog-resource question is answered.**
`panel_window.cpp`'s `openPanel` calls `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`'s `else()` (Linux) branch has the `target_sources`
line commented out (and the `elseif(APPLE)` branch's own copy for macOS). The registry head stays null, `SWELL_CreateDialog`
returns null, `openPanel`'s `if (!g_panel.hwnd) return;` guard returns, and the toggle action is a silent no-op with
no console line and no Actions-list checkmark (Λ-01, L2-06). Three defects stack in the
commented-out instructions themselves: the script named in the `elseif(APPLE)`/`else()` branches' comment lines (`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 (Λ-01). Λ-F2 decides the route.
**The instrument's Linux editor is a from-scratch X11 job, and today the target does not
configure at all.** `src/shell/instrument/CMakeLists.txt`'s
`if(WIN32 AND EXISTS "${VST3_SDK}/…/pluginfactory.cpp")` gate is
a conjunction, so a Linux
configure silently omits `reasampler_vst` even with the submodule slice fully initialised.
Beyond the gate: the wrong module entry point is compiled, the artifact is a file where
Linux wants a directory bundle, nothing hands a VST3 plugin the SWELL function table, and
the view reports `kPlatformTypeHWND` only (B1B4, L2-08). The paint side is the cheapest
part — every painter already draws into a `LICE_IBitmap` through the shared kit, so the
drawing survives a window-system change intact. It is the window and event plumbing that
is entirely absent.
**Nothing about the build is optimized, and the documented ship command is a no-op on
Linux.** Root `CMakeLists.txt`'s `set(CMAKE_CXX_STANDARD ...)`/`set(CMAKE_CXX_STANDARD_REQUIRED ...)`/`set(CMAKE_POSITION_INDEPENDENT_CODE ...)` block is the complete list of language settings — there is
no `CMAKE_BUILD_TYPE`, no `CMAKE_CXX_FLAGS`, no IPO/LTO, and no `target_compile_options`
anywhere in the tree. `--config Release` is accepted and ignored by Ninja and Make, so the
README's ship incantation produces a binary with no `-O` flag at all, on a tree whose
`peaks` hot path is documented as presuming an optimizing build (Λ-02). There is also no
symbol-visibility control (Λ-03), no thread library linked (Λ-04), no warning flags
(Λ-09), no `install()` rule anywhere, and no CI configuration in the repo on any platform
(T1 §1e).
---
## Settled decisions (Daniel, 2026-08-02)
Six rulings. They are settled; do not re-litigate them. Each is recorded with the reasoning
given, because the reasoning is what a later reader will need.
### Λ-D1 — The VST3 instrument is in scope. (Audit fork D1 = **Fork B**.)
Phase Λ ships both artifacts on Linux, not just the extension.
**Why.** The instrument is not a standalone product on any platform. The capture browser
reads the extension's bank over the bridge, and the resample bake invokes an extension
action synchronously over the VST3 host bridge — root `CLAUDE.md` §"The resample bake"
makes the crossing structural. **A non-REAPER Linux host was never getting the full
instrument regardless**, because the two features that make it ReaSampler rather than a
generic sampler both tie into the extension directly. So the marginal thing Λ-D1 buys is
precisely a REAPER-hosted Linux instrument — which is the only complete one that exists on
any platform.
**What it costs.** T1 §4 states the asymmetry plainly: Fork A's build work is small and its
unknown is source portability; Fork B's build work is genuinely large *and* carries an
unbounded source tail *and* requires reversing a recorded decision. They are not "the same
phase, one bigger" — they are different-shaped commitments, and this doc's wave structure
keeps them separable (Λ-W6…Λ-W8 can be halted without unshipping Λ-W2…Λ-W5).
**Consequence — D5 is reversed, in writing.** "No cross-platform… Windows-only, VST3-only,
REAPER-only (D5)" is a recorded invariant in three files: `src/core/instrument/CLAUDE.md`
(twice — Invariants and Non-goals), `src/shell/instrument/CLAUDE.md` (Non-goals), and
`src/shell/panel/CLAUDE.md` ("Windows-only (D5) — font/GDI/HFONT choices assume Windows; no
cross-platform font-fallback concern"). D5's *other* two clauses survive untouched: still
VST3-only, still REAPER-only. **Only the platform clause moves**, and it moves to
"Windows and Linux; the editor is REAPER-hosted." Rewriting those three passages is
audit finding B5 and is a named deliverable of Λ-W6-T1.
### Λ-D2 — The instrument reaches SWELL by `dlopen`ing REAPER's own `libSwell.so`. (D2 = **route B3a**.)
`vendor/WDL/WDL/swell/swell-modstub-generic.cpp:69131` is the vendored implementation:
resolve the host executable's directory from `/proc/self/exe` (falling back to `dladdr`),
`dlopen` `libSwell.so` beside it, `dlsym` `SWELLAPI_GetFunc`, call `SWELL_set_app_main`,
populate the API table.
**Route B3b is rejected.** Building SWELL into the module means the 14 SWELL TUs plus 8
LICE TUs from SWELL's own Makefile and a wholly new third-party dependency
surface — GDK/GTK3, FreeType, Fontconfig, OpenGL — that the extension deliberately avoids
via `SWELL_PROVIDED_BY_APP`. That is a scope expansion in a phase whose entire cost
advantage comes from not having one.
**The consequence Daniel accepted:** B3a makes the instrument's UI load-bearing on being
hosted by REAPER specifically. That is consistent with D5's surviving REAPER-only clause
and with Λ-D1's reasoning above. **It is also why Λ-D3 exists** — the coupling has to be
safe, not merely intended.
**Important: the vendored stub cannot be used as-is.** See §"The non-REAPER-host safety
criterion" — `swell-modstub-generic.cpp:102` calls `exit(2)` on a failed `dlopen`, from a
static constructor. Route B3a is the ruled route; *how* it is implemented is specified
there and is not a re-litigation of this ruling.
### Λ-D3 — Non-REAPER hosts must be protected. First-class acceptance criterion.
Daniel, verbatim: *"REAPER-only is good for now, but we need to protext other hosts."*
A working editor on Linux is required under REAPER only. But the plugin must **degrade
safely** in Bitwig, Ardour, or any other Linux host: no crash, no host takedown, defined
behaviour when `libSwell.so` is absent or unusable. The audits flagged the absent-`libSwell`
failure mode as unverified; this phase must both determine it and hard-guard it. It is
specified as an observable contract in its own section below, and it is an acceptance
criterion of two tracks, not a footnote on one.
### Λ-D4 — macOS is out. (D3 = **out**.)
Daniel, verbatim: *"I don't have a mac to compile on."*
Λ-01, Λ-03, Λ-04, Λ-07, Λ-09 and L2-09 are all marked *shared macOS/Linux* in the audits,
meaning the same edit covers both. **Those edits still get made in their shared form and
noted as shared** — a `#ifdef _WIN32` / `#else` that is right for both costs nothing extra
and does not require a mac. What is out is: macOS as a phase deliverable, any macOS
verification, the `swell-modstub.mm`-under-a-CXX-only-`project()` question (root
`CMakeLists.txt`'s `project(...)` call is `LANGUAGES CXX`), signing and notarization, and the macOS-only
half of L2-11 (`normalizeSlashes` under-folds on case-insensitive APFS — a real pre-existing
defect this phase surfaces and does not own).
**Do not "fix macOS while you're in there."** An unverifiable edit to the APPLE branch is
worse than no edit, because it looks tested.
### Λ-D5 — The Linux artifact is shipped, not developer-only. (D6 = **shipped**.) Hard `unlink` is acceptable. (T2 [Daniel] 2 = **yes**.)
Two rulings that travel together, because both are about what "shipped" obliges.
**Shipped** means Λ-02 (an actual Release/optimized build) and Λ-08 (a documented install
path and artifact name) are **must-fix, not nice-to-have**, and that a Linux `install()`
rule is worth authoring. A developer-only artifact could have skipped all of it.
**Hard `unlink` is acceptable**, with a **platform-aware confirmation string**. XDG trash
is not a ship blocker. The user must be told, at the confirm, that the deletion is
permanent on this platform. Note that L2-05 has a **second half the ruling does not
cover**: on Windows a bank file held open by REAPER's audio engine cannot be deleted and is
counted as "skipped"; on Linux `unlink` on an open file succeeds, the audio keeps playing
from the open fd, and the bytes are gone when it closes. That is not a trash question — it
is the disappearance of the recovery floor root `CLAUDE.md` §"The resample bake" relies on
("the superseded file survives on disk until a prune reclaims it"). Λ-W4-T1 owns it.
### Λ-D6 — Fork B's editor work is real work, and it is sequenced last.
Not a separate Daniel ruling but the direct consequence of Λ-D1 + Λ-D2 + Λ-D3: the X11
editor is the phase's only L-band source item (L2-08; T1 B4 costs it "build S, source L")
and it is the last thing sequenced. Everything before it ships something. If Λ-W8 is
halted, Linux users still have a shipped extension (Λ-W5) and a loading, processing,
generic-UI instrument (Λ-W6) — which is a real product, not a stub.
---
## The non-REAPER-host safety criterion
Λ-D3 stated as something a person can check. This section is the specification the two
instrument waves are graded against.
### The finding that makes this urgent
The audits flagged the absent-`libSwell.so` failure mode as unverified. **It is verifiable
from the vendored source, and the answer is bad.** Reading
`vendor/WDL/WDL/swell/swell-modstub-generic.cpp`:
- `:125` declares `SwellAPPInitializer m_swell_appAPIinit;` — a **file-scope static
object**, so its constructor runs when our `.so` is `dlopen`ed, i.e. **during the host's
plugin scan**, before any VST3 code of ours executes.
- `:98103``dlopen(fn, RTLD_LAZY)`; if it returns null, the stub prints to stdout and
calls **`exit(2)`**.
- `:117` — if `doinit` reports any unresolved SWELL name, **`exit(1)`**.
So compiling the vendored `SWELL_LOAD_SWELL_DYLIB` path unmodified means: **a Bitwig or
Ardour plugin scan on a machine with no `libSwell.so` beside the host binary terminates the
host process.** Not a failed scan — a killed DAW, mid-scan, with the user's session. That is
precisely the host takedown Λ-D3 forbids, and it is a certainty rather than a risk.
The mitigating detail, worth knowing because it shapes the fix: `doinit` (`:5367`) does
**not** leave unresolved entries null. It substitutes `&dummyFunc` (`:63`), and `dummyFunc`
(`:51`) is `static int dummyFunc() { return 0; }`. A *partial* SWELL therefore degrades to
"every missing call returns 0/NULL" rather than jumping through a null pointer. A partial
load is survivable; `exit()` is not.
### The implementation shape this obliges
Route B3a (Λ-D2) is the ruled route. It must be reached without the stub's own loader:
1. **Never define `SWELL_LOAD_SWELL_DYLIB`.** Compile
`swell-modstub-generic.cpp` in its default branch — the same branch the extension already
uses (`src/app/CMakeLists.txt`'s `else()` (Linux) branch's `target_sources`/`target_compile_definitions` pair) — which exports `SWELL_dllMain(hInst, callMode,
GetFunc)` (`:135`) and calls `doinit` on the pointer it is handed. The whole file is
inside `#ifdef SWELL_PROVIDED_BY_APP` (`:21`), so the VST3 target must define that
symbol too; today it does not.
2. **Own the load.** A new `shell/instrument/swell_bootstrap` TU does the work the stub
would have done — 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. No vendored
file is patched. 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 is sufficient 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 the bootstrap on the host name via
`IHostApplication::getName()` **and** on the `dlopen` succeeding; either failing means no
editor. Cheap, and it makes the REAPER-only coupling explicit rather than emergent.
4. **Probe once, lazily, off the audio thread.** The result is a tri-state — untried /
available / unavailable — computed on first `createView` and never recomputed. No
`dlopen` from `process()`, no per-`createView` retry, no static-constructor work.
### The observable contract
In **any** Linux host, with or without `libSwell.so`:
- **Scan.** The host opens `reasampler_9000.vst3`, finds `ModuleEntry` / `ModuleExit` /
`GetPluginFactory`, and enumerates the one class. No crash, no hang, no process exit, no
blacklist entry.
- **Instantiate and run.** `setActive(true)` and `process()` produce audio (or silence with
nothing loaded). MIDI notes play. The component state round-trips through
`setState`/`getState` byte-identically with the Windows build.
- **Editor request.** When no usable platform surface exists, `isPlatformTypeSupported`
returns `kResultFalse` for **every** type — including `kPlatformTypeX11EmbedWindowID`
and `createView(kEditor)` returns **`nullptr`**. It never returns a view whose
`attached()` then fails, and never returns a view that draws nothing. The host falls back
to its generic parameter UI.
- **Diagnosis.** Exactly one line, once per process, naming why the editor is unavailable
(host not REAPER / `libSwell.so` not found at `<path>` / SWELL API incomplete). Written
through the SDK's own logging or stderr — never a modal, never a message box, never
repeated per instantiation.
- **Teardown.** Unloading the module does not crash. Nothing calls `exit()`, `abort()`, or
leaves a partially-initialised SWELL table reachable by a later call.
Under REAPER on Linux the same contract holds with the editor branch taken instead: the
view reports `kPlatformTypeX11EmbedWindowID`, attaches to the host-supplied window id, and
drives its timer through `Linux::IRunLoop`.
### How it is verified
**The VST3 SDK ships a non-REAPER host harness and it is already on disk.**
`vendor/vst3sdk/public.sdk/samples/vst-hosting/validator/source/validator.cpp` is present
in the vendored slice (it lives under `public.sdk/`, which the documented narrow submodule
init already pulls). It is a scriptable, headless, REAPER-free host that instantiates a
plugin and exercises the standard suite. **It is the right instrument for this criterion**
and neither audit named it, because neither swept the SDK's `samples/` tree.
The verification set, all `[verify — Linux]`:
- `validator reasampler_9000.vst3` completes and reports no failures, on a machine with
**no** `libSwell.so` on the filesystem. This is the load-bearing check: the validator
process surviving is the direct negation of the `exit(2)` finding.
- The same, in **Ardour or Bitwig**, through a real plugin scan: the DAW finishes scanning,
the plugin appears in the browser, an instance instantiates, and the generic parameter UI
opens. A hand check, but the one that answers Daniel's actual sentence.
- The same, under **REAPER on Linux**: the editor opens.
- A **negative-control** run: rename `libSwell.so` beside a Linux REAPER, load the plugin,
and confirm the generic-UI fallback and the single diagnostic line — not a crash, not a
blank window.
**The acceptance criteria above must pass with zero VST3 parameters registered.** See
§"Cross-phase dependency: Γ-W4-T1" — today the generic UI is empty, and "the host shows an
empty generic UI" is a pass. It is a poor experience, and Γ fixes it; it is not Λ's to fix.
---
## Wave and track structure
### Why this sequence
Three constraints shape it, and they conflict.
**One: the `[verify — Linux]` items are worthless as a late checklist and near-free as an
early wave.** T1's own §6 Stage 0 says it outright — running V1 and V2 on a Linux box is
"the one action that reprices everything", and every effort band in both audits is
provisional until it happens. A plan that puts verification at the end has, in effect, no
estimates at all.
**Two: nothing can be verified from this Windows box.** Every acceptance criterion for the
first Linux edits is itself a `[verify — Linux]` item. This is not a fixable property of
the plan; it is the phase. The honest structure is to say so: **Λ-W2's edits are authored
blind, from the audits' citations, and Λ-W3 is the session that discharges Λ-W2's
acceptance criteria and answers the sweep in the same sitting.** Λ-W2 is not "done" in the
usual sense until Λ-W3 runs. Pretending otherwise would launder unverified work into
landed work, which is exactly what the audits' `[verify]` discipline exists to prevent.
**Three: the sweep cannot answer the panel-dependent items until the panel renders.**
T2 §6 step 4 flags this and notes the critical path runs through the other track. The
resolution is to pull the dialog-resource work forward into Λ-W2 — which is why Λ-F2 is
worth ruling early, and why the plan states what happens if it is not (Λ-W2-T3 slips and
Λ-W3 splits).
Two further sequencing decisions, both deliberate:
- **The locale codec (L2-04) is pulled forward into Λ-W2**, 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, and there
is no reason to spend a Linux session discovering that.
- **V9 (does REAPER's Linux install ship `libSwell.so` beside its executable?) runs in the
sweep, not in the instrument waves.** It is a one-line `ls`, and the entire Λ-D2 route
rests on it. If it is false, B3a is void and the instrument's editor needs a re-ruling —
a thing to learn in Λ-W3, not four waves later.
**On the audits' severity disagreement.** 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 the
inconsistency as a scope call for whoever prioritises the two. This plan **dissolves it
rather than resolving it**: both land in Λ-W2, in different tracks, so the relative grade
never has to be adjudicated.
### Λ-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 here so the wave numbering matches the
audit filenames and branch names; nothing to re-spec.
---
### Λ-W2 — Make it buildable, and make it honest
**Four tracks, disjoint at the file level.** Three of them are authored blind against the
audits and verified in Λ-W3; T4 is verifiable on the current box.
#### Λ-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.
**Surface boundary — owns:** `src/shell/panel/draw_kit.cpp` (`loadFont`),
`src/shell/actions/instrument_drop_win.cpp` (`writeTempPreset`), `src/app/main.cpp`
(the `REAPERAPI_LoadAPI` failure branch only). **Does not own:** any `CMakeLists.txt`,
`panel_window.cpp`, or any `core/` file.
**Behavior.**
- Drop the `| FF_DONTCARE` 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.
- Replace `GetCurrentProcessId()` with a platform-neutral uniqueness source behind a guard;
the atomic counter in `writeTempPreset` already carries the intra-process half (L2-02).
- On the load-failure branch, either switch `main.cpp` to `REAPERAPI_MINIMAL` plus an
explicit `WANT` list — the pattern `panel_window.cpp` and `panel_audition.cpp` already
use — or keep the full load and print the failure count via
`rec->GetFunc("ShowConsoleMsg")` before returning 0 (L2-03). The minimal list is also the
honest inventory of what this extension actually needs.
**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 targets, no `-C` flag needed.
`[verify — Linux]` = **V2**.
- A deliberately misspelled entry in the WANT list (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.
**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** — default `CMAKE_BUILD_TYPE` when neither it nor `CMAKE_CONFIGURATION_TYPES` is
set, and correct the docs' ship instruction to name the single-config-generator path. The
README currently sends a Linux user to `build/Release/`, which on Ninja/Make does not
exist; 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`; the two 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`,
and `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 the tree is not estimable from
Windows. Separately, `-Wl,--no-undefined` on the module targets, to restore 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. It is invisible
on Windows and load-bearing off it.
- **Λ-07** — record the compiler floor, 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. Runs concurrently with T1, T3, T4.
**Discharges:** Λ-02, Λ-03, Λ-04, Λ-05, Λ-07, Λ-09, Λ-10; enables V5, V6, V8.
#### Λ-W2-T3 — `panel-dialog-resource` *(gated on Λ-F2)*
**Goal.** The docked bank panel opens on Linux, and if it ever fails to, it says so.
**Surface boundary — owns:** `src/shell/panel/panel_window.cpp` (the `CreateDialogParam`
call and its `if (!g_panel.hwnd) return;` guard, both in `openPanel`, the dialog proc's platform contract, the drop-accept opt-in
also in `openPanel`), `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.
**Behavior.** Whichever route Λ-F2 picks, plus, unconditionally: 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** — land it on both platforms, not behind a guard. 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** — see the fork.
- 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 the process locale, so a
bank index written on a comma-decimal machine is not written unparseable.
**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. Add pure tests that pin the emitted text for a fractional value, and that a
fractional value survives a write/read round trip.
Why this 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 (L2-04). 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` in
the test itself. 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; 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 this change 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
**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.
**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 false answer voids Λ-D2's
route and needs a re-ruling before Λ-W6 is dispatched), 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, and any font check that needs a rendered
panel) are deferred 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
**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.
**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), 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` on
an open file succeeds and the branch never fires. Either the branch 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` relies on
("the superseded file survives on disk until a prune reclaims it") has nothing under it
on Linux otherwise.
- **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, and it is not covered by Λ-D5's ruling.
**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.
**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.
**Surface boundary — owns:** `src/shell/panel/draw_kit.cpp`'s five `loadFont` call sites 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 worth carrying into the work: `draw_kit.cpp`'s `loadFont`'s `if (!hf) return` guard does
**not** catch the failure mode here. SWELL's `CreateFont` always returns a non-null handle
even when the face never resolved — the failure is recorded internally as a null
`typedata`, not as a null return. So 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.
**Surface boundary — owns:** the `add_library(reaper_reasampler MODULE ...)` source list in `src/app/CMakeLists.txt`
(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** — make `drag_out_win.h`'s file-header invariant comment the doc a Linux reviewer is pointed at, and treat
"MOVE is structurally impossible" as a Windows-scoped claim. **The wording is Λ-F3's
ruling**; the edit is this track's regardless of which way it goes.
- **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
#### Λ-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.
**Surface boundary — owns:** an `install()` rule in `src/app/CMakeLists.txt`, `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 first `install()` rule in the tree, 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.** It is 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.
**Acceptance criteria.**
- A clean-machine walkthrough: clone, narrow submodule init, configure, build, install,
restart REAPER, actions present, panel docks. Following only the README.
`[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 — the Λ-02 criterion,
re-checked on the artifact that actually ships.
- `versioning-and-release.md` no longer promises an artifact this phase does not produce.
**Prerequisites.** Λ-W3-T1 (V4), Λ-W4 (all three tracks — shipping means the correctness
fixes are in). **Discharges:** Λ-08, and Λ-02's shipped half.
---
### Λ-W6 — The instrument module, and the host-safety contract
**Two tracks, disjoint: one is build + docs, the other is source.** At the end of this wave
the Linux instrument loads in any host, processes audio, and has **no editor at all** —
which is 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.
**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: platform clause
becomes "Windows and Linux; the editor is REAPER-hosted", VST3-only and REAPER-only
survive verbatim. 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** in `src/shell/instrument/CMakeLists.txt`'s `if(WIN32 AND EXISTS ...)` gate.
The `EXISTS` half stays (a fresh clone with no VST3 slice must still configure); the
`WIN32` half becomes a Windows-or-Linux predicate.
- **B2** — the artifact becomes a directory:
`reasampler_9000.vst3/Contents/<uname -m>-linux/reasampler_9000.so`, per V10's answer.
`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 `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; 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.
**Prerequisites.** Λ-W2-T2 (thread linkage, visibility, warnings), Λ-W3-T1 (V10, and 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.
**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), 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 until someone decides otherwise; see §"What Phase Λ does
not do".
- **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 in §"The non-REAPER-host safety
criterion", 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. 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** (see the Γ dependency below).
**Prerequisites.** Λ-W6-T1 (there must be a module to load). Concurrent with nothing else in
this wave. **Discharges:** Λ-D3's Λ-W6 half; L2-08's fallback half.
---
### Λ-W7 — The SWELL bootstrap
#### Λ-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.
**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 implementation shape in §"The non-REAPER-host safety
criterion": never define `SWELL_LOAD_SWELL_DYLIB`; own the load through exported surface
only (`SWELLAPI_GetFunc`, `SWELL_set_app_main`, our own `SWELL_dllMain`); gate on the host
name **and** the `dlopen`; probe once, lazily, off the audio thread.
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 one is not fatal**
— `doinit` substitutes a zero-returning `dummyFunc` for each miss, so a partial table
degrades rather than crashes; whether to accept a partial table or refuse is a judgment for
implementation 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, and **V9 answered affirmatively**. If V9 is negative,
this track does not exist and Λ-D2 needs a re-ruling.
**Discharges:** B3 (route B3a); Λ-D3's runtime half.
---
### Λ-W8 — The X11 editor
#### Λ-W8-T1 — `x11-embed-view`
**Goal.** The ReaSampler 9000 editor opens, draws and responds under REAPER on Linux.
**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 cost this at **L**, and call it a new competence rather than a port
(L2-08; T1 B4 = "build S, source L"). 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` / their unregisters
against `Linux::IRunLoop`). 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. It sits
under `public.sdk/`, so the documented narrow submodule init already pulls it. **This does
not shrink the L band** — it is a reading input, not a library — but 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, 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 (the phase-wide
structural heuristic in root `CLAUDE.md`).
- 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."
**Prerequisites.** Λ-W7-T1. **Discharges:** B4, L2-08.
---
### Concurrency, and the shared files
**Wave order is serial; tracks within a wave are concurrent.** Two deliberate exceptions to
the strict reading:
- **Λ-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.
- **Λ-W6-T2 is prerequisite-gated on Λ-W6-T1** (there must be a module before it can refuse
to show a view). They are file-disjoint but not order-free — the same shape PLAN.md
already records for Γ-W3 ("T1 before T2 — serial").
**Shared files, named** — all textual adjacency, not semantic contention, unless marked:
| 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), Λ-W4-T2 (`kitFontsInit`'s five `loadFont` call sites) | 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 resource-id-0 route they are 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. |
---
## Cross-phase dependency: Γ-W4-T1
**Verified against `docs/PLAN.md` while writing this: `Γ-W4-T1 vst3-parameter-set` is
listed as pending** (PLAN.md §Γ-W4, and in the plan's summary map). Its own spec states the
current position plainly: *"Today the plugin has zero parameters"* —
`ReaSamplerProcessor::initialize` never populates `SingleComponentEffect::parameters`, so
`getParameterCount()` returns the SDK default 0. The T2 audit confirms the same by grep.
**Why it matters to Λ.** The host's fallback when a plug-in reports no usable editor is its
**generic parameter UI**. With zero parameters registered, that fallback is empty — so
today, "no editor" degrades to *nothing*, not to *controls*. Once Γ-W4-T1 lands its 44
derived parameters, the identical Λ code path degrades to a usable generic UI instead.
**The dependency and its direction:**
- **Λ does not block Γ**, and **Γ does not block Λ.** Λ-W6-T2's acceptance criteria are
written to pass with zero parameters — "the host shows its generic UI, which may be
empty" is a pass. Nothing in Λ is scheduled against Γ.
- **Γ landing strictly improves Λ's user-visible outcome**, at no cost to Λ. If Γ-W4-T1 is
already on `dev` when Λ-W6-T2 is verified, the tester will see a populated generic UI;
that is a better result, not a different criterion.
- **Λ must not register any VST3 parameter itself.** Γ-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.**
- **Another team is running Γ concurrently. Λ assumes no schedule for it** — not "Γ will
land first", not "Γ will land later". If a Λ track's brief needs a Γ fact, it reads
`dev` at dispatch time.
---
## Calls made here (product-designer calls, not forks)
Two questions the audits raised as `[Daniel]` that this doc answers rather than escalates.
Contradict either in review with an argument.
**Silent font substitution is not acceptable; the kit gets an explicit Linux face list**
(T2 [Daniel] 3, landed as Λ-W4-T2). The reasoning is `docs/product/visual-design-language.md`:
the kit's type is part of a deliberate visual identity, and the palette-role discipline
exists precisely so a visual direction is a single-file change rather than an emergent
property of the host. Letting fontconfig pick is the opposite of that. The cost is five
string literals. The one thing that would change the call is if the sweep shows the stock
substitution is *already* the intended face on every target distro — unlikely, and it would
make the face list a no-op rather than wrong.
**The `[verify — Linux]` items are not forks and do not wait for Daniel.** T2's [Daniel]
list included items that a Linux box settles (which SWELL build REAPER ships, whether
fontconfig lands Consolas on a monospace face, whether drag-out is accepted as a copy).
Those are Λ-W3 sweep items. A question is only a fork here if reading code and running a
Linux box both leave it open.
---
## What Phase Λ does not do
- **macOS.** Λ-D4. Shared-form edits are made and noted as shared; no macOS build, no macOS
verification, no signing or notarization, no `swell-modstub.mm`-under-CXX-only
investigation, no fix for L2-11's case-insensitive-filesystem defect.
- **XDG trash.** Λ-D5. Hard `unlink` with a platform-aware confirmation. The XDG spec
(`~/.local/share/Trash` plus a `.trashinfo` sidecar) is a self-contained move-plus-metadata
write and remains a clean follow-up if a Linux user ever asks for it — file it to
`docs/TODO.md`, do not smuggle it into Λ-W4-T1.
- **Route B3b's dependency surface.** Λ-D2. No vendored SWELL build, no GDK/GTK3, no
FreeType, no Fontconfig, no OpenGL, no `pkg-config` in this tree. If a track finds itself
reaching for one of those, the design has drifted and the track stops.
- **An editor in a non-REAPER host.** Λ-D3 requires safe degradation, not capability. Even
with `libSwell.so` reachable, a non-REAPER host has no SWELL message loop, so the
host-name gate refuses on purpose.
- **The TCP/MCP embed strip on Linux.** `reasampler_embed`'s
`REAPER_FXEMBED_WM_IS_SUPPORTED` keeps returning 0 off Windows. It is a second 0→1 on a
read-only secondary surface, its paint body is LICE and would port, and it becomes
genuinely cheap once Λ-W8 lands. Deliberately out, and worth revisiting after.
- **VST2 / AU / CLAP / standalone.** D5's format and host clauses survive Λ-D1 untouched.
- **CI.** Λ-F1 is open; until it is ruled, Λ ships nothing that presumes a runner.
- **Any parameter registration.** Γ-W4-T1's, exclusively. See above.
- **Windows behaviour changes.** Every track carries a "Windows unchanged" criterion. This
is a port, and a port that improves Windows by accident has also changed Windows by
accident.
---
## Open forks — Daniel's
### Λ-F1 — Does CI get built in this phase, and on what runner? *(audit D4, unruled)*
**No recommendation.** This one is genuinely a resourcing call, and both sides are
defensible enough that a product-designer preference would be noise.
**For building it now:** `versioning-and-release.md` already commits to multiple platform
artifacts per channel per release and hands "the pipeline" to dev-ops — and there is no CI
configuration anywhere in the repo, on any platform (`.github` and `.gitlab-ci.yml` are both
absent). The moment Linux ships, the tree has two toolchains and one of them is not on the
developer's machine. Every Windows-only commit after that is a Linux regression waiting to
be discovered by a user. A GitHub-Actions job running `cmake` + `ctest` on Ubuntu would
catch L2-01-class breakage — an undefined Win32 symbol — at push time, which is exactly the
class of defect this whole phase exists to clear.
**For deferring it:** CI is infrastructure, not product, and this phase is already the
largest thing in the plan. A runner also forces Λ-F4 (the support floor) to be answered
immediately, because the runner image *is* the declared floor. And CI's value is highest
*after* the first Linux build works, not before — a red pipeline on day one teaches nothing
the Λ-W3 sweep is not already teaching.
**What closing it unblocks:** whether Λ-W5-T1 writes a pipeline paragraph or a
"deferred to dev-ops" paragraph, and whether Λ-F4 must be closed in this phase or can be
carried. Nothing is gated on it before Λ-W5.
### Λ-F2 — The dialog-resource route: resource-id-0, or resgen? *(audit D7, unruled)*
**This is the only fork that gates a track.** Both audits now carry the evidence.
**Route A — the resource-id-0 escape hatch.** SWELL documents it in
`swell-functions.h:606608`: a `CreateDialog` creating a child window may pass resource id
0, which creates an opaque child window, provided a `WNDPROC` returning `LRESULT` (cast to
`DLGPROC`) is passed instead of a real `DLGPROC`. The implementation confirms both halves —
`swell-dlg-generic.cpp` skips the resource lookup entirely when `resid` is 0. And
`IDD_BANK_PANEL` is precisely the case it was written for: `src/resource.rc`'s `IDD_BANK_PANEL` dialog block is a
`WS_CHILD` dialog with an empty `BEGIN`/`END` body and zero controls, whose own header
comment says "the bank_panel shell owns every pixel and draws the sample grid with LICE in
`WM_PAINT`". **Taking this route deletes the entire resgen pipeline from the non-Windows
builds** — the PHP dependency, the generated-file-in-the-source-tree problem, the
per-platform regeneration story, and the three stacked defects in the current commented-out
instructions. Cost: a source change in `panel_window.cpp` and the DLGPROC→WNDPROC return
contract.
**Route B — wire up resgen properly.** `find_program(PHP …)`, an `add_custom_command`
generating into `${CMAKE_CURRENT_BINARY_DIR}` (never the source tree — `.gitignore` has no
pattern for the generated name), a one-line shim TU that includes `swell-dlggen.h` then the
generated file, and that shim added to `target_sources`. This is the M end of Λ-01's band.
**The asymmetry neither audit stated, and it is the reason this is worth ruling rather than
delegating.** L2-07 establishes that SWELL's only opt-in surface for `WS_EX_ACCEPTFILES` is
the **dialog resource** — `swell_resgen` translates a dialog's `WS_EX_ACCEPTFILES` style
into `SWELL_DLG_WS_DROPTARGET`, and `swell-dlg-generic.cpp` sets the ex-style from that flag
at creation time. **Under Route A there is no resource, so that bit can never be set from
one.** L2-07's own analysis says the bit only *suppresses* the unhandled-drop
forward-to-parent and that the drop reaches `dlgProc` anyway via the GDK backend's
`ChildWindowFromPoint` descent — and notes, correctly, that with zero child controls the
descent has nowhere to land but the panel HWND itself. That reasoning is good but it is
**reasoning, not a run**. So:
- Route A is cheaper and structurally simpler, and it stakes the panel's file-drop ingest —
one of three ingest surfaces — entirely on that unverified descent.
- Route B is more machinery and one more build-time dependency, and it can set the bit
explicitly if the descent turns out not to be enough.
**What closing it unblocks:** Λ-W2-T3's dispatch, and therefore whether Λ-W3 is one Linux
session or two. **Leaving it open has a real cost and it is the only fork here that does.**
**A third option, if Daniel wants the cheap route without the exposure:** take Route A and
treat the file-drop check as a hard gate in Λ-W3's sweep — if the drop does not arrive,
Route B becomes a follow-up track rather than a rewrite, because the panel is already
rendering by then and the resgen work is additive. That is the option I would take, and it
is stated as an option rather than a recommendation because it spends a second Linux session
in the bad case.
### Λ-F3 — Does "copy-only is structural" survive as a shipped invariant? *(T2 [Daniel] 4, unruled)*
**Recommendation: keep the invariant, scope the *enforcement* claim to Windows.** The
product promise ("dragging a bank file out of ReaSampler copies it; the bank is never
raided by a drop target") stays; the *mechanism* sentence in `drag_out_win.h` becomes
"structurally enforced on Windows via `DoDragDrop`'s `DROPEFFECT_COPY`-only mask;
advertised, not enforced, on SWELL, which takes no effect mask."
**The counter-argument is real.** An invariant that one platform cannot enforce is arguably
not an invariant, and the tree's house style treats invariants as load-bearing rather than
aspirational. Weakening the *global* wording to "copy-only, best effort" would be the honest
alternative — and it would also correctly warn a Windows reader not to rely on the guarantee
in shared code. What that loses is a genuinely stronger Windows property, stated as weaker
than it is.
**Non-gating.** Λ-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, not the decision)*
The audits surface three separate manifestations of one unmade decision: Λ-07 (no stated
minimum compiler; GCC 8 needs an explicit `-lstdc++fs` for `std::filesystem`, GCC ≥ 9 does
not), V8 (the same, as a check), and V10 (which `uname -m` values the VST3 bundle must
carry — `module_linux.cpp` keys the bundle directory off exactly that string, and which
architectures REAPER itself ships for Linux is an external fact with no source in this tree).
**Recommendation: declare a narrow floor — current-stable-distro glibc, GCC ≥ 9,
`x86_64-linux` only — and widen it on request.** ReaSampler is a per-project capture tool
for people running a commercial DAW; the audience running a five-year-old toolchain on
armv7 is small, and every architecture in the bundle is a build target someone has to
maintain and verify.
**The counter:** REAPER's own Linux reputation is partly built on running well on modest and
non-x86 hardware, and `aarch64` in particular is no longer exotic. Adding it later means a
second bundle directory and a second verification pass on every future release, which is
cheaper to establish once than to retrofit.
**What closing it unblocks:** Λ-W2-T2's compiler-floor line, Λ-W5-T1's install and README
text, Λ-W6-T1's bundle directory set, and Λ-F1's runner image. **Non-gating for Λ-W2** (a
recorded floor can be widened), **gating for Λ-W5** — a shipped artifact has to say what it
runs on.
---
## Where the audits disagreed, or left a hole
Recorded so a reader knows which parts of this doc are synthesis rather than citation.
1. **The `WS_EX_ACCEPTFILES` / resource-id-0 interaction is a genuine gap between the two
audits.** T1's Λ-01 recommends the id-0 route and says it makes resgen disappear; T2's
L2-07 independently establishes that the drop-accept ex-style bit can only come from the
dialog resource. Neither doc composed the two. §Λ-F2 does, and it is the substantive
reason that fork is worth ruling rather than delegating to review.
2. **T2 flags its own severity inconsistency and hands it on.** L2-03 is graded Blocker on
failure-mode quality with no direct evidence; L2-04 is graded Major with a confirmed
mechanism and an equally severe outcome. T2 says explicitly that resolving the axis is a
scope call for whoever prioritises the two. This doc **dissolves rather than resolves**
it — both land in Λ-W2, in different tracks — so the grade never has to be adjudicated.
3. **The absent-`libSwell.so` failure mode was flagged unverified and is, in fact,
readable.** Both audits treat it as a `[verify — Linux]` question. It is not: the
vendored `swell-modstub-generic.cpp` calls `exit(2)` on a failed `dlopen` from a static
constructor, and `exit(1)` on an incomplete API table. That is a fact about a file in
this tree, and it converts Daniel's Λ-D3 from a defensive precaution into a specific
design constraint on the ruled route. Filled here, in §"The non-REAPER-host safety
criterion".
4. **Neither audit swept the VST3 SDK's `samples/` tree.** Both enumerate the vendored
*slice* the build compiles, which is correct for their questions, but
`public.sdk/samples/vst-hosting/` is on disk under the documented narrow submodule init
and contains two directly relevant things: `validator/` (a headless non-REAPER host
harness — the right instrument for Λ-D3's acceptance criteria) and
`editorhost/source/platform/linux/` (a worked `Linux::IRunLoop` implementation and an
X11-embed window). Named in Λ-W6-T2, Λ-W7-T1 and Λ-W8-T1. **This does not move the L
band on L2-08** — the audits' estimate stands; it removes a "we are guessing at the
contract" risk from it.
5. **The beta channel on Linux is uncosted in both audits.** T1 confirms the channel fork is
platform-independent by construction and stops there; neither audit builds, installs, or
coexists two channels on Linux, and neither considers the VST3 bundle's per-channel name
and UID on that platform. Almost certainly free; assigned to Λ-W5-T1 and Λ-W6-T1 as an
explicit criterion rather than an assumption.
6. **Two loose ends left as-is, deliberately.** T1's `lice_colorspace.cpp` question (SWELL's
own Makefile lists a fifth LICE TU our four-TU slice omits; the audit calls it
"unreconciled but harmless") and the `-Wl,--no-undefined` question are both answered by a
single successful Linux link. Both are Λ-W3 sweep items, not findings.
---
## Wave map
```
Phase Λ — ReaSampler on Linux (extension + ReaSampler 9000; macOS out)
W1 The audits [COMPLETE]
T1 build-toolchain-audit ....... Λ-01..Λ-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 ............. Λ-02..Λ-05, Λ-07, Λ-09, Λ-10 -> V5, V6, V8
T3 panel-dialog-resource ....... Λ-01, L2-06, L2-07 -> V7 [GATED: Λ-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 §5's seven; 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 . Λ-06, L2-10, L2-11 (Linux half)
W5 Ship the extension [1 track]
T1 linux-packaging-and-install . Λ-08 + Λ-02's shipped half + both channels
W6 The instrument module, and 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 Λ-D2 re-rules]
W8 The X11 editor [1 track]
T1 x11-embed-view .............. B4, L2-08. The phase's one L-band source item.
W6 may run concurrently with W4 and W5 — file-disjoint; the only reason to serialise
is attention. W2's edits are authored BLIND and their acceptance criteria are
discharged in W3; W2 is not "done" until W3 runs, and the plan says so rather than
pretending otherwise.
Open: Λ-F1 (CI), Λ-F2 (resource route — the only one that gates a track),
Λ-F3 (copy-only invariant wording), Λ-F4 (declared support floor — gates W5).
```