Files
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

79 KiB
Raw Permalink Blame History

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 dlopening 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.

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 dlopened, i.e. during the host's plugin scan, before any VST3 code of ours executes.
  • :98103dlopen(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.
  • Λ-03CXX_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).
  • Λ-04find_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_asserts (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 fataldoinit 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: MoveWindowSetWindowPos; GetWindowLongPtr/SetWindowLongPtr → the non-Ptr forms returning LONG_PTR; GetKeyStateGetAsyncKeyState; 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 resourceswell_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).