# PLAN

The post-1.0 roadmap. Seventeen queued items consolidated into overlapping areas and
sequenced into a Phase → Wave → Track hierarchy that implementation specialists can be
dispatched against directly — **plus Phase Γ**, which did not come from those seventeen
(it came from a direct interview, 2026-08-01) and is scoped in
`docs/product/instrument-control-surface.md`, **and Phase Ψ**, which likewise did not
come from the seventeen: it came from a direct list of seven defects and refinements
(Daniel, 2026-08-01) and is specified inline in its own section below — there is no
backing product doc for it, **and Phase Ε**, which likewise did not come from the
seventeen: it came from a direct request (Daniel, 2026-08-02) and is scoped in
`docs/product/bank-package.md`, **and Phase Ρ**, likewise a direct request (Daniel,
2026-08-02), scoped in `docs/product/render-in-place.md`, **and Phase Λ** — the Linux port
of both artifacts — likewise a direct request (Daniel, 2026-08-02), scoped in
`docs/product/linux-readiness.md`.

## What this doc is, and how it relates to the others

- **`docs/PLAN.md`** (this file) — the active on-deck specification list. Each track is
  written so a specialist brief is writable from this file alone: goal, consolidated
  source items, surface boundary, behavior, acceptance criteria, open questions,
  prerequisites.
- **`docs/TODO-1.0.md`** — retained as the **verbatim-provenance appendix**. It holds
  Daniel's raw asks and every answer round, unedited; where this plan compresses a
  behavior bullet, that file is the backing record. Items are cited here by number
  (e.g. "consolidates items 1, 8, 14"). It is not a work queue any more; this file is.
- **`docs/TODO.md`** — deferred follow-ups with recorded rationale, unrelated to the
  seventeen (with one flagged intersection: see "Flagged for awareness" below).
- **`docs/COMPLETED.md`** / **`docs/ARCHIVE.md`** — doc-keeper's. When a track here
  finishes, its point is removed from this file and appended to `COMPLETED.md` with any
  deviation between landed code and spec noted.
- **`docs/product/`** — the product-design reasoning behind prior phases. Grep for a
  cited section rather than reading a file whole.

**Worktree slug convention:** `p<phase>-w<wave>-t<track>-<slug>`. Greek phase letters
transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**, **Ψ → `psi`**, **Ε → `e`**, **Ρ → `r`**,
**Λ → `l`**. So Θ-W1-T1 dispatches into `pth-w1-t1-zone-retirement`, Γ-W1-T1 into
`pg-w1-t1-knob-interaction-law`, Ψ-W1-T1 into `ppsi-w1-t1-capture-range-exactness`, and
Λ-W2-T1 into `pl-w2-t1-linux-compile-blockers` (Phase Λ's two audit tracks already ran
under `pl-w1-t1-build-toolchain-audit` and `pl-w1-t2-source-runtime-audit`).

## Decision state

Everything carried forward from `TODO-1.0.md` is classified **[verify]** (answerable by
reading code or running the DAW) or **[propose]** (a design call made at implementation
review with a proposal, not a Daniel call); that classification is preserved per
question, attached to the track that will answer it.

**Read the Phase Λ paragraph below before relying on the plan-wide "nothing is unanswered"
claim the Γ, Ε and Ρ paragraphs make.** Λ (added 2026-08-02) carries **four open
[Daniel]-class forks**, so that claim is no longer true of the plan as a whole; it is true
of every phase except Λ, and Λ's paragraph states exactly which of its four forks gate
anything.

Θ-W3-T1's two genuine **[Daniel]** questions — which no amount of code-reading could
answer — are both ruled on and the track has landed; see `docs/COMPLETED.md` for the
full narrative. **Reload tier = Grouping B** (continuous knobs live: filter cutoff/Q/
morph/drive/mod amount/key-track, every envelope stage time and level; root note, loop
span, and start frame still trigger a full reload). **Mid-stage rule = candidate (iv),
hold normalized stage position** (φ = elapsed/duration held fixed across a duration
change, then advancing at 1/newDuration). **Phase Γ opened six [Daniel]-class forks
(Γ-F1…Γ-F6) and all six are ruled** (Daniel, 2026-08-01) — the rulings are folded into the
tracks below and indexed in `docs/product/instrument-control-surface.md` §8. **Γ-F6 closed
with a correction to the analysis, not merely a ruling**: dynamic reported latency is routine
for VST3 instruments and REAPER handles it as a matter of course; what makes the mandated
restart expensive *here* was self-inflicted (`setActive(true)` called `reloadInstrument`), so
the cost was ours to reduce. **That reduction landed in Γ-W3** — see `docs/COMPLETED.md`.

**Γ-F3 was subsequently REVERSED and a seventh fork opened AND CLOSED, all by Daniel's later
rulings of 2026-08-01.** Γ-F3 (*"the stage-time ceiling stays 2.0 s"*) is replaced by *"extend
the stage lengths to 10s"* — the ceiling moves in Γ-W1-T1. **Γ-F7** (the VST3 parameter
*order*) is **RULED: signal flow** — *"signal flow order."* **There is now NO unanswered
[Daniel]-class question anywhere in this plan.**

**Phase Ε (added 2026-08-02) opened three more [Daniel]-class forks and ALL THREE ARE
RULED**, same day (Daniel, 2026-08-02): **Ε-F1** container format — *"proprietary
container"*, the hand-rolled `RSBK`; **Ε-F2** import target — *"always lands as a new bank,
with an auto suffix if name collision"*; **Ε-F3** import under a degraded tracking ledger —
*"refuse mismatched import."* The rulings are folded into the tracks below and indexed at
`docs/product/bank-package.md` §"Rulings". **Two of the three landed somewhere other than
the framing recommendation:** Ε-F2 dropped the proposed rename prompt in favour of a
deterministic suffix, and Ε-F3 reversed allow-with-confirm to refuse — carried by the
framing's own counter-argument, that the accepted tracking residual contemplates *one*
untracked capture while a bulk import strands hundreds in a single gesture. **The plan-wide
claim above therefore holds unqualified — no unanswered [Daniel]-class question remains
anywhere in this plan, Phase Ε included — and no track in this plan is gated on a
decision.**

**Phase Ρ (added 2026-08-02) opened three more [Daniel]-class forks and ALL THREE ARE
RULED**, same day (Daniel, 2026-08-02): **Ρ-F1** multi-track — *"refuse"*, one track per
fire, a settled non-goal rather than a deferral; **Ρ-F2** the result track's mode —
*"for this action which is not a capture, the result track should always go to
arrange"*; **Ρ-F3** tail — *"follow panel tail settings."* The rulings are folded into
the track below and indexed at `docs/product/render-in-place.md` §"Rulings". **Ρ-F2
overrode the request's own original wording** ("stays in whatever mode was active") and
is the only one of the three that changed the spec: the result track is now an Arrange
member unconditionally, the A/B-on-the-bench behaviour mode-following would have enabled
is gone, and the ruling pulls in a two-line fix to the panel's auto-tag detector that
would otherwise reverse it on the next timer tick. **The plan-wide claim above therefore
still holds unqualified — no unanswered [Daniel]-class question remains anywhere in this
plan, Phase Ρ included — and no track in this plan is gated on a decision.**

**Phase Λ (added 2026-08-02) breaks that unqualified claim, and it is corrected here rather
than left to be discovered.** The claim as written in the three paragraphs above — *"no
unanswered [Daniel]-class question remains anywhere in this plan"* and *"no track in this
plan is gated on a decision"* — is now **scoped to Θ, Ξ, Γ, Ψ, Ε and Ρ.** It is still true
of all six. It is **not** true of Λ. **Λ opened four [Daniel]-class forks and NONE of them
is ruled** (Λ-F1 CI; Λ-F2 the dialog-resource route; Λ-F3 the copy-only drag-out invariant's
wording; Λ-F4 the declared support floor). Each is stated with the evidence for both sides
at `docs/product/linux-readiness.md` §"Open forks — Daniel's", and the Phase Λ section below
carries them as a table. What they gate is narrow and is not a matter of judgement:

- **Λ-F2 is the only one that gates a dispatch** — Λ-W2-T3 `panel-dialog-resource` cannot be
  briefed until the route is chosen, because the two routes own different files. Leaving it
  open does not stall the phase; it slips that one track to Λ-W4 and **splits Λ-W3's
  verification sweep into two Linux sessions**, which is the fork's actual price.
- **Λ-F4 gates the ship wave, not a dispatch.** Λ-W2 can record a floor and widen it; Λ-W5
  cannot ship an artifact that does not say what it runs on. Answerable any time before
  Λ-W5-T1 is briefed.
- **Λ-F1 and Λ-F3 gate nothing at all.** Λ-F1 (CI) decides only which paragraph Λ-W5-T1
  writes; Λ-F3 decides one sentence's wording inside an edit Λ-W4-T3 makes either way.
- **The other eleven Λ tracks are dispatchable against open forks**, in the sense that none
  of them waits on a decision. What every Λ track waits on instead is a Linux box — a
  different kind of unknown, and the reason Λ-W3 exists.

**Λ's six [Daniel] rulings of 2026-08-02 are settled** and are recorded in the phase section
below in the same shape as Γ's and Ε's: the instrument is in scope; SWELL is reached by
`dlopen`ing REAPER's own `libSwell.so` rather than vendoring it; the editor is REAPER-only
but every other host must degrade safely; macOS is out; the Linux artifact is shipped rather
than developer-only; and a hard `unlink` prune is acceptable with a platform-aware
confirmation. **Do not re-litigate them.**

**Ruling 3 (Daniel, 2026-08-01) — real units at the host boundary.** *"The parameter values
exposed to the VST host should be in real units, such that the host automation lanes report
usable values."* Satisfied through VST3's **plain-value layer**, not its wire format (which is
normalized and cannot be otherwise): `toPlain`/`toNormalized`, `getParamStringByValue` and
`ParameterInfo::units`. Specified at **`docs/product/parameter-automation.md` §6.7** — the
per-category unit/precision table, the one-formatter invariant, the `stepCount` sweep, and the
resolution of the apparent conflict with the filter's re-taper prohibition. It lands entirely
in Γ-W4-T1 and changes no wave boundary.

### Flagged for awareness — not blocking, but decision-grade

1. **Item 15's cross-artifact seam is RESOLVED — this is no longer an unknown.** The
   instrument is a read-only bank consumer by invariant (`src/shell/instrument/CLAUDE.md`),
   and the one previous attempt at an instrument→extension relay (S13) closed with a
   **DEGRADED** spike verdict and was deferred (`docs/TODO.md`). Resample required that
   crossing. Ξ-W2-T1 ratified Decision 1 = (1b): the editor invokes the extension's bake
   action directly over the VST-host bridge (`NamedCommandLookup`/`Main_OnCommandEx`), no
   poller, no nonce — dissolving the S13 DEGRADED verdict rather than re-litigating it. The
   read-only bank invariant held: the crossing is a bridge call, not a shell-side bank
   write. See `docs/COMPLETED.md` for the full narrative.

2. **The "Γ before Ξ-W2" ordering is VIOLATED, it was never Daniel's choice, and Γ now owns
   the correction.** Daniel, 2026-08-01: *"xi was started before I spun you up, we'll have to
   correct phase xi inside gamma. wasn't a choice."* Ξ-W2-T1 (`resample-bake-chain`) ran
   ahead of this plan's sequencing claim, so the bake's settled reset scope — which
   enumerates parameters **by name** — ships incomplete: it cannot name rate, pitch offset or
   the limiter flag, none of which existed when it was written.

   **This is no longer a scheduling constraint to honour. It is a correction obligation with
   a named owner: Γ-W3-T2 `bake-reset-amendment`.** The classification costs no Daniel
   decision — `docs/product/instrument-control-surface.md` §3.4 pre-classifies all three
   against Ξ-W2's own ratified rule (all **reset**) — but the amendment must be written
   **against what Ξ-W2-T1 actually shipped, not against what this plan predicted it would
   ship.** Ruling 1 adds a second correction of the same shape, homed on Γ-W4-T1 rather than
   here: see item 3.

3. **The one-way doors are now IN-PHASE, and the sweep for them is a delivered artifact.**
   Ruling 1 (Daniel, 2026-08-01) schedules VST3 parameter reporting **inside Phase Γ**, as
   Γ-W4-T1. Everything that participates in a parameter's normalization therefore freezes at
   the end of this phase rather than at the start of some later one, and anything that ought
   to move must move first.

   - **The taper** (Γ-W1-T1) — known, and the reason this phase was ordered as it was.
   - **The stage-time ceiling 2.0 → 10.0 s** (Γ-W1-T1) — Γ-F3 **reversed** by Daniel's
     *"extend the stage lengths to 10s."* A range endpoint is normalization exactly as much
     as the curve between the endpoints is.
   - **Two further doors that need action, both new**, both landing on Γ-W1-T1: every
     default must have an **exact normalized preimage** (a host's reset-to-default has no
     `resetDeckParam` bypass to use), and the filter's four `*Norm` controls **must not be
     re-tapered** (their laws are already wire-frozen in payload v9).
   - **Six more constants freeze without needing to change**, and three are already frozen
     for unrelated reasons; the complete sweep, with dispositions and with what was checked,
     is `docs/product/parameter-automation.md` §8. **That doc is no longer scoping-only —
     §§6–10 are the specification Γ-W4-T1 is built from.**

## Phase-wide acceptance criteria

These bind every track in all four phases and are stated once here rather than repeated
per track. **Phase Γ adds a set of its own**, stated in its phase header. **Phase Ψ adds
none** — the structural heuristics, performance guardrails, and product invariants below
bind it exactly as written (its phase header states its performance posture against the
named hot paths).

### Structural (root `CLAUDE.md`, Daniel 2026-07-28)

- **More directories is a must; more files is good; ~600-line file ceiling.** The
  ceiling is the *bar*; a responsibility seam is the *method*. Bisection-to-hit-the-
  number is rejected. `sampler_core.cpp` (956 lines) is the standing documented
  hot-path exception — Θ-W1-T1 re-seams it, and any surviving over-ceiling TU must
  carry the same explicit justification.
- **Templates where earned** — compile-time dedup with zero runtime cost, off the hot
  paths. Not for types that differ in name only.
- **SOLID is great, but saved CPU is better.** No dispatch-stack blowouts anywhere;
  prefer static polymorphism where the types are compile-time-known.

### Performance guardrails

Root `CLAUDE.md`'s five extension-side guardrails (peaks envelope compute, audition,
realtime-capture tick, JSON, `FxBypassGuard`) are unchanged by this plan; no track here
touches them. The instrument adds a sixth surface that binds every Θ track:

- **`process()` — the per-voice-per-sample path — takes no new indirection.** The
  filter tick, the envelope evaluation (staged *and* spline), the pitch-shift read, and
  the loop read all sit on it. Concrete, inlineable types only: **no `IEnvelope`, no
  `IFilter`, no virtual per-voice `tick()`**. A filter with two modes is a
  branch-predictable switch or a compile-time-known dispatch, never a vtable. Spline
  evaluation is a binary search over a point array, not a polymorphic curve object.
- **No allocation, no file I/O, no bridge call in `process()`.** The off-audio-thread
  `reloadInstrument` + atomic pointer swap stays the only way new state reaches the
  audio thread. Every new parameter this plan adds follows that path.
- **A split that would add a hot-path indirection is out of scope — rework it or drop
  it.**

### Product invariants

- **Capture and placement are separate acts.** No track here may place a timeline item.
  Item 15's bake explicitly must not, and one of its candidate architectures uses a
  *temporary* arrange mutation — that candidate must leave the arrange byte-identical.
- **Prune is the single, exclusive file-deletion authority.** Item 15's "replace" never
  deletes bytes; item 17's consolidation may not weaken any protection prune has today.
- **The instrument never writes the bank.** Item 15 is the first feature that needs to,
  and it resolves that by *asking the extension*, not by breaching the invariant.
- **Migration bar: a project saved before a change reopens sounding identical.** Holds
  everywhere except item 16's genuinely-multi-zone case, where Daniel deliberately
  relaxed it.
- **Every pure module gets a `<module>_tests` target** that runs without REAPER or a
  DAW. New pure modules in this plan are not optional-test.

---

## Phase Θ — ReaSampler 9000: one parameter set, a filter, shapeable envelopes, a legible editor

**Ships:** the instrument with the zone system retired, a resonant HP/LP filter stage in
the voice path, curve-shapeable and spline-drawable envelopes on all three EGs,
Gate-mode loop sustain, and a re-laid, high-DPI-clean editor — plus the two extension
drag/drop defects that block getting captures into it.

**Consolidates items** 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16.

**All seven waves have landed — Phase Θ is complete.** W1 through W7 each carry their own
landed note above; see `docs/COMPLETED.md` for every track's full narrative. Θ-W7 was
opened after Θ-W6-T1 shipped, to fix two rendering defects Daniel found by eye; it did not
exist in the plan when this phase was originally scoped.

**The organizing constraint.** Three surfaces in the instrument are single-writer by
nature and dictate the wave shape:

1. **The parameter model** (`zone_params.h` + `component_state_io`) — every parameter
   addition touches both. Two tracks adding parameters concurrently is a merge fight
   and two competing `ComponentState` version bumps.
2. **The voice render path** (`sampler_core.cpp`) — filter insertion, envelope
   evaluation, loop read, and the Trigger tail all live there.
3. **The Sample face** (`editor_paint_sample.cpp` 516 lines / `editor_input_sample.cpp`
   583 lines) — every UI item repaints it, and both are already at the ceiling.

Parallelism in this phase therefore comes from **splitting the Sample face into bands**
(done once, in Θ-W1-T1) and from **extension-side work being genuinely disjoint** — not
from running two parameter-model tracks at once. Where a wave has one track, the
collision is real and the serialization is the correct answer.

---

### Θ-W1 — Collapse and re-seam

**All three tracks have landed** — Θ-W1-T1 (`zone-retirement`), Θ-W1-T2
(`capture-handoff-bugs`), and Θ-W1-T3 (`filter-dsp-port`) — see `docs/COMPLETED.md` for
the full narrative of each. Between them: the zone subsystem is retired, the wave's two
responsibility seams (the `sampler_core` split, the Sample-face band split) are in
place, the two extension-side capture-handoff defects are fixed, and the filter DSP has
landed as a standalone pure module — Θ-W2-T1 has since wired it into the voice path (see
below).

---

### Θ-W2 — Filter in the voice path; waveform and chrome bands

**Depends on W1 for:** the one-parameter-set model and the `sampler_core` seam that T1
writes the filter into; the `filter` module T1 wires up; the Sample-face band split and
band-stack allocator that T2 and T3 fill; and W1-T1's key-range answer, which decides
what T3's piano strip displays. Authoring any of this against the per-zone model means
writing storage plumbing W1 deletes.

**All three tracks have landed** — Θ-W2-T1 (`filter-voice-path`), Θ-W2-T2
(`stereo-waveform-lanes`), and Θ-W2-T3 (`toolbar-and-piano-strip`) — see
`docs/COMPLETED.md` for the full narrative of each. Between them: the filter sits in the
per-voice signal path with its own deck, the waveform band shows both channels in
stereo mode behind a type-enforced full-height overlay contract, and the chrome band's
toolbar and piano strip are cleaned up per spec. The three tracks were disjoint by
band — T1 owned the parameter model and the deck band, T2 owned the waveform band, T3
the chrome band — and none re-allocated the band stack.

---

### Θ-W3 — Live parameters, then the staged envelope system

**Depends on W2 for:** the filter envelope's existence — items 1, 8, and 14 govern
*three* envelopes, and the filter is the third; and for the Filter deck, which must
exist before it can receive a corner radio switch and inner curve dials. T1 additionally
depends on W2-T1 for the filter itself: the filter is the first control set where the
latched-at-note-on delivery model fails audibly, and it is what made the defect visible.

**Both tracks have landed** — Θ-W3-T1 (`live-parameter-delivery`) and Θ-W3-T2
(`staged-envelope-curves`) — see `docs/COMPLETED.md` for the full narrative of each.
Between them: every continuous playback control (filter cutoff/Q/morph/drive/mod
amount/key-track, every stage time and level on all three envelopes) now reaches a
sounding voice live instead of latching at note-on, and the envelope-overlay editor
grew from an amp-only fixture into the shared graphical surface for all three
envelopes — a corner radio switch per deck (none active by default), curve-shapeable
segments on every sloped stage (0.1–10 exponent, an inner dial paired with an overlay
knot), the release-right-anchored AHDSR layout against the pitch envelope's 1:1 AHD,
and the Trigger amp/filter fade pair folded into a Trigger AHD, consolidating what
were two staged-shape mechanisms into one. The Trigger × Preserve end-of-sample click
is fixed; the landed fix is wider than scoped, also ringing out Gate × Preserve ×
source-exhaustion, previously a hard cut. The ordering was deliberately serial — T1's
live-delivery mechanism landed first so T2's new curve exponents and Trigger AHD
fields were authored directly into it rather than backfilled afterward.

---

### Θ-W4 — Loop sustain and the velocity deck

**Depends on W3 for:** the envelope system both tracks compose with — T1's loop-sustain
is the Gate-mode sustain the AHDSR releases out of, and T2's bipolar pitch/filter curves
modulate targets whose envelopes W3 just reshaped. T2 additionally depends on W2-T1 for
the filter's existence and on W2-T3 for the preview button's toolbar position.

**Both tracks have landed** — Θ-W4-T1 (`gate-loop-sustain`) and Θ-W4-T2
(`velocity-deck-and-bipolar-curves`) — see `docs/COMPLETED.md` for the full narrative of
each. Between them: loop points are now a usable feature, with a Gate-mode loop acting
as the sustain and a parameterized crossfade at the seam; and the three velocity-curve
popups (amp, pitch, filter) now live together in a new VELOCITY deck group, with the
pitch and filter curves bipolar and flat-by-default so their modulation is off until
drawn, while the amp curve stays unipolar and unchanged, and the preview button's text
is replaced by a drawn play-triangle glyph. Params payload reached v11 with T1's loop
block and v12 with T2's velocity→pitch curve appended after it.

---

### Θ-W5 — Spline EGs

**Depends on W4 for:** the bipolar velocity-curve domain (W4-T2) — the spline algorithm
is **singly implemented and multi-referenced**, so its enhancement must land against the
final consumer set, and the last consumer to change domain is the pitch/filter velocity
curve; and for the Gate-mode loop (W4-T1), since "Gate is unavailable in Spline mode" is
only a real, testable rule once Gate has something to be unavailable *for*. It also
depends on W3's radio switch, which is how a spline contour reaches the overlay at all.

**Θ-W5-T1 has landed** — `spline-egs` — see `docs/COMPLETED.md` for the full narrative.
It shipped a free-drawn alternative to every staged envelope: pitch, filter, and amp
EGs can switch Staged → Spline and have their contour drawn directly on the waveform
overlay. The one shared monotone-spline implementation gained hard points — sharp
corners, no smoothing on either adjacent segment — flowing to every consumer including
the existing velocity→amp transfer curve, no fork. Both Staged and Spline state persist
simultaneously (saved-but-inactive, lossless round-trip); params payload reached v13,
and v12 projects still load. Gate is unavailable while a Spline EG is active; the
contour is a pure time function over the full sample length, normalized and drawn 1:1
with the sample's time axis. Point grammar converged on left-click add / right-click
delete / control-click hard-smooth toggle, one grammar across both spline consumers.
Staged segment knobs and their inner curve dials render disabled and reject edits while
Spline is active. Point-count ceiling: 128, a musical bound rather than a performance
one. A follow-on change in the same track reworked deck cell width: `-1` in `cellIds`
now means one cell's width, reserved and redistributed, rather than a blank cell holding
geometry — a Trigger face that drops Sustain and Release gets wider cells instead of
dead slots; group widths, row packing, deck height, and Gate-mode cell widths are
unchanged.

---

### Θ-W6 — Editor legibility pass

**Depends on W5 for:** the last change to a drawn surface. Item 13 is an audit whose
output is a disposition list over "every class of drawn surface," and item 10's sizing
pass is judged by eye over the finished layout — running either while the spline contour,
the disabled-knob state, or the deck inventory was still moving would have meant
auditing and then re-auditing. Θ-W5-T1 has landed (see `docs/COMPLETED.md`), so that
surface has stopped moving and this dependency is satisfied. The source doc names this
sequencing as an observation (item 13 after the layout/knob work); this plan adopts it
as the boundary.

**One track.** Both items repaint essentially every surface in the editor; concurrent
tracks would collide everywhere.

**Θ-W6-T1 has landed** — `legibility-and-antialiasing` — see `docs/COMPLETED.md` for the
full narrative. It shipped both halves together: knobs grew 28→40 px (inner curve dial
14→20), the deck cell 48×58→60×74, and the label band 12→16 px, now drawn in
`Font::Label` rather than `Font::Micro` — group captions and toggle segments deliberately
stayed `Font::Micro`, since bumping them would outgrow row 1's headroom at the floor
width. The editor's default/minimum size grew 840×620→980×680 to fit the wider deck at
floor width; an existing saved instance's window grows on open, and the floor is
validated by a derived test rather than literals. All fourteen time-constant labels now
read in ms (display-only; internal representation untouched) — `holdFraction` knobs,
`Len %`, and the bank panel's duration readout stayed out of scope. Double-click resets
each ring independently — outer ring resets the value, inner dial resets the exponent to
1.0 — reaching the chrome's preview-velocity knob too via a shared `inKnobFace` rule.

The antialiasing audit fixed knob arcs and needle, the inner dial arc and needle, staged
and spline envelope slopes, the spline contour, the velocity-popup trace, the waveform
outline, and the preview triangle; node handles, curve knots, knob discs, buttons, piano
keys, loop markers, borders, gradients, and text were already clean. The disposition
table is a standing artifact in `docs/product/visual-design-language.md` §8. The
piano-key open question is answered: not aliasing — every key is an axis-aligned
`LICE_FillRect`, so the earlier width defect was integer-division residue in the tiling,
not a sloped edge. High-DPI host scaling itself is unverified (deferred, see
`docs/TODO.md`).

All visual outcomes remain pending Daniel's by-eye sign-off on `dev`.

---

### Θ-W7 — Arc-and-spline antialiasing fix

**Depends on W6 for:** a drawn surface to find a defect on — this wave did not exist in
the plan; it was opened after Daniel found two rendering defects by eye once Θ-W6-T1
shipped, so the audit's own output is what surfaced them.

**One track.**

**Θ-W7-T1 has landed** — `arc-and-spline-aa` — see `docs/COMPLETED.md` for the full
narrative. The stacked-`LICE_Arc` knob and dial rings never reached an opaque core (peak
alpha measured 138/255), and the staged/spline envelope and velocity-curve traces were
fully aliased rather than gapped, from integer `cy` quantizing the slope. Both defects,
plus the two needles, now route through one pure analytic thick-stroke rasterizer —
coverage in a new `core/ui/stroke_aa`, the single LICE blend in a new
`shell/instrument/editor_stroke` — replacing `LICE_Arc` and `LICE_ThickFLine` outright.
Measured: arc peak alpha 138/255 → 255/255, arc perpendicular-weight ripple 67% → 5%,
spline weight ripple 29% → 3%, at a cost of +0.09 ms per full editor repaint. Daniel then
ruled every sub-2 px stroker width up to 2 px, since the stroker only guarantees an
opaque core at width ≥ 2 px; the knob track arc, the inner-dial needle, and the deck's
mini velocity trace each moved 1.0 → 2.0 px. `docs/product/visual-design-language.md` §8
is corrected — a false "keeps every ring antialiased" claim is deleted, the rows are
re-dispositioned with measurements, and the audit's methodological lesson (verifying
which primitive was called is not verifying what it rasterized) is recorded as a standing
blockquote. All visual outcomes remain pending Daniel's by-eye sign-off on `dev`.

---

## Phase Ξ — The resample loop

**Ships:** one consolidated, fully robust provenance/usage tracking system, and on top of
it the one-click in-sampler resample — dial → bake → dial again, with the bank as the
medium each iteration passes through.

**Consolidates items 15, 17.**

**Why these two and not more.** Item 17 is a **prerequisite of meaning** for item 15's
bank-side half: the replace-vs-add rule is "does provenance-tied usage exist," which
denotes nothing until the consolidated lineage records exist. Item 17 is otherwise
independent of the editor chain — which is what makes Ξ-W1 concurrency-safe with Θ.

**Ξ-W2 onward requires Phase Θ complete.** Item 15 presupposes the processing surface it
bakes — "filtering, pitching, amp all set up nice" is the instrument items 1, 2, 3, and 14
build — and its settled reset scope enumerates the filter parameters, the spline contours,
and the loop points by name. Baking a processing chain that does not exist yet is not a
schedule preference; the feature is not expressible. Phase Θ landed as of Θ-W7-T1 (see
`docs/COMPLETED.md`); this gate is satisfied.

**All three waves have landed — Phase Ξ is complete.** W1 through W3 each carry their own
landed note below; see `docs/COMPLETED.md` for every track's full narrative.

---

### Ξ-W1 — Consolidated tracking, and the programmed-note model

#### Ξ-W1-T1 — `tracking-consolidation`

**Landed** — see `docs/COMPLETED.md` for the full narrative. The provenance/usage
territory is now one system: a new `src/core/tracking/` directory holds `origin_ledger`
(the record family — `OriginRecord`/`OriginKind`, the insertion-ordered `OriginLedger`,
its JSON codec, and the `Fresh`/`Loaded`/`Unreadable`/`FutureVersion` load
classification) and `tracking_authority` (the one decision surface: `pruneProtection`
and `tiedUsageExists`), retiring `core/model/owned_manifest`. Both prune's protected set
and the resample's replace-vs-add decision are computed from one borrowed
`TrackingState`, so the two safety-critical consumers cannot drift apart.
`sample_usage` deliberately **stays in `core/wire`** — the consolidation is of the
decisions, not the codecs. The deferred persisted-instance-identity fix was **not**
folded in — the deferral is restated in `docs/TODO.md`, its one home. This track
landed `OriginRecord`'s birth-time `parentSampleId` chain as the lineage mechanism, but
**Ξ-W2-T1's "naming and lineage" open question (jointly held with this track) is
unaffected and still theirs to close** — display naming and user-readable iteration
lineage were not decided here.

#### Ξ-W1-T2 — `note-program-model`

**Landed** — see `docs/COMPLETED.md` for the full narrative. The programmed-capture-
signal model is a new pure module directory, `src/core/instrument/note/` — a fourth
peer of `engine/`/`map/`/`ui/` under `core/instrument/` — holding `musical_division`,
`tempo`, and `note_program` (`Velocity`, the denominated `OffsetAmount`, the anchored
`StartOffset`/`EndOffset`, `NoteProgram`, `resolveNote`). **Both open questions below
are answered, for Ξ-W3-T1:** negative offsets are legal in both directions (sign
uniform, positive is later in time; only an inverted window is refused, reported via
`ResolvedNote::windowCollapsed`), and the denomination seam is confirmed as of this
landing — note length stays musical-division-only, and an offset stores the denomination
it was entered in. **Superseded by Ξ-W3-T1's landed work** (see `docs/COMPLETED.md`):
`note/CLAUDE.md`'s musical-division-only rule is amended — a note length now carries
EITHER an exact derived duration or a musical division, not division-only. The offset
denomination rule is unaffected.

---

### Ξ-W2 — The bake chain

**Depends on Ξ-W1 for:** the consolidated lineage records that make replace-vs-add
computable (T1) and the programmed-note record the offline pass renders (T2). **Also
depends on all of Phase Θ** — see the phase note above.

**This wave ran AHEAD of Phase Γ, and that was not a choice.** The plan asserted Γ must land
first so the bake's reset list would be complete on the day it shipped; Ξ-W2-T1 was already
live. The consequence is owned, not absorbed: **Γ-W3-T2 `bake-reset-amendment`** completes
the list afterwards, and **Γ-W4-T1** adds the host-notification obligation once parameters
exist. Neither is this wave's work, and neither is a defect report against it.

**One track.** The bake is one gesture and one chain; the architecture decision at its
head governs every step after it.

#### Ξ-W2-T1 — `resample-bake-chain`

**Landed** — see `docs/COMPLETED.md` for the full narrative. The architecture decision
(this track's first deliverable) is ratified: **Decision 1 = (1b)**, the editor invokes
the extension's bake action directly over the VST-host bridge
(`NamedCommandLookup`/`Main_OnCommandEx`), no poller/nonce, dissolving the S13 DEGRADED
verdict; **Decision 2 = (2c)**, the instrument renders in-process and the extension
banks the file, taken over the plan's leaning toward (2a) on an engine-version-skew
argument. **Extension presence** resolved to cleanly-unavailable, not silently lossy —
the affordance refuses up front when the extension is not loaded. **Reset-scope edge
cases** classified against the ratified rule: play mode → RESET to Trigger, start point
→ RESET, channel mode and preview velocity → SURVIVE. **Phase Γ's additions remain
NOT this track's** — Γ-W3-T2 `bake-reset-amendment` still amends the reset list for
Γ's own new values, unaffected by this landing. **Naming and lineage** is proposed
(`Kick` → `Kick r2` → `Kick r3`) but not itself ratified — still open jointly with
Ξ-W1-T1's lineage-record question, which Ξ-W1-T1 answered on its own side
(`OriginRecord::parentSampleId`). Nothing was verified in a live REAPER session;
Daniel's manual verification is still owed.

---

### Ξ-W3 — The capture-signal popup

**Depends on Ξ-W2 for:** the bake chain this track was originally scoped to build a
preview against, under the acceptance criterion "preview and bake cannot diverge." **That
motivation is retired, not satisfied** — see below.

**One track.**

**Ξ-W3-T1 has landed** — `capture-signal-popup` — see `docs/COMPLETED.md` for the full
narrative, and it diverges substantially and deliberately from this section's spec. The
popup this track was scoped to build (a musical-division note-length picker, ms/beat-
editable offsets, velocity, and a preview trigger) was built (~1500 lines) and then
**abandoned unmerged** on Daniel's ruling: *"I didn't realize you had already derived a
usable window. The manual stuff for baking a specific midi length was just an idea, if we
have a smarter, fewer-clicks way of doing it, that is ideal. I just don't want to lose
anything when we bake. We can abandon the whole parameterized bake window if we can
safely derive the window in gate and trigger modes."* An audit, backed by executable
tests, established the window derives losslessly everywhere except Gate over an active
sustain loop, which has no intrinsic duration to derive. What shipped instead: the bake
window derives itself in Trigger and loop-less Gate; one control, a musical-division
**Hold** picker, covers the one irreducible case and is shown only for Gate-with-active-
loop (`bakeWindowNeedsHold`, reading the engine's own loop fold); bake velocity now reads
the instance's persisted preview velocity rather than a hard-coded value; and the
chrome-row play button stays a pure MIDI trigger — Daniel's ruling: *"play button is pure
MIDI trigger, Bake parameters are their own thing."* **This retires this section's
acceptance criterion 2 ("preview and bake cannot diverge") and its preview-trigger
behavior bullet by ruling, not by shortfall** — there is no popup and no preview trigger.
Three pre-existing truncation bugs were found and fixed along the way, and
`note/CLAUDE.md`'s musical-division-only invariant is amended (see Ξ-W1-T2 above): a note
length now carries either an exact derived duration or a musical division, not
division-only.

---

## Phase Γ — The instrument's control surface

**Ships:** the deck reflowed into two categorical rows with a double-height MASTER bus deck,
a PITCH/RATE deck with playback-rate and baseline-pitch controls, a master limiter with
dynamic reported latency and a real output meter, one consistent knob interaction/taper law
across every variable control **over a stage-time range raised 2 s → 10 s**, a fix for staged
contour traces drawing straight, a re-approached loop/crossfade marker UX under an explicit
chrome-row loop enable, **the Phase Ξ bake's reset list corrected**, and — as the phase's last
track — **the instrument's first VST3 automatable parameters, reported to the host under a
frozen id contract.**

**Consolidates:** none of the seventeen. Phase Γ came from a direct interview with Daniel
(2026-08-01); the product reasoning, the measured layout table, the invariant collisions and
the fork rulings are in **`docs/product/instrument-control-surface.md`**, and the parameter
system's is in **`docs/product/parameter-automation.md` §§6–10**. Read §1.2 (the layout
table) and §7 (collisions) before dispatching any track here — every number in this phase is
derived there, and `docs/TODO.md`'s old deck-rework geometry is superseded.

**Fork state — SEVEN ruled, ONE OF THEM LATER REVERSED, NONE OPEN.** Indexed at spec §8,
folded into the tracks below:
- **Γ-F1** — `kEditorMinHeight` stays **680**.
- **Γ-F2** — the limiter has **lookahead with DYNAMIC reported latency** (zero when off,
  the lookahead when on, reported to host PDC). *This inverted the product recommendation;*
  W1-T2's scope grows accordingly — spec §3.1.1.
- **Γ-F3 — RULED, THEN REVERSED THE SAME DAY.** First ruled *"the ceiling stays 2.0 s in this
  phase"*; then Daniel: ***"extend the stage lengths to 10s."*** `kEnvTimeMaxSeconds` /
  `kGateStageMaxSeconds` move **2.0 → 10.0 in Γ-W1-T1**, and the `docs/TODO.md` entry that
  carried the ambition is discharged rather than deferred. **The reversal's cause is Ruling 1**
  — parameters now ship in-phase, so the ceiling is a one-way door that must be walked through
  before them, not after. Spec §4.3.1.
- **Γ-F4** — there **is** an explicit loop enable, and it lives on the **chrome row**, not
  in a deck. W2-T2's scope grows accordingly — spec §6.4.
- **Γ-F5** — MASTER's reserved slot is **one** cell. The 90 px headroom argument behind
  that is spec §1.6 and governs every future control addition. (**Moved afterwards:**
  Γ-W3-T1 widened the row block, so §1.6's ledger is now 82 px — the ruling and its
  purchasing power are unchanged, only the number.)
- **Γ-F6** — **ship dynamic latency as ruled.** The `restartComponent(kLatencyChanged)`
  deactivate/reactivate the SDK mandates is accepted: *"the limiter will either be on or off
  on its instance, toggling during playback is not a use case."* No constant-latency
  fallback, no measurement gate. *This ruling also corrected the analysis* — spec §3.1.1.
- **Γ-F7 — RULED: SIGNAL FLOW.** Daniel, 2026-08-01: *"signal flow order."* The VST3 parameter
  order — both the frozen id numbering and the `getParameterInfo` presentation index — is
  **PITCH/RATE → PITCH ENV → FILTER → FILTER ENV → AMP → VELOCITY → VOICE → MASTER**, the
  deck's own `sampleDeckGroups` rule, with each group's cells in the semantic order the id
  table freezes. The editor's visual rows after the reflow were the rejected alternative.
  **The reason, because a future reader will ask why the id order does not match the screen:**
  the editor's layout has already moved twice (Θ-W6-T1 grew the floor 840 → 980; Γ-W3-T1 takes
  it to 1198 and re-rows every group) and within-row order is settled by width fitting, not by
  meaning — so **binding a permanently-frozen id order to a demonstrably mobile layout
  guarantees the two drift apart**, after which the order is neither logical nor matching.
  Signal flow is the axis that does not move. Full argument and the accepted residual cost:
  `docs/product/parameter-automation.md` §6.4; **the resulting 44-id table is stated at §6.2.**

**Ruling 1 (Daniel, 2026-08-01) — VST3 parameter reporting ships in this phase.** Verbatim
intent: *"correct the phase gamma plan to account for complying with the VST3 standard for
parameter reporting… by the end of gamma we have the automatable params reported. Make the
parameter order logical."* `docs/product/parameter-automation.md` was written as scoping and
has been **promoted in place**: §§1–5 are the original analysis, **§§6–10 are the
specification** Γ-W4-T1 is built from. Four things it decides that the scoping pass left
open: the blob stays authoritative and parameters are a third surface onto the one model
(§6.1); the id space is an independent, hand-assigned, FOREVER-FROZEN table, **now stated in
full as 44 numbered rows in signal-flow order** (§6.2, §6.3); the exposed list is **derived
from the three-state commit predicate**, never hand-maintained (§7); and, under Ruling 3,
every parameter's **plain unit, range and display precision** (§6.7). Today the plugin has
**zero** parameters — `ReaSamplerProcessor::initialize`
(`reasampler_processor.cpp:56-73`) never populates `SingleComponentEffect::parameters`, so
`getParameterCount()` returns the SDK default 0.

**What the Γ-F6 ruling changed in the analysis, not just in the plan.** Dynamic latency
reporting is **routine** for VST3 instruments and REAPER handles it as a matter of course;
the SDK's deactivate/reactivate requirement (`pluginterfaces/vst/ivsteditcontroller.h:105-108`)
is the normal contract, not an exotic one. What made the cycle expensive **here** was entirely
our own doing: `ReaSamplerProcessor::setActive(true)` called `reloadInstrument()` — a bridge
read plus a full WAV re-decode — where a typical plugin's `setActive` only allocates and frees
buffers. **The cost was therefore ours to reduce, and the reduction was decoupling reload from
activation — not abandoning dynamic latency.** **Landed in Γ-W3**: the activate branch
(`reasampler_processor.cpp:86-132`) now resumes the voice state around a parked `SampleData`
and reloads only when there is nothing to resume from or a bank refresh moved what the park was
decoded from; the deactivate branch parks the PCM and frees everything else. Narrative and
consequences are in `docs/COMPLETED.md`.

**Sequencing against Phase Ξ — the ordering claim is RETIRED and replaced by an owned
correction.** This plan previously asserted that Γ must run before Ξ-W2 and called it *"a
correctness point, not a preference."* **Ξ-W2-T1 ran first.** That was not a decision anyone
took — the track was live before this phase existed (Daniel: *"xi was started before I spun
you up, we'll have to correct phase xi inside gamma. wasn't a choice."*). So:

1. **The bake's reset list is incomplete as shipped, and Γ-W3-T2 amends it.** Rate, pitch
   offset and the limiter flag are all **reset** under Ξ-W2's own ratified rule (spec §3.4),
   so no Daniel decision is owed — only the edit, and it must be made **against what Ξ-W2-T1
   actually shipped rather than against what this plan predicted it would ship.**
2. **Ruling 1 adds a second correction of the same shape, and it lands one wave later.**
   Exposing the reset-class values as VST3 parameters means the bake's reset must notify the
   host, and a host automation lane on a reset-class parameter re-imposes its curve onto
   already-baked audio. Both are Γ-W4-T1's acceptance criteria — that track creates the
   condition, so it carries it (`docs/product/parameter-automation.md` §9).
3. **The payload-ladder half of the old claim needs re-checking, not restating** — see the
   ladder block below, which now states rungs **relatively** rather than by number.

**The organizing constraint.** Six surfaces are single-writer and dictate the wave shape:
`ui/deck_values.cpp` **and the taper module extracted from it** (the taper law and the new
ceiling, then the two new controls, then the host normalization — three tracks, three waves),
`editor_paint_waveform.cpp` (the contour trace, then the loop marks), `ui/deck_groups.cpp`
(the row predicate, then the PITCH/RATE descriptor, then the reflow's row consumption — three
tracks, three waves), `engine/voice.cpp` (the Preserve read path, then the rate compounding
into it), `shell/instrument/reasampler_processor` (the limiter chain and latency, then the
parameter surface), and the params-payload ladder. **Every wave boundary below is one of
those collisions**, not a preference. Where a wave has more than one track, the tracks are
disjoint by surface.

**Resequenced 2026-08-01 (Daniel), three changes.** The prior four-wave shape put the reflow
at W3 and the Preserve stretcher at W4; both moved. **Ruling 1 then added a fourth wave** —
see "The wave shape after Ruling 1" below.

1. **The reflow is split, canvas from arrangement.** The window floor and the width budget it
   is derived from land **early** (Γ-W1-T4), so every other UI track in the phase is drawn,
   tested and judged at the final 1198 × 680 window instead of at a size a later wave changes
   under it. The two-row *arrangement* stays late (Γ-W3-T1), because it can only be measured
   once the final PITCH/RATE and MASTER descriptors exist. The seam is stated at Γ-W1-T4.
2. **`preserve-time-stretch` moved W4 → W1-T5.** It is the longest pole in the phase and has
   **zero dependency on any UI work** — a pure `core/instrument/engine/` module. Scheduling it
   last was a scheduling error. Consequence: it is no longer Rate's *successor* but its
   **prerequisite**, which retires the interim resample-and-cancel stand-in entirely — see
   Γ-W2-T1.

Net: four waves become three, and both of the phase's DSP unknowns (the limiter, the
stretcher) are exposed in wave 1 rather than one of them landing last.

**The wave shape after Ruling 1 — three waves become four.** The parameter system cannot be
a track inside any existing wave, and the reason is a chain of hard prerequisites, not
caution:

- **after W1-T1**, because the taper and the 10 s ceiling *are* the host-facing
  normalization, and Γ-W1-T1 is also what extracts them into the one module the host will
  read through;
- **after W1-T2 and W2-T1**, because every control that could be a parameter must exist
  before the list is declared — the list is derived from the control inventory, and an
  inventory that is still growing produces a list that has to be re-frozen;
- **after W2-T1 specifically**, because `deckParamCommit` becoming three-valued is the
  *prerequisite* of the classification, not an incidental of it: the exposed set is exactly
  `Live ∪ NoteOnLatched`;
- **after W3-T1**, because MASTER's inventory (the limiter toggle, the GR bubble, the
  reserved cell) is the last change to what controls exist at all;
- **after W3-T2**, so the bake's reset list is already complete when Γ-W4-T1 adds the
  host-notification obligation over it — one amendment instead of an amendment to an
  amendment.

The result is a single-track **Γ-W4**, which is the right shape for it anyway: the storage
decision governs every part of the work, exactly as Ξ-W2-T1's crossing decision governs its
chain. **And it satisfies Daniel's own framing literally** — *"by the end of gamma we have
the automatable params reported."*

**The params-payload ladder — re-checked, and now stated RELATIVELY.** The old block named
v14 and v15 as absolutes. **That is no longer safe to assume**, because Ξ ran ahead of its
sequencing and this plan is not the record of what Ξ-W2-T1 actually took. On `dev` today
`kParamsPayloadVersion` is **14** (`map/component_state_io.h:163`) and Ξ-W2-T1 was specced to
take no rung — but the plan's prediction is not evidence. So:

> **Γ owns the next three rungs above whatever `dev` carries when Γ-W1-T2 dispatches, and
> that number is READ, not assumed.** In order: **the first rung to W1-T2** (the limiter
> enable flag), **the second to W2-T1** (rate + pitch offset), **the third RESERVED for
> W4-T1** — spent only if the storage-architecture verification forces a persisted field,
> which the specification says it will not (`docs/product/parameter-automation.md` §6.1,
> §10). If unspent, that rung falls through to the next phase unclaimed.
>
> **On `dev` as of 2026-08-01 that resolves to v15 / v16 / v17-reserved.** Ξ-W3-T1 landed and
> consumed a rung (v14, the bake Hold division) — not Ξ-W2-T1, which took none as specced — so
> every number shifted by one and **nothing else about the ownership changes** — which is the
> whole point of stating it relatively.

**Every other track in the phase owns no rung**: W1-T1 changes no persisted field (the payload
stores raw engine doubles, so both the taper and the new ceiling are persistence-neutral),
W1-T3, W1-T4 and W1-T5 add no field, W2-T2's loop enable maps onto the already-persisted
`SampleLoop::hasLoop`, W3-T1 is layout only, and W3-T2 changes a reset list, not a format.

**The editor's deck is knowingly mis-composed between Γ-W1-T4 and Γ-W3-T1, and that is not a
defect report.** Raising the floor without the reflow leaves the greedy whole-group wrap
packing two ragged left-aligned rows with categorically wrong membership. Γ-W1-T4 states the
exact interim layout; do not "fix" it in a track that does not own it.

**Phase-wide acceptance criteria** (in addition to the ones stated at the top of this file):
- **Bypassed means byte-identical.** With the limiter off, the per-sample output path is
  byte-identical to today's bare ramped multiply — the same discipline that makes
  `live == nullptr` byte-identical to the pre-live core and the filter's exact skip at
  `modAmount == 0` hold the at-rest path unchanged.
- **No `ComponentState` sound change.** A project saved before this phase reopens sounding
  identical: absent rate lifts to 100 %, absent pitch offset to 0 st, absent limiter flag to
  bypassed. Re-tapering a knob **and raising the stage-time ceiling** (both Γ-W1-T1) change
  needle angles only — the payload stores raw engine doubles, so saved values reload
  bit-identical, and a 3 s stage saved at the old ceiling is simply unreachable-by-hand
  rather than altered.
- **`kVelocityPitchRangeSemitones` / `kPitchDepthMaxSemis` (24.0) does not move.** It is
  load-bearing in the v12 wire format. The new Pitch knob **reads** it; it does not mint a
  second ±24 constant. **From Γ-W4-T1 it is also a frozen host normalization** — one more
  reason, not a new rule.
- **The taper has exactly ONE home and three consumers, and from Γ-W4-T1 the taper IS the
  host's `toPlain`/`toNormalized`.** Γ-W1-T1 extracts it into a pure module; the knob's needle
  (`deck_values`), the AHDSR overlay's schematic axis (`envelope_overlay` + `envelope_edit`),
  and the host's `normalizedParamToPlain` / `plainParamToNormalized` (Γ-W4-T1) all call the
  same function. **Three functions that agree today is a defect, not an implementation
  choice** — the failure it prevents is a host automation lane that means one value and a
  needle that draws another. Under Ruling 3 this stops being an analogy: `toNormalized` is
  not *like* the taper, it *is* the taper (`docs/product/parameter-automation.md` §6.7.3).
- **ONE formatter per unit category, and the editor and the host are both its callers.** The
  formatter is pure and returns the **digits** of a plain value in that category's single
  `units` string — no embedded unit, no magnitude-switched unit, no width-conditional
  abbreviation, no caller-side branch. The editor's knob label and
  `getParamStringByValue` read the same function. **The editor and the host printing
  different text for the same stored value is a defect class, forbidden structurally rather
  than caught at review** — same discipline, same reason, as the taper criterion above.
  Consequences (existing formatters stop embedding their unit; cutoff's `k` abbreviation is
  retired; the curve dial's `^` is static cell chrome, not value): spec §6.7.2.
- **Every default value has an EXACT normalized preimage under its own taper.** Binds
  Γ-W1-T1 (which designs the taper) and Γ-W4-T1 (which declares
  `ParameterInfo::defaultNormalizedValue`). `resetDeckParam` bypasses the taper; **a host's
  reset-to-default cannot**, so exactness in the map itself is the only thing that makes the
  editor's reset and the host's reset land on the same value. **Ruling 3 tightens this twice
  and adds one non-requirement** (§6.7.7): `defaultNormalizedValue` is **computed** as
  `toNormalized(default)`, never written as a normalized literal; the assertion is made on
  `toPlain(defaultNormalizedValue)`, the pair the host actually calls; and
  `toNormalized(toPlain(n)) == n` at **arbitrary** n is explicitly NOT required — no log map
  satisfies it in double, and demanding it would over-constrain the taper for nothing.
- **Nothing in this phase may re-map the filter's four normalized controls.** Cutoff, Q,
  morph and drive persist as `*Norm` doubles in payload v9 — their laws are already
  wire-frozen, and re-tapering them would re-tune every saved project independently of
  automation. The snap-unit table names them; that is display, not law. **This does NOT
  conflict with Ruling 3's real-unit requirement**, and the two must not be read as a
  collision: `toPlain` is a pure read-side mapping that never touches the stored value, so
  reporting Hz / Q / drive depth means **calling** `filterCutoffHzFromNorm` and its peers, not
  replacing them — which the editor's own labels already do today. The prohibition forbids
  *editing* those laws; the requirement is satisfied by *calling* them. One additive gap:
  drive has no published inverse and `filterNormFromDriveDepth` must be added beside the two
  that exist — the analytic inverse of a frozen law is not a change to it. Spec §6.7.5.
- **Shift-snap is a drag rule; `stepCount` is a parameter property; they are independent.**
  The editor's snap grid must never be exposed as `ParameterInfo::stepCount` — that would
  quantize the parameter itself, permanently and for the host's automation too, freezing the
  grid into the forever contract and putting continuous cents out of reach from a lane.
  **All 44 exposed parameters ship `stepCount = 0`**, swept and confirmed, and the coincidence
  is structural: every discrete control is reload or rebuild tier and therefore omitted by the
  predicate. Spec §6.7.6.
- **From Γ-W4-T1, the parameter-id table is FOREVER-FROZEN**, on the same footing as the
  extension's `"STABLE_FOREVER_STRING"` command ids, the two VST3 class UIDs, and the
  params-payload field order. No id is reassigned, reused or re-pointed; no exposed
  parameter's normalization ever changes; a retired control's id is retired with it.
  Full wording: `docs/product/parameter-automation.md` §6.3.
- **The exposed parameter set is DERIVED, never hand-maintained.** A control is a parameter
  if and only if its commit class is `Live` or `NoteOnLatched`. There is no second table
  beside `deckParamCommit` / `liveCommitFor`, and no list that can drift from it.
- **The window floor is 1198 × 680 and must not exceed 1280 × 720.** **Γ-W1-T4 set it at
  1190, in wave 1; no other track in the phase may move it** — with ONE ruled exception,
  Daniel 2026-08-02: Γ-W3-T1 widened the row block 1020 → 1028 and the floor 1190 → 1198, so
  the justification law puts both rows' filter edges on one pixel (spec §1.3). That is the
  only reopening, and only these two constants moved. From that point every track is
  authored and judged at it. A track that pushes the floor past 1280 has failed, not overrun.
  **`kEditorMinHeight` stays 680** (Γ-F1). The remaining **82 px of width headroom is the
  budget for the life of this layout** — one deck cell is 60 px, so there is room for exactly
  one more, once. Spec §1.6 states the ledger; read it before adding any control. Chrome-row
  additions are a **separate purse** (they are paid for out of the title slot, not the floor)
  and must not be charged against this one.
- **Reported latency is zero unless the limiter is on.** `getLatencySamples()` returns 0 with
  the limiter bypassed, in every track and at every point in the phase. Only W1-T2 may
  introduce a non-zero value, and only under the limiter-on condition.
- **Geometry stays pure.** Every new layout, cap, label and hit-test rule lands in a pure
  CTest-covered module (`knob_deck`, `sample_bands`, `waveform_view`), never in a painter.

---

### Γ-W1 — Foundations

**Depends on:** nothing in this phase.

**All seven tracks have landed** — Γ-W1-T1 (`knob-interaction-law`), Γ-W1-T2
(`master-bus-audio`), Γ-W1-T3 (`contour-trace-curves`), Γ-W1-T4 (`editor-floor-and-row-law`),
Γ-W1-T5 (`preserve-time-stretch`), Γ-W1-T6 (`exhaustive-switch gate on pure libraries`), and
Γ-W1-T7 (`psola-preserve`) — see `docs/COMPLETED.md` for the full narrative of each. **T6 has
no subsection below**, matching this plan's original choice not to give it one; its record in
`docs/COMPLETED.md` is reconstructed from the CMake change and its enforcement call site
rather than from a spec section here.

#### Γ-W1-T1 — `knob-interaction-law`

**Landed** — see `docs/COMPLETED.md` for the full narrative. One consistent interaction and
taper law across every variable control, landed before Rate/Pitch or any VST3 parameter
existed so both are authored into it rather than retrofitted. The taper is extracted into
its own pure module, `core/instrument/ui/param_taper` — the one home three consumers read
(the knob's needle, the AHDSR overlay's schematic axis and its drag inverse, and — from a
later wave — the VST3 host's `toPlain`/`toNormalized`). Shift snaps to whole units in the
control's displayed category; Ctrl scales the drag by 0.05; Shift+Ctrl resolves to Shift; a
mid-drag modifier transition re-anchors value and cursor position. **The stage-time ceiling
moves 2.0 s → 10.0 s** (`kGateStageMaxSeconds`/`kEnvTimeMaxSeconds`, moved together so they
cannot drift), reversing Γ-F3 on Daniel's later ruling, and every default now has an exact
normalized preimage under its own taper — the requirement Γ-W4-T1's `defaultNormalizedValue`
depends on, since a host's reset-to-default has no `resetDeckParam` bypass to fall back on.
The filter's four `*Norm` controls stay untouched (wire-frozen in payload v9); the change is
persistence-neutral throughout.

#### Γ-W1-T2 — `master-bus-audio`

**Landed** — see `docs/COMPLETED.md` for the full narrative. The master bus: a bypassable
true-peak limiter (baked −0.3 dBTP ceiling, default off, no makeup gain, stereo-linked,
sidechain-only oversampled detection), the meter's audio and publication half, and the
plugin's first latency report — `getLatencySamples()` returns 0 with the limiter off and the
lookahead in samples with it on, driving `restartComponent(kLatencyChanged)` on toggle. New
pure modules `core/instrument/engine/limiter` and `engine/meter_ballistics`. **Spent the
phase's first payload rung: `kParamsPayloadVersion` reaches 15**, appending the limiter
enable flag as a strict suffix.

#### Γ-W1-T3 — `contour-trace-curves`

**Landed** — see `docs/COMPLETED.md` for the full narrative. Staged envelope segments now
draw as the curve their exponent defines, closing the defect where the mid-segment knot
floated off its own trace. A new pure module, `curve_tessellate`, draws every sloped stage
through the same curve the audio's evaluators use, so the drawn stage and the sound it makes
cannot diverge. All three envelopes, both play modes, every sloped stage, share the one fix;
audio is unchanged.

#### Γ-W1-T4 — `editor-floor-and-row-law`

**Landed** — see `docs/COMPLETED.md` for the full narrative. Commits the editor's canvas
ahead of the rest of the phase's UI work: `kEditorMinWidth` moves 980 → 1190
(`kEditorMinHeight` stays 680, Γ-F1), derived from three budget constants — the row block
(1020), MASTER's reserved width (142), and the 1280 ceiling (`kEditorCeilingWidth`, relocated
into `sample_bands.h`) — leaving 90 px of headroom. Row membership becomes a property of the
group id via an exhaustive `deckRowFor(DeckGroupId)` switch (Sound / Contour / Spanning),
consumed by no one yet — **that consumption, and the fit inside the 1020 block, is
Γ-W3-T1's** to assert. No drawing code, descriptor, parameter, or audio changed in this
track. **Superseded in part:** Γ-W3-T1 asserted the fit and found 1020 could not deliver the
tie-line it was chosen for, so the block is now 1028 and the floor 1198 — see that track's
block below.

#### Γ-W1-T5 — `preserve-time-stretch`

**Landed** — see `docs/COMPLETED.md` for the full narrative. A real pitch-preserving
time-stretcher for Preserve mode, moved up from a later wave (Daniel, 2026-08-01) so Rate
ships onto a finished engine instead of a disposable stand-in — the interim
resample-and-cancel path that had been planned for Γ-W2-T1 was not built at all. New
header-only pure module `time_stretch` alongside `pitch_shift`'s existing shift-ratio
control; rate 1.0 is exactly one source frame per output frame with no residue, keeping the
unity-ratio Preserve read bit-identical to the pre-stretch engine. No new third-party
dependency, no allocation/lock in `process()`, no per-sample dispatch.

---

#### Γ-W1-T7 — `psola-preserve`

**Landed** — see `docs/COMPLETED.md` for the full narrative, including the corrected closure
status below. Preserve's splices become pitch-synchronous: a new pure module,
`core/instrument/engine/period_detect` (two-pass YIN), estimates the source's fundamental
period once at load; `pitch_shift`'s splice jump becomes the multiple of that period nearest
the fixed window, so an aligned landing point exists by construction. Detection runs off the
audio thread by link graph — `sampler_core` does not link `period_detect` — and an unknown
period restores the fixed-window geometry byte for byte. A period is derived from the audio
at load, so it is cache, not state: no `ComponentState` field, no payload rung. It gates the
Rate control (Γ-W2-T1) on the plan's own stated principle that Rate must not ship before its
Preserve engine.

**Both failure modes this track set out to close are now closed.** The **geometry** failure
mode (no phase-aligned landing existing inside the search window for low material) closed at
the original merge. The **cadence** failure mode — splices recurring faster than the output
period — was left open at that point, with unresolved review findings from a later review of
a follow-up fold; three remediation commits have since landed and a re-review confirmed the
earlier findings closed. The closing measurement (one-machine, Debug-build) is recorded in
`docs/COMPLETED.md`, not restated here.

---

### Γ-W2 — New controls, and the overlay's marks

**Depended on Γ-W1** for the taper law and the Preserve stretcher (T1), and the contour trace
and the floor/row law (T2) — see `docs/COMPLETED.md` for the full narrative.

**Both tracks have landed** — Γ-W2-T1 (`pitch-rate-deck`) and Γ-W2-T2 (`loop-crossfade-ux`) —
see `docs/COMPLETED.md` for the full narrative of each.

#### Γ-W2-T1 — `pitch-rate-deck`

**Landed** — see `docs/COMPLETED.md` for the full narrative. PITCH became PITCH/RATE: three
knobs (`Key Trk | Rate | Pitch`) under the existing Varisp|Presrv toggle, both new controls
wired through the engine. Rate is 50–200 % on a taper linear in semitones over ±12 (the stated
exception to the centre-expansion law), note-on latched; Pitch is a ±24 st baseline offset,
live. Keytrack × rate × pitch-offset compound into a single read-increment multiply — the
per-sample path gained nothing. Merged as `9dbb8b8`; spent the phase's second payload rung,
v16, as a strict suffix, a v15 blob lifting to rate 100 % / pitch 0 st.

**`isLiveDeckParam` was renamed `deckParamCommit`** and now returns a three-state `LiveCommit`
(`Live` / `NoteOnLatched` / `Reload`) rather than a bool — one predicate widened, not a second
mechanism. **Rate classifies `NoteOnLatched`, Pitch classifies `Live`; Γ-W4-T1 derives the
VST3 exposed parameter set from this predicate**, so the classification is load-bearing two
waves out.

Code review found one Critical (the resample bake read a stale frame window at non-unity Rate;
fixed by deriving the window from the rate the voice actually reads at, proved non-vacuous by
reverting the fix) and folded in a ruling that Pitch, unlike key-track and velocity, is now
compensated against Varispeed's stage-time coupling. A Varispeed golden hash was added,
honestly witnessed from the remediation commit rather than pre-track, so it stands as a
witness for the next track rather than proof of this one.

#### Γ-W2-T2 — `loop-crossfade-ux`

**Landed** — see `docs/COMPLETED.md` for the full narrative. An explicit loop enable, a
legible mark grammar, and the crossfade painted where it is heard. **No format change** — no
`ComponentState` version moved, no new persisted field, `resolveLoop` untouched, audio
unchanged. The enable **is** `SampleLoop::hasLoop`, whose provenance changes from
marker-gesture-derived to user-owned, with the gestures as shortcuts onto it. Merged as
`a8e30a9`. A new pure module, `core/instrument/ui/loop_marks`, holds the state machine
(`resolveLoopMarks`/`applyLoopMarks`); the four marks (START/LOOP/END/XFADE) get one grammar —
line + shaped cap + label, the cap being the grip — with cap/label/suppression geometry pure
and unit-tested. The crossfade moved to `[loopEnd − crossfade, loopEnd)`, drawing as a
top-and-bottom edge wedge, never a second fill; the loop fill's peak alpha stays exactly 0.20.

**START draws in `overlay/trace`, a deliberate deviation from
`docs/product/instrument-control-surface.md` §6.3's table**, because the spec's
`accent/primary` choice would measure 1:1 against the waveform fill it marks.

Code review found three Majors, all fixed before merge. `docs/TODO.md`'s "Pre-existing
staged-envelope-node shadow at zero-attack" entry was resolved incidentally — giving START a
cap is what closed it — and rewritten in place with the recorded outcome by the track itself.
**One thing is deliberately left open, not accepted:** the audible crossfade wedge draws at
1.60:1 against the envelope trace, under the 3:1 floor, with no alpha fix available; Daniel is
judging it visually in the DAW and has not yet ruled. **Neither track has been verified in a
running DAW; both are asserted in CTest only** — 99/99, Debug config, on one machine.

---

### Γ-W3 — The reflow, and the bake correction

**Depended on Γ-W2** for the PITCH/RATE descriptor (W2-T1) — the reflow measured the real
three-cell group rather than a forecast of it, which is the whole reason the arrangement was
scheduled late. **Depended on Γ-W1** for W1-T2's published meter/GR/clip state, which MASTER's
deck draws, and W1-T4's floor, budget constants and row predicate, which T1 consumed rather
than re-derived. **T2 depended on Phase Ξ** — `Ξ-W2-T1 (resample-bake-chain)` landing on `dev`
first, the phase's only external gate — see `docs/COMPLETED.md` for the full narrative of each
landed track.

**Two tracks have landed** — Γ-W3-T1 (`deck-reflow`) and Γ-W3-T2 (`bake-reset-amendment`) — see
`docs/COMPLETED.md` for the full narrative of each. **A third track is now open and has not
run:** Γ-W3-T3 (`bake-prints-limiter`), added below on Daniel's ruling of 2026-08-02.

**None of the three tracks takes a payload rung.** T1 was layout only; T2 changed a reset list,
not a format; T3 changes what the render's audio contains, not what is stored.

#### Γ-W3-T1 — `deck-reflow`

**Landed** — see `docs/COMPLETED.md` for the full narrative. The knob deck's row law stops
being a wrap outcome and becomes a property of the group descriptor: two categorical rows
(Sound — PITCH/RATE, FILTER, VELOCITY, VOICE; Contour — PITCH ENV, FILTER ENV, AMP ENVELOPE)
plus a double-height, right-anchored MASTER bus deck outside both, carrying the limiter enable
toggle, one reserved cell, the output meter column and a passive gain-reduction lamp.
`DeckRow`/`deckRowFor` (`ui/deck_groups`) is an exhaustive switch over every `DeckGroupId`, so a
group added later without a row assignment is a compile error; the greedy whole-group wrap this
replaces is gone entirely. FILTER's `Band|Notch` moves into its caption's previously-unused
second toggle slot, taking the group from 524 to 432 px; VOICE deliberately keeps its
`Retrig|Legato` row toggle, since moving it to the caption would make VOICE wider, not narrower.

**The row block widened 1020 → 1028 px and the editor floor moved 1190 → 1198 px (Daniel's
ruling, 2026-08-02).** The originally specified 1020 could not simultaneously deliver the
filter tie-line (both rows' FILTER/FILTER ENV right edges at one x) and equal,
no-narrower-than-12px gutters on both rows; at 1028 all three hold, with 82 px of headroom left
against the 1280 px ceiling.

The MASTER meter's per-block state moved from a plain overwriting store to an accumulated one —
at 48 kHz/512-frame blocks roughly 47 blocks elapse between two 500 ms UI ticks, and the
overwriting store had been displaying one block in ~47 and dropping the rest. The processor now
folds a per-channel peak max and a limiter min-gain across the whole interval, drained by
`masterBusMeter()`. **The instrument reload was decoupled from VST3 activation as part of this
track** — `setActive(false)` now parks the decoded `SampleData` and destroys only the voice
state, `setActive(true)` rebuilds the voices around the parked sample — discharging the
`docs/TODO.md` follow-up already recorded in full there. The limiter toggle's commit is split so
the audible state commits inline on the click and only the host's
`restartComponent(kLatencyChanged)` notification is deferred to the editor's existing 500 ms
sync tick.

**Not verified in a running DAW — CTest-asserted only:** the meter at its 500 ms UI cadence, the
GR lamp under real limiter action, the limiter toggle's latency renegotiation, the clip cap's
click-to-clear, and the recapture-while-editor-closed path.

#### Γ-W3-T2 — `bake-reset-amendment`

**Landed** — see `docs/COMPLETED.md` for the full narrative. The correction Phase Γ owed Phase
Ξ: Ξ-W2-T1's bake shipped ahead of the sequencing this plan asserted, so its reset list predated
rate, pitch offset, the limiter enable, and the loop enable. **`resetAfterBake` needed no code
change** — all four already reset by construction, since the function defaults everything and
copies back only survivors, and none of the four was ever named a survivor. The track shipped
field-by-field assertions over two independently-dialled fixtures, never struct equality, plus a
mutation-verified spot-check sweep confirming both fixtures actually moved every asserted field
off its default.

**One invariant correction:** `bake/CLAUDE.md` had claimed the whole signal chain prints, master
gain included. It doesn't — the render's gain multiply is the only master-stage value it
prints; the limiter runs in the processor's block, off the bake path entirely.

**Outstanding, not closed by this track.** A capture baked with the limiter engaged comes back
unlimited — a real audible gap, and Daniel has ruled that a future track will change the bake to
print the limiter. **That track is now Γ-W3-T3, below.**

**Neither track has been verified in a running DAW; both are asserted in CTest only.**

#### Γ-W3-T3 — `bake-prints-limiter`

**Not started. Opened by Daniel's ruling, 2026-08-02.**

**Goal.** Print the limiter through the bake's master stage, so a capture baked with the
limiter engaged returns limited audio rather than unlimited audio.

**Why this exists.** Γ-W3-T2's own finding disproved the premise
`docs/product/instrument-control-surface.md` §3.4's reset classification rested on:
`renderBake` (`core/instrument/bake/bake_render.cpp`) prints only a flat master-gain multiply,
and the limiter (`core/instrument/engine/limiter`) runs in the processor's `process()` block,
off the bake path entirely. Until this track lands, this is a recorded, known limitation — see
`docs/COMPLETED.md`'s Γ-W3-T2 entry — not an oversight.

**Consolidates:** nothing from the seventeen. A correction, on the same footing as Γ-W3-T2 (see
"Work in this plan that is not one of the seventeen").

**Spec:** none yet written. This ruling postdates §3.4 and has no product-doc section of its
own; §3.4 is superseded on this one point, which a future scoping pass of this track should
correct there as well as here.

**Surface boundary — likely, not yet confirmed against a full scoping pass:** owns
`core/instrument/bake/bake_render` (the gain-multiply step, extended to also run the signal
through a limiter), consuming `core/instrument/engine/limiter` — not owned, not modified. Does
not own the processor's live block, the limiter DSP itself, the parameter surface, or
`bake_reset` (the limiter-enable reset classification is already Γ-W3-T2's, landed).

**Open questions — none of this is ruled yet, only the goal is:**
- **[propose at review]** Whether the bake instantiates its own `Limiter` — mirroring
  `renderBake`'s existing bake-only `VoiceEngine`, off the audio thread, never linked into
  `reaper_reasampler` — or reaches the limiter's settled behavior some other way. The
  bake-only-engine precedent (`bake/CLAUDE.md`) argues for the former.
- **[propose at review]** Whether the limiter's lookahead needs any accommodation in an
  offline, non-realtime render — the processor's `getLatencySamples()` PDC report exists for
  the live block, and a bake is not on that clock, so this may be a non-issue; it has not been
  checked.
- **[propose at review]** Whether this track also corrects `bake/CLAUDE.md`'s invariant text
  ("the limiter is not [printed]") alongside the code, once scoped in full.
- **No [Daniel] question on the goal itself** — the ruling above is the goal; what is open is
  the mechanism, not whether to do it.

---

### Γ-W4 — VST3 parameters

**Depended on every earlier wave in this phase** — W1-T1's taper module (the host-facing
normalization itself), W1-T2 and W2-T1's control inventory and three-state commit
predicate (the exposed set is exactly `Live ∪ NoteOnLatched`), W3-T1's final MASTER
inventory, and W3-T2's completed bake reset list — each a hard prerequisite rather than a
courtesy. See `docs/COMPLETED.md` for the full narrative of each.

**The track has landed** — Γ-W4-T1 (`vst3-parameter-set`) — see `docs/COMPLETED.md` for
the full narrative. One track: the storage decision governed every part of the work, so a
pure/host-wiring split would have been serial and bought no concurrency.

#### Γ-W4-T1 — `vst3-parameter-set`

**Landed** — see `docs/COMPLETED.md` for the full narrative. The instrument now reports
its automatable parameters to the host under a FOREVER-FROZEN `ParamID` table (blocks of
100 per deck group in signal-flow order, steps of 10 within a block, a curve dial at its
outer knob's id + 1) — 44 of 44 issue, derived from `deckParamCommit` / `liveCommitFor`
rather than hand-maintained, each with real units, a plain range and display precision at
the host boundary. A new pure module, `core/instrument/param`, holds the id table, the
plain-value layer, the one formatter per unit category, and the audio thread's
block-boundary merge decision; `shell/instrument/instrument_params` adapts it onto
`Steinberg::Vst::Parameter` and decides nothing.

**Both VST3 delivery channels are serviced** — the controller's `setParamNormalized` and
the audio thread's `IParameterChanges` drain, mirroring the SDK's own
`SingleComponentEffect` sample rather than the controller-only delivery an earlier pass
shipped. **A host automation point's authority is bounded**, not permanent: it outranks
the model only until the UI thread folds it in and republishes — at most one UI tick —
never a later restore, bake reset or knob move. `shell/instrument/CLAUDE.md`'s "THE
AUTHORITY MODEL" section states it, `param_merge` enforces it.

**Two rulings, both Daniel, 2026-08-02.** Pitch key-track and Trigger length promote from
`Reload` to `NoteOnLatched` — the promotion that takes the count to 44 of 44 and issues
ids 1000 and 1450, and which needed `LiveValues`/`foldLive` widened and `Voice::start`
handed the two latched values, not just a predicate flip; the new `param::valueHomeFor`
guard closes the class of bug the promotion exposed. And the curve-shape dials' ±0.01
snap-to-centre band now applies on the mouse-drag path only, never on a host-facing map.

**The bake's reset now notifies the host; the double-processing limitation (a host
automation lane outranking the reset) is a named, documented boundary, not discovered
later.** `IMidiMapping`, `IParameterFunctionName` and `IAutomationState` are all assessed
and NOT implemented.

**Not verified in a running DAW — CTest-asserted only.** `docs/TODO.md` carries the
residual DAW-verification items.

---

## Phase Ψ — The extension trust pass: exact bounds, disjoint solo surfaces, reachable actions, honest drops, real names, true mono

**Ships:** capture ranges that mean what was asked — a time selection or razor area over
a longer item captures the selection, both scopes; per-mode solo surfaces that cache,
clear, and restore across the Design/Arrange switch, with the switch itself refused while
the transport runs; the Media-Explorer import action registered into the Media Explorer
action section so it can live on that toolbar; a drag-out gesture law that resolves its
target from what is actually under the cursor, continuously and reversibly, with no
silent no-op release anywhere; captures labeled after their source track instead of the
literal `"item"`/`"track"`, surfaced on the panel card; and lossless mono collapse for
new captures whose channels are bit-identical.

**Consolidates: none of the seventeen.** Phase Ψ came from a direct list of seven defects
and refinements (Daniel, 2026-08-01). There is no backing product doc — the design
content lives inline in the tracks below, and the seven are recorded here as the phase's
provenance, cited throughout as Ψ.1–Ψ.7:

> **Ψ.1** — item and track captures should be named (labeled) after their source track
> name, plus a discriminator (date, etc.); currently they aren't named anything useful.
> **Ψ.2** — Design vs. Arrange modes: any SOLO state in one mode is cached and removed
> when switching to another mode, disjoining the solo surfaces.
> **Ψ.3** — the Design/Arrange active-mode toggle is gated if playback is running; only
> allow the switch when the project is not playing.
> **Ψ.4** — the action that moves a Media Explorer item to a new ReaSampler on the
> selected track is not in the Media Explorer action category, so it cannot be added to
> the Media Explorer toolbar; fix this.
> **Ψ.5** — drag-and-drop targets are inexact: sometimes dropping into the arrange
> doesn't work, sometimes dropping into the FX area doesn't work; dragging between banks
> is fine.
> **Ψ.6** — capture into mono: if the left and right channels of a new capture are
> bit-identical, collapse to mono — one channel of data, mono arrange items, ReaSamplers
> load in mono mode.
> **Ψ.7** — capture item / capture track with a small time selection on a large item
> captures the entire item, not the selection/razor; make capture regions consistent and
> correct.

**Where the seven land:**

| Ψ-item | Track | Worktree slug |
|---|---|---|
| Ψ.7 | Ψ-W1-T1 | `ppsi-w1-t1-capture-range-exactness` |
| Ψ.2 + Ψ.3 | Ψ-W1-T2 | `ppsi-w1-t2-mode-switch-discipline` |
| Ψ.4 | Ψ-W1-T3 | `ppsi-w1-t3-media-explorer-section` |
| Ψ.5 | Ψ-W1-T4 | `ppsi-w1-t4-drop-target-resolution` |
| Ψ.1 | Ψ-W2-T1 | `ppsi-w2-t1-capture-naming` |
| Ψ.6 | Ψ-W2-T2 | `ppsi-w2-t2-mono-collapse` |

**Ψ-W3 is not one of the seven** — opened mid-phase, after Ψ-W2's review surfaced that
the track scope carried the same multi-track stem-collapse hole Ψ-W1-T1 had just closed
for item scope. It consolidates none of the original seven and carries no `Ψ-item` row
above; see Ψ-W3 below.

**All three waves have landed — Phase Ψ is complete.** W1 and W2 each carry their own
landed notes below; W3 carries its own too. See `docs/COMPLETED.md` for every track's
full narrative. **None of the seven tracks is DAW-verified** — all are code-complete and
unit-tested, several resting on a shared unverified inference about how REAPER's
selected-tracks render source interacts with custom time bounds, which Ψ-W3-T1's
refusal now also rests on; each track's DAW-verification obligation is restated inline
below.

Ψ.2 and Ψ.3 share one track deliberately: they share one chokepoint — `applyMode`, the
sole mode mutator (`shell/view/view.cpp:380-469`) — and one discriminator
(`targetModeId != model.activeModeId()`, the test that distinguishes a real switch from a
reapply). Splitting them is two tracks fighting over the same function.

**Concurrency with Γ and Ξ.** Phase Ψ is extension-side. Γ and Ξ-W3 live in
`core/instrument/` + `shell/instrument/`; the surfaces are disjoint with ONE named
exception — Ψ-W2-T2 touches `shell/instrument/processor_reload.cpp` for a stale comment
and an index/file consistency check, and that touch is bounded to the minimum in its
surface boundary precisely because Γ is live in that directory.

**Three invariant amendments were DELIVERABLES of this phase, not asides.** Each landed
in its owning track, as an acceptance criterion of that track:

1. **The never-touch-solo rule** (Ψ-W1-T2): `src/shell/view/CLAUDE.md:14-17`,
   `src/core/view/CLAUDE.md:10`, and `docs/product/design-view.md:160-165` + `:588-592`.
   Ψ.2 requires writing `I_SOLO`; the amended form is stated in the track. A track that
   lands solo writes without the amendment reads as an invariant breach in review.
2. **The action-registration contract** (Ψ-W1-T3): root `CLAUDE.md`
   §"REAPER extension contract" documents only the main-section 4-step pattern; the
   second, non-main mechanism (`custom_action` + `hookcommand2` + `-custom_action`
   mirror) must be added beside it.
3. **The channel-count-preserved rule** (Ψ-W2-T2): root `CLAUDE.md:208` — "channel count
   preserved (no silent stereo fold)" — is contradicted in text (not in spirit) by a
   lossless bit-identical collapse, and was already untrue in the other direction (a mono
   source renders at `RENDER_CHANNELS=2` today). The amended form is stated in the track.

**Performance posture.** Every Ψ surface is cold — per-capture, per-click,
per-mode-switch, per-mouse-move. None of the named hot paths (peaks envelope compute,
audition, realtime-capture tick, instrument `process()`) is touched. Two disciplines
carry anyway: the realtime tick's single-pointer-test idle fast path is untouched by
Ψ-W1-T1's render work, and Ψ-W1-T4 keeps the inside-client drag path free of SDK
hit-tests, exactly as today (`panel_drag.cpp:273-274` — "costs nothing on the common
internal-drag path").

---

### Ψ-W1 — Exact bounds, disciplined switches, reachable actions, resolved drops

**Depends on:** nothing in this phase.

#### Ψ-W1-T1 — `capture-range-exactness`

**Landed** — see `docs/COMPLETED.md` for the full narrative. A ranged item capture now
renders exactly the requested window instead of the whole item, by re-sourcing through
the selected-tracks render when the item extent does not already print the window.
**Deviation:** the spec named two candidate architectures (re-source vs. render-then-
trim); the engineer shipped a conditional form of the re-source candidate — the
full-extent case runs literally unchanged code, keeping the byte-identity regression
floor structural and the fix cheap to revert if the override inference proves wrong.
Also landed: a transient isolation guard (cutting `B_MAINSEND` on direct folder
children, muting receives) so an item capture stays true to item scope, and a
post-render frame-count gate (±1 tolerance, tail-None only) that refuses a widened
render and retains it outside the bank for diagnosis rather than deleting it. New modules `core/capture/render_window`, `core/capture/track_topology`,
`shell/capture/render_selection`, `shell/capture/render_isolation`. The whole fix rests
on the unverified inference that REAPER's selected-tracks render source overrides
custom time bounds — Ψ-W3-T1 (below) now also depends on it. **DAW-verification
obligation, unmet:** the four-cell scope × selection-type matrix over a source item
substantially longer than the selection (tail None), plus one razor-union case — none of
it run in a live REAPER session yet.

#### Ψ-W1-T2 — `mode-switch-discipline`

**Landed** — see `docs/COMPLETED.md` for the full narrative. Per-mode SOLO surfaces now
cache, clear, and restore across a Design/Arrange switch, with the switch itself
refused, visibly, while the transport is playing or recording. The never-touch-solo
invariant — stated three times (`src/shell/view/CLAUDE.md`, `src/core/view/CLAUDE.md`,
`docs/product/design-view.md`) — is amended, in this track, to the snapshot sense of
non-destructive: solo is cached per mode on a real switch and restored verbatim, not
left untouched absolutely the way `B_MUTE` and the master track are. Also closed: a
pre-existing bug where a footer mode-segment click never persisted view state. New
`core/view/solo_cache`, `shell/view/view_solo`. **DAW-verification obligation, unmet:**
the disjoint-surface solo matrix, the playback-gated refusal (playing and recording),
and the panel-persist case — none run in a live REAPER session yet.

#### Ψ-W1-T3 — `media-explorer-section`

**Landed** — see `docs/COMPLETED.md` for the full narrative. The Media Explorer import
action now registers into REAPER's Media Explorer action section (32063) via
`custom_action` + `hookcommand2`, alongside its existing Main-section entry so existing
keybindings survive — a second FOREVER-STABLE id, `INGEST_IMPORT_MEDIA_EXPLORER_MX`,
minted per channel. Root `CLAUDE.md`'s "REAPER extension contract" is amended, in this
track, with the second, non-main registration mechanism beside the original four-step
main-section pattern. **DAW-verification obligation, unmet:** adding the action to the
Media Explorer toolbar and firing it from there; confirming the Main-section binding
still fires; and confirming unload/reload does not leak a duplicate Media Explorer entry
(the `-custom_action` unload mirror is unconfirmed against the SDK header) — none run in
a live REAPER session yet.

#### Ψ-W1-T4 — `drop-target-resolution`

**Landed** — see `docs/COMPLETED.md` for the full narrative. The drag-out gesture is now
a per-move, stateless law: target class resolves from what is under the cursor on every
move, every class transition is reversible until release or until the pointer leaves
REAPER, and the OS hand-off is reserved for leaving REAPER entirely — the whole TCP/MCP
is now an instrument-drop hotspot, and a single-card arrange drop lands a timeline item
at the pointer's track and time. New `shell/actions/arrange_drop_win`.
**DAW-verification obligation, unmet:** the full target-class matrix (single- and
multi-card), reversibility across a drag that crosses the arrange en route to an FX
window, drag-speed independence, and the narrow-TCP case — none run in a live REAPER
session yet.

---

### Ψ-W2 — Names and channels, over the settled render block

**Depends on Ψ-W1 — specifically Ψ-W1-T1**, which rewrites the capture
render-configuration block in `shell/capture/capture.cpp` and
`core/capture/render_settings.cpp`; both W2 tracks edit adjacent regions of those same
TUs, so W2 dispatches only after W1-T1 is on `dev`. (T2–T4 of W1 gate nothing here; the
wave boundary is the file collision, not a semantic dependency.)

#### Ψ-W2-T1 — `capture-naming`

**Landed** — see `docs/COMPLETED.md` for the full narrative. Captures are now named
after their source track's name plus a discriminator
(`<Track> [+N] [#ordinal] MM-DD HHMM`) at every interactive mint site, with the name
shown on the panel card over a scrim clearing the 4.5:1 contrast floor. New
`core/capture/capture_name`. Recapture, ingest, and the bake deliberately keep their own
naming — the bake's naming-and-lineage open question stays Ξ-W1-T1's/Ξ-W2-T1's to close,
untouched here. **DAW-verification obligation, unmet:** capturing from a named track, an
unnamed track, and a multi-item selection, and confirming the labels show on the card
and in the instrument — none run in a live REAPER session yet.

#### Ψ-W2-T2 — `mono-collapse`

**Landed** — see `docs/COMPLETED.md` for the full narrative. A capture whose channels
are bit-identical now collapses losslessly to one mono channel, written via temp file
plus atomic rename, with `Sample::channelCount` measured off the landed file rather than
echoed from the request on every capture path — including realtime, which previously
parsed the layout and echoed the request anyway. Root `CLAUDE.md:208`'s
channel-count-preserved invariant is amended, in this track, to the landed wording:
channel count preserved, except that bit-identical channels may collapse losslessly to
mono; a lossy fold remains forbidden. The `shell/instrument/processor_reload.cpp` touch
stayed to the minimum named in its surface boundary (Phase Γ was live in that
directory). **Open, not closed here:** whether bake landings collapse too — `[propose]`,
leaning yes, still deferred to be confirmed against what Ξ-W2-T1 actually shipped.
**DAW-verification obligation, unmet:** capturing a dead-center mono source and a
true-stereo source, inserting both, loading both into the instrument, and running the
null test on the collapsed one (REAPER's mono-item-on-stereo-track summing at unity is
the specific thing to confirm) — none run in a live REAPER session yet.

---

### Ψ-W3 — Closing the track-scope stem-collapse hole

**Depends on Ψ-W2 for:** existing at all — this wave did not exist when the phase was
scoped. Daniel opened it after Ψ-W2's review surfaced that the track scope carried the
same multi-track stem-collapse hole Ψ-W1-T1 had just closed for item scope.

**One track. Consolidates none of the seven** — it came from a review finding, not from
Ψ.1–Ψ.7.

#### Ψ-W3-T1 — `track-scope-range`

**Landed** — see `docs/COMPLETED.md` for the full narrative. Any multi-track
selected-tracks render now refuses, in both scopes, keyed on the render *source* rather
than the capture scope — closing the hole Ψ-W1-T1 left open for track scope. Realtime
deliberately diverges and was left untouched, because it sums correctly. The refusal
rests on the same unverified inference Ψ-W1-T1 rests on — that REAPER's selected-tracks
render source overrides custom time bounds — so if that inference is wrong, this refusal
costs a working capture. `docs/verify-track-scope-multitrack.md` is a new standalone
verification script on this branch, for this track's multi-track refusal specifically.
**DAW-verification obligation, unmet:** confirmed nowhere in a live REAPER session yet.

---

## Phase Ε — The bank package: one file that carries a bank between projects

**Ships:** a bank exported to a single version-tagged `.rsbank` file — audio bytes
byte-exact, index metadata intact — and imported into another project's bank folder and
index, with a compatibility policy that names both directions concretely: an older package
in a newer build always imports, a newer package in an older build refuses whole with an
actionable message, and neither direction is ever a partial landing.

**Consolidates: none of the seventeen.** Phase Ε came from a direct request (Daniel,
2026-08-02) and is scoped in `docs/product/bank-package.md`. Nothing in `docs/TODO.md` or
`docs/TODO-1.0.md` records export, import, or a package format — a sweep of `docs/` for
`export|package|portable` returns only unrelated matches (prune's portable move-to-trash,
MIDI-playback prose) — so this phase **supersedes nothing and absorbs nothing.**

**Three forks were opened at framing and all three are RULED** (Daniel, 2026-08-02). The
rationale for each — including why two went against the framing recommendation — is
recorded at `docs/product/bank-package.md` §"Rulings"; the binding specification for each
is in the section of that doc named below.

| Fork | Ruling | Specified in | Bound into |
|---|---|---|---|
| **Ε-F1** | **Proprietary container** — the hand-rolled `RSBK`. ZIP via the vendored MiniZip64 (`vendor/WDL/WDL/zlib/`) and a hand-written stored-only ZIP shape are both rejected and are not to be revisited in this phase | §"The container" | Ε-W1-T1 |
| **Ε-F2** | **Import always lands as a new bank**, with an automatic numeric suffix on a display-name collision — no prompt, no overwrite. Merge-into-existing is **out of scope for Phase Ε**, not deferred | §"Identity and collision on import", incl. the auto-suffix rule | Ε-W2-T2 |
| **Ε-F3** | **Refuse** the import when the tracking ledger is degraded. No confirm-and-proceed path, no opt-out | §"Import under a degraded tracking ledger" | Ε-W2-T2 |

**No Ε track is gated on a decision.** Every track in this phase is dispatchable as
written.

### Phase-Ε acceptance criteria

These bind every track in this phase, in addition to the plan-wide set above.

- **Byte-exact round-trip is the phase's trust anchor.** Export → import → export yields
  byte-identical payloads, and the `hashBytes` digest of every landed file equals the digest
  recorded at export. Frame count, sample rate, bit depth, and channel count are untouched on
  both sides. **No re-encode anywhere:** `wav_codec` may be called to hash and to read
  metadata already recorded, never to rebuild, trim, normalize, or collapse. The mono collapse
  is a capture-path behaviour and must not reach the import path — the same exclusion ingest
  already carries (root `CLAUDE.md`, exact-bounds invariant).
- **The format cannot express a path.** Manifest entries are bare file names — no directory
  component, no `..`, no drive letter, no leading separator — validated on encode *and*
  decode. Relative-paths-only becomes structural rather than remembered, and the
  archive-traversal bug class closes by construction.
- **Import places no timeline item.** Capture and placement stay separate acts; import is a
  capture-shaped act, not a placement one.
- **Import never overwrites and never deletes an existing bank-folder file.** The one
  deletion path is the rollback of files *this call wrote* that no index ever referenced —
  the documented carve-out at `src/shell/persist/prune_fs.cpp:5-11`, which every track
  touching it must **cite, not restate**.
- **Export is read-only against the project.** No ext-state write, no `bumpBankGeneration()`,
  no undo point. Import does the opposite: it bumps the generation
  (`src/shell/persist/session.h:108`) so live ReaSampler 9000 instances reload, and batches
  its index mutation into one Ctrl-Z through `persistBankOp`.
- **All-or-nothing on both sides.** No partial export, no partial import. A truncated
  `.rsbank` must never exist on disk (temp file + atomic rename, the Ψ-W2-T2 precedent); a
  half-imported bank must never exist in the index (rollback).
- **At most one entry's payload in memory at a time**, on both paths. The pure codec owns
  framing and offset arithmetic; the shell owns the stream. A whole-package
  `vector<uint8_t>` on either side is a rejected shape, not an optimization opportunity.
- **The pure planners take value inputs, never a handle.** The decoded manifest, the
  destination `BankBook`, and the set of names present in the bank folder cross the seam as
  values. No `ReaSamplerSession&`, no service container, no "pass the thing that has
  everything" reaches `core/package/`. If a circular dependency appears during the build, the
  fix is a service split or a thin interface — **not** parameter propagation, and **not** a
  base class gaining a dependency that grows its subclasses' constructors.
- **Every pure module gets a `<module>_tests` target** that runs without REAPER or a DAW.
  The whole collision/version rule set is expressible as pure functions over strings and
  hashes; if a rule can only be tested through the shell, the seam is in the wrong place.

**Performance posture.** Every surface in this phase is cold — per-gesture, once. None of
the named hot paths (peaks envelope compute, audition, realtime-capture tick, instrument
`process()`) is touched by any track here. The one performance fact that *is* load-bearing is
the memory criterion above, and it is stated as a structural constraint rather than a
guardrail because exceeding it does not slow the feature down, it makes it fail.

**Concurrency with Γ and Λ.** Phase Ε is extension-side and lands almost entirely in **two
new directories** (`src/core/package/`, `src/shell/package/`) that no other phase touches.
Γ lives in `core/instrument/` + `shell/instrument/`; Λ is being specced concurrently and is
not read here. The only pre-existing files any Ε track edits are named per track below —
`core/tracking/origin_ledger` (W1-T3, exclusively), the root `CMakeLists.txt`
`add_subdirectory` list (W1-T1 and W1-T2, one line each), `src/app/main.cpp` and the panel's
bank menu (W2-T1 and W2-T2, one registration line and one menu row each), and
`core/model/bank_book.{h,cpp}` (W2-T2 only — **one additive public `const` member**, required
by the Ε-F2 auto-suffix rule so the name fold keeps its single home). **No Ε track
touches `core/instrument/`, `shell/instrument/`, or any capture backend.**

---

### Ε-W1 — The contract, the filesystem, and the ledger's new kind

**Depends on:** nothing in this phase. **Three tracks, disjoint by directory** — the split is
by *what each track's inputs are*, which is why they genuinely parallelize: T1 knows only
bytes and structs, T2 knows only paths and bytes, T3 knows only the ledger.

| Track | Owns |
|---|---|
| **T1** `package-format` | the whole of the new `src/core/package/` **except** `export_plan` / `import_plan` (W2's), plus its `CMakeLists.txt` and `CLAUDE.md` |
| **T2** `package-fs-shell` | the whole of the new `src/shell/package/` **except** `export_bank` / `import_bank` (W2's), plus its `CMakeLists.txt` and `CLAUDE.md` |
| **T3** `import-origin-kind` | `src/core/tracking/origin_ledger.{h,cpp}` and its tests, exclusively |

**One shared file in the wave, named rather than discovered at merge:** the root
`CMakeLists.txt` `add_subdirectory` list — T1 appends `src/core/package`, T2 appends
`src/shell/package`. Two append-only lines in one list: **textual merge adjacency, not
semantic contention.** Whichever lands second rebases.

**All three tracks are dispatchable now.** T1 carries the Ε-F1 ruling — the container is the
hand-rolled `RSBK`, decided, not a candidate T1 chooses among. T2 and T3 never depended on
that ruling anyway: T2's API is bytes-in/bytes-out regardless of what those bytes mean, and
T3 touches no package code at all.

#### Ε-W1-T1 — `package-format`

**Goal.** The container and its version ladder, entirely pure — the contract every later
track consumes, landed once so nothing downstream re-litigates the shape.

**Spec:** `docs/product/bank-package.md` §"The container", §"Version tagging", §"What a
package carries", §"What a package deliberately does NOT carry", §"Memory".

**Surface boundary — owns:** new `src/core/package/package_format` (the magic, the header
layout, `kPackageFormatVersion`, `kPackageMinReaderVersion`, and
`classifyPackageVersion(formatVersion, minReader) -> Readable | TooNew | Malformed`), new
`src/core/package/package_manifest` (the manifest model + its JSON codec), new
`src/core/package/bank_package` (header encode, prefix decode, entry-layout arithmetic), the
directory's `CMakeLists.txt` and `CLAUDE.md`, and one appended `add_subdirectory` line in the
root `CMakeLists.txt`. **Does not own:** `export_plan` / `import_plan` (Ε-W2), anything under
`shell/`, `core/model`, or `core/tracking`.

**Behavior.**
- **The container is the hand-rolled `RSBK` (Ε-F1, ruled).** Magic `RSBK`, a fixed
  little-endian header carrying the two version fields, a length-prefixed JSON manifest, then
  each entry's payload concatenated in manifest order. Framing is built on `core/wire/bytes.h`
  (`putLE` / `ByteReader`) and the manifest on `core/json` — both already owned and tested
  here. **No ZIP, no compressor, no new third-party source in the build**; a link edge to
  `vendor/WDL/WDL/zlib/` means the ruling was misread.
- **Two version integers, not one.** `formatVersion` = what this writer emitted;
  `minReaderVersion` = the oldest reader that can read it safely. The reader's whole rule is
  `minReaderVersion <= kPackageFormatVersion`. An **additive** change (a new optional
  manifest key, a new `Sample` field with a defined absent-value) bumps `formatVersion`
  only; a **structural** change bumps both. **Growing a persisted enum's vocabulary is
  structural here, not additive** — `BankModel::deserialize` *rejects* an out-of-range
  `SourceMode` or `Tier` rather than degrading it (`bank_model.cpp:232-239`, `:339-346`),
  and every enum a package carries rides inside the nested `BankModel` blob, so a new
  `SourceMode` or `Tier` value bumps both integers. The header carries the writer's semver
  (`version::stampVersion()`) alongside them, informational, so a refusal message can name
  what to install.
- **The ladder is documented the way `origin_ledger.cpp:8-21` documents its own** — a header
  comment listing every shipped version and what changed, with the read-and-validate rule
  stated, not implied.
- **Unknown manifest keys are skipped** (the `bank_book_json.cpp:182` behaviour), and
  **unknown persisted enum integers — the manifest's own, not `BankModel`'s nested ones, which
  reject per the bullet above — degrade to their defined `Unknown` equivalent**, never to
  the numeric default and never to a parse failure (`core/wire/CLAUDE.md`'s `BakeStatus` rule,
  verbatim). Both are pinned by tests, not left to inheritance.
- **The manifest nests `BankModel`'s own serialization verbatim**, exactly as
  `bank_book_json.cpp:15-20` nests it, so per-sample shape has one owner and a future
  `Sample` field reaches packages for free. Per entry the manifest adds only: the bare file
  name, the byte length, and a `hashBytes` digest (`core/capture/wav_codec.h:143`) —
  `hashBytes`, **not** `hashWavContent`, because the latter deliberately skips chunks
  (`wav_codec.h:145-151`) and so cannot answer "did these bytes survive."
- **The bank's `slot_map` rides along** — display positions are part of what the user built.
- **Framing only, never a payload.** `bank_package` produces the header bytes and an ordered
  `[{ name, offset, length }]` layout; it never holds, copies, or hashes an entry's audio.
  Decode is symmetric: prefix in, manifest + layout out.
- **Path expression is structurally impossible.** Entry names are validated to contain no
  `/`, `\`, `:`, no leading separator, and no `..` component, on both encode and decode.

**Acceptance criteria.**
- `decodePackage(encodePackage(x)) == x` over a manifest fixture exercising every field,
  including every `Sample` optional in both present and absent states.
- A synthetic header with `minReaderVersion` above this build classifies `TooNew` and **no
  manifest is produced** — the decode does not half-succeed.
- A synthetic header with `formatVersion` above this build but `minReaderVersion` at or below
  it classifies `Readable`, and its unknown manifest keys are skipped without error. This is
  the additive-forward-compatibility claim, and it is the reason the two-integer design
  exists; a test that does not exercise it leaves the design unproven.
- Truncated input at every byte offset in a valid package returns `Malformed` — never UB,
  never a partial manifest, never a read past the buffer. Hostile-input hardening at the
  `bank_model.h:204-206` standard.
- Entry names containing `..`, a separator, or an absolute prefix are rejected on encode
  *and* rejected on decode. Both directions, because a package can arrive from anywhere.
- No file in the new directory exceeds ~600 lines; the three-module split above is the
  responsibility seam, and a fourth module is preferred over a bisection if one is needed.
- `package_format_tests`, `package_manifest_tests`, `bank_package_tests` all run without
  REAPER or a DAW.

**Open questions.** **No [Daniel] questions — Ε-F1 is RULED** (proprietary `RSBK`), so this
track is dispatchable as written. **[propose at review]** whether `package_format` and
`bank_package` are genuinely two modules or one — the split is proposed on responsibility
grounds (constants and classification vs. offset arithmetic) and may collapse if the
arithmetic turns out to be twenty lines.

#### Ε-W1-T2 — `package-fs-shell`

**Goal.** Every filesystem and dialog act the two verbs need, landed behind an API that knows
nothing about what a package contains — so it can be authored, reviewed, and tested in
parallel with the format it will carry.

**Spec:** `docs/product/bank-package.md` §"Where it lives", §"Failure modes", §"Memory".

**Surface boundary — owns:** new `src/shell/package/package_io` (read a file's bytes, write
bytes through temp + atomic rename, read one bank file, write one landed file, enumerate the
bank folder's existing names, and the rollback delete), the file-picker seam for both verbs —
**one picker, REAPER's own, on every platform**, so this track carries **no**
`#ifdef _WIN32` / `#else swell/swell.h` split. That split is a real pattern in this codebase
(`src/shell/panel/draw_kit.cpp:11-15`, `src/shell/persist/prune_fs.cpp:35-38`); it is simply
not this track's shape, because REAPER owns the dialog. Also owns the
directory's `CMakeLists.txt` and `CLAUDE.md`, and one appended `add_subdirectory` line in the
root `CMakeLists.txt`. **Does not own:** `export_bank` / `import_bank` (Ε-W2), anything under
`core/`, and — emphatically — `prune_fs`, which stays the deletion authority.

**Behavior.**
- **Atomic write.** A package is written to a temp path in the destination directory and
  renamed on complete success. A failed or interrupted write leaves no `.rsbank` behind. This
  is the mono-collapse precedent (Ψ-W2-T2, temp file + atomic rename) applied to a much
  larger file.
- **Streaming, both ways.** Append one payload at a time on write; seek and read one payload
  at a time on read. The API must make holding the whole package awkward, not merely
  discouraged.
- **The rollback delete is the carve-out, cited.** Its TU header cites
  `src/shell/persist/prune_fs.cpp:5-11` and states the discriminator it satisfies — this call
  created the file, and no index ever referenced it — rather than restating the carve-out's
  text. Anything that does not satisfy that discriminator is not this function's business.
- **Both pickers are REAPER's own, and they are symmetric.** `GetUserFileName(int mode,
  const char* caption, const char* initial_file_or_path, const char* extension_list, char*
  fnOutNeedBig, int fnOutNeedBig_sz)` — **verified**,
  `vendor/reaper-sdk/sdk/reaper_plugin_functions.h:3790`, documented at `:3788` — serves
  both directions: `mode=0` ("choose a new file") is export's destination picker, `mode=1`
  ("existing file") is import's source picker. `extension_list` takes the
  `'ReaSampler banks|*.rsbank|All files|*.*'` form, and `initial_file_or_path` may be a bare
  `'.rsbank'` to set the default extension. There is **no `#ifdef _WIN32` / SWELL split and
  no wide-char round trip here** — REAPER owns the dialog on every platform, so no
  `GetSaveFileNameW` and no `BrowseForSaveFile`. `GetUserFileNameForRead` is explicitly
  "Superseded, see GetUserFileName" (`:3796`) and is not used.
- **No fallback path.** `src/app/main.cpp:15` defines `REAPERAPI_IMPLEMENT` *without*
  `REAPERAPI_MINIMAL`, so the resolver walks the full table — `GetUserFileName` included
  (`reaper_plugin_functions.h:9084`) — and `main.cpp:292-293` aborts the extension load if
  any single function fails to resolve. No REAPER build that loads this extension can lack
  `GetUserFileName`, which makes a fallback unreachable code.
- **No REAPER project state is touched here.** No ext-state read or write, no undo block, no
  generation bump; those belong to the verbs in Ε-W2.

**Acceptance criteria.**
- A write interrupted before completion leaves the destination path absent or holding its
  prior contents — never a partial new file. Tested by injecting a failure at the writer seam.
- Reading and writing a multi-entry package never holds more than one entry's payload; the
  test asserts against a seam counter, not against a memory measurement.
- The rollback deletes exactly the files it was given and nothing else, and is a no-op on a
  path it did not write.
- **There is exactly one picker call site and it is REAPER's.** Both verbs reach
  `GetUserFileName` — export with `mode=0`, import with `mode=1` — and no symbol named
  `GetSaveFileNameW`, `BrowseForSaveFile`, or `GetUserFileNameForRead` appears anywhere in
  `src/shell/package/`, nor any platform `#ifdef` in the picker's TU. Greppable, so it stays
  true. `[verify — DAW]` — the picker is not exercised in a live REAPER session by this track.
- No file exceeds ~600 lines; the picker lives in its own TU with its own header. **Not** for
  the `drag_out` / `drag_out_win` reason — that precedent isolates a *Win32-only* TU, and
  there is no platform split here — but because the picker is the only REAPER-facing part of
  an otherwise REAPER-free, unit-tested module: folding it into a shared header would drag
  `reaper_plugin_functions.h` into the testable seam's include graph.

**Open questions.** **[propose at review]** where the `extension_list` and default-extension
strings live — this track's picker TU, or the Ε-W2 verbs that call it. They are user-facing
text, and the verbs own the rest of the user-facing text; the counter-argument is that they
are picker plumbing and only one picker exists. **[verify — DAW]** two things the header does
not answer: (1) whether the `mode=0` picker **appends** an extension when the user types a
bare name — `:3788` documents that `initial_file_or_path` may be `'.rsbank'` "to set the
default extension," but not that the dialog enforces it on return, so the verb may still have
to append `.rsbank` itself; (2) **dialog parenting** — `GetUserFileName`'s signature
(`:3790`) takes no owner window, where the abandoned Win32 `OPENFILENAME` path would have
passed `GetMainHwnd()`, so modality against the REAPER main window is unobserved.

#### Ε-W1-T3 — `import-origin-kind`

**Goal.** Give the ledger a birth-record kind for a package import, so an imported file is
tracked from the moment it lands rather than becoming a permanently unreclaimable foreign
file — landed as its own track, with its own review, because it edits safety-critical
territory that nothing else in this phase touches.

**Spec:** `docs/product/bank-package.md` §"What a package deliberately does NOT carry" (the
origin-ledger bullet); `src/core/tracking/CLAUDE.md` for the invariants it must not weaken.

**Surface boundary — owns:** `src/core/tracking/origin_ledger.{h,cpp}` and its tests,
exclusively. **Does not own:** `tracking_authority` (no decision changes), `shell/persist`,
or any consumer.

**Behavior.**
- **Append `OriginKind::PackageImport` as value 5.** Append only — `Capture`=1, `Ingest`=2,
  `Recapture`=3, `Resample`=4 keep their integers, per `core/tracking/CLAUDE.md`'s
  "PERSISTED INTEGERS — never renumber, only append".
- **A build that does not know value 5 degrades it to `Unknown`**, which is the existing
  `kindFromInt` behaviour and is the safe direction: the path is still owned, so still
  protected; only the kind detail is lost. This is the *field-vocabulary* rule, and it must
  stay distinct from the *document-version* rule right beside it, which blocks
  (`origin_ledger.cpp:18-21`).
- **Nothing else changes.** No new field, no version bump, no lineage semantics. A new enum
  value in an append-only vocabulary is precisely the change that does **not** need `"v"` to
  move, and demonstrating that is part of the point.
- **`recordCreated` needs no signature change** — it already takes an `OriginKind`
  (`src/shell/persist/session.h:95`). Confirm that in the same pass; if it turns out
  otherwise, that discovery is this track's, not Ε-W2's.

**Acceptance criteria.**
- A ledger containing a kind-5 record round-trips through serialize/deserialize unchanged.
- A record carrying an *unrecognized* kind integer (6, 99, negative) loads as `Unknown` and
  the ledger loads `Loaded`, not `Unreadable` — the vocabulary gap does not halt prune.
- `kLedgerVersion` is **unchanged** at 2, and a test asserts it, so the append-vs-bump
  distinction is pinned rather than assumed.
- `pruneProtection`'s output is unchanged for every existing kind — this track alters no
  decision.

**Open questions.** **[propose at review]** whether the kind is named `PackageImport` or
folded onto the existing `Ingest`. The plan's recommendation is a distinct value: `Ingest`
means "the user brought in a file," which is close, but losing the distinction makes a future
"where did this bank come from" question unanswerable, and an appended integer costs nothing.

---

### Ε-W2 — The two verbs

**Depends on Ε-W1 — all three tracks.** T1 for the format the verbs speak, T2 for every
filesystem act they perform, T3 for the kind their birth records carry. No part of either
verb is authorable against a format that has not settled.

**Two tracks, disjoint by direction.** They share only the manifest type. The split is real
enough that the plan **pre-split the pure planner into two TUs** (`export_plan` /
`import_plan`) rather than one `package_plan` — that separation exists specifically so these
two tracks do not fight over a file.

| Track | Owns |
|---|---|
| **T1** `bank-export` | `core/package/export_plan`, `shell/package/export_bank`, `shell/actions/package_export_action` |
| **T2** `bank-import` | `core/package/import_plan`, `shell/package/import_bank`, `shell/actions/package_import_action`, the panel's `.rsbank` drop route |

**Two shared files, named — and the disjointness here is CONDITIONAL, unlike W1's.**
`src/app/main.cpp` (one action-family registration line each) and the panel's bank menu (one
row each). Both are textual adjacency by construction, but Phase Ψ set the precedent of
granting `main.cpp` to a single track rather than sharing it (Ψ-W1-T3). **If the dispatcher
wants zero contention, serialize T2 behind T1** — T2 is the larger track and loses nothing by
starting second. The plan's default is to run them in parallel and rebase whichever lands
second.

#### Ε-W2-T1 — `bank-export`

**Goal.** One bank leaves the project as one file, or the export refuses and says why.

**Spec:** `docs/product/bank-package.md` §"Failure modes" (export rows), §"What a package
carries".

**Surface boundary — owns:** new `core/package/export_plan` (pure: which entries, what
names, what is missing, and therefore whether the export may proceed), new
`shell/package/export_bank` (the promptless verb — takes a `ReaSamplerSession&`, returns an
outcome, **no prompts and no message boxes**, mirroring `src/shell/bank_ops/`), new
`shell/actions/package_export_action` (the bindable-action skin, mirroring `prune_action`),
one registration line in `src/app/main.cpp`, one panel menu row. **Does not own:** anything
on the import side, `bank_ops`, or `persist`.

**Behavior.**
- **The exported unit is one bank** — the pool included, since the pool is structurally a bank
  (`core/model/CLAUDE.md`'s pool-privileges section). Whole-book export is an explicit
  non-goal of this phase and is preserved as an additive future by the manifest's shape, not
  by a promise.
- **Refuse-if-incomplete, report-before-acting.** An index entry whose file is missing or
  unreadable stops the export by default; the "export the N present entries" path exists only
  behind an explicit confirm that lists what is absent, distinguishing missing from
  unreadable. This is prune's dry-run-then-confirm discipline applied to a non-destructive
  act, and it is deliberate: a silently-incomplete package is discovered on the far side, in
  another project, weeks later.
- **The project is not touched.** No ext-state write, no generation bump, no undo point. An
  export that mutates project state is a defect, and the acceptance criteria name it as one.
- **Nothing is re-encoded.** Payload bytes are copied and hashed. `wav_codec` is not asked to
  rebuild anything.
- **A new FOREVER-STABLE command id** is minted through `version::channelCommandId(suffix)`
  per the root `CLAUDE.md` action contract, with its per-channel display name through
  `channelActionName`.

**Acceptance criteria.**
- `planExport` is pure and total over its inputs: a bank with a missing file, an unreadable
  file, zero samples, and one sample all classify without touching a filesystem.
- Exporting a bank and re-reading the package yields, for every entry, a `hashBytes` digest
  equal to the source file's — asserted per entry, not in aggregate.
- The exported manifest contains no absolute path and no path separator, asserted by a test
  that scans the emitted bytes rather than by inspecting the model.
- A failure injected mid-write leaves no `.rsbank` at the destination and the prior file, if
  any, intact.
- Project ext state is byte-identical before and after an export, and `bankGeneration()` is
  unchanged — a direct assertion, because "we did not mean to write anything" is not a
  property that survives without one.
- Exporting an empty bank produces a valid, importable package with zero entries rather than
  refusing. An empty bank is a legitimate thing to carry.

**Open questions.** **[propose at review]** whether the export affordance is action-only,
panel-only, or both at ship. **[propose at review]** whether the default file name is derived
from the bank's display name (recommended, sanitized through
`capture_paths::sanitizeStem`) or from the project name.

#### Ε-W2-T2 — `bank-import`

**Goal.** A package becomes a **new** bank in this project — completely, or not at all —
with every one of the four collision classes answered explicitly rather than by whatever the
model happens to do.

**Spec:** `docs/product/bank-package.md` §"Identity and collision on import" (including the
auto-suffix rule), §"Failure modes" (import rows), §"Import under a degraded tracking
ledger", §"Version tagging: both directions".

**Surface boundary — owns:** new `core/package/import_plan` (pure: the id remap table, the
parent remap, the per-entry write / skip-already-present / rename disposition, the
destination bank name after uniqueness folding), new `shell/package/import_bank` (the
promptless verb), new `shell/actions/package_import_action`, the panel's `WM_DROPFILES` route
for a `.rsbank` (routing only — the existing ingest route for audio files is untouched), one
registration line in `src/app/main.cpp`, one panel menu row, and — the **only** `core/model/`
edit in the phase — **one additive public `const` member on `BankBook`** (recommended
`std::string uniqueDisplayName(const std::string& seed) const`), so the auto-suffix probe runs
behind the model's own name fold. **Does not own:** anything on the export side,
`bank_book`'s *rules* (consumed, never re-implemented — the new member exposes the existing
fold, it does not add a second one), `origin_ledger` (Ε-W1-T3's).

**Behavior.**
- **The ledger guard runs FIRST — before the file picker opens (Ε-F3, ruled: refuse).** If
  `tracking::ledgerDegraded(status)` holds for the project's loaded ledger status
  (`Unreadable` or `FutureVersion`; `core/tracking/origin_ledger.h:94`, `:100-101`), the
  import **refuses outright** — no picker, no bytes read, no confirm-and-proceed path, no
  opt-out. `Fresh` and `Loaded` both proceed. **Do not key this on
  `PruneReport::blockedByTracking`**: that flag also fires on undecodable `rsusage_*` keys,
  which govern deletion-time protection and have nothing to do with writing birth records.
  The refusal is a `ShowConsoleMsg` block mirroring `prune_action.cpp:30-69` in structure and
  tone, with two variants (malformed / newer-build) and every recovery line naming **this
  build's** namespace through `version::extStateNamespace()`. Exact wording in the spec doc.
  **Export is deliberately not gated this way** — that is Ε-W2-T1's, and it stays ungated.
- **Version gate second, before any byte is written.** `minReaderVersion` above this build
  refuses the whole package and reports through `ShowMessageBox`
  (**verified**, `reaper_plugin_functions.h:6546`) naming three things: the package's
  requirement, this build's ceiling, and the writer's semver. Two of the three is not enough
  to act on. A malformed or truncated package reports **distinctly** — the two failures have
  opposite recoveries, which is exactly why `origin_ledger.cpp:178-185` separates them.
- **Four collisions, four answers.** (1) **Sample id** — remint every id and remap
  `Provenance::parentSampleId` (`bank_model.h:45-50`) through the same map, to the reminted
  parent when it came in the same package and cleared otherwise; a foreign id never enters
  the index. (2) **File name** — never overwrite; mint a fresh unique name through
  `capture_paths::deriveBankPaths`, silently, counted in the summary. (3) **Content hash** —
  consult `BankModel::findByHash` **before writing the payload**; on a hit, skip the write
  entirely and let the entry collapse, so a dedup never manufactures an orphan. (4) **Bank
  display name** — **auto-suffix, no prompt** (Ε-F2, ruled). Seed = the package's recorded
  source bank name **verbatim** (or the literal `Imported bank` if absent/blank); take the
  first of `seed`, `seed + " 2"`, `seed + " 3"`, … whose fold is free in the destination
  book, ascending from 2. Four points that decide the behaviour and must not be re-invented:
  the seed is **never re-parsed** (`"Drums 2"` colliding lands as `"Drums 2 2"`, not
  `"Drums 3"` — a bare trailing integer is indistinguishable from `"Kit 808"`); the probe
  **fills gaps** (first-free, not highest-plus-one, so it is a pure function of the current
  name set); the probe **terminates** by pigeonhole within `B + 1` candidates for `B` banks,
  so **no arbitrary cap**; and the fold is `BankBook`'s own (`bank_book.h:252-258`), reached
  through the new public member, never re-implemented in `import_plan`. Sample display names
  are **not** suffixed, and `slot_map` positions are untouched.
- **Always a new bank; never a merge (Ε-F2, ruled).** The import creates a bank — it never
  merges into an existing one, never lands into the pool, and offers no target picker. A
  pool export therefore lands as a **named** bank `"Pool 2"`, which is correct, not a glitch.
  This track ships **one** action, not two; merge-into-existing is out of scope for the
  phase, and move/copy already cover the after-the-fact case.
- **Birth records at landing.** Every landed file goes through
  `ReaSamplerSession::recordCreated(sample, OriginKind::PackageImport)` at the same point the
  `Sample` is added, in the same straight-line block, per `core/tracking/CLAUDE.md`'s
  no-silent-gaps invariant. An import that lands a file without a record is the exact failure
  that section exists to prevent.
- **All-or-nothing, with rollback.** Any failure after the first write deletes the files
  *this call wrote* and abandons the index mutation. The rollback cites the
  `prune_fs.cpp:5-11` carve-out; it does not restate it, and it does not reach outside the set
  it wrote.
- **One Ctrl-Z for the index, and the file residue is stated, not implied.** The index
  mutation batches through `persistBankOp` (`Undo_BeginBlock2` / `Undo_EndBlock2`, verified at
  `reaper_plugin_functions.h:7758` / `:7806`). Undo does **not** un-write the files; they
  remain as orphans until a prune reclaims them — the same designed window a non-empty bank
  delete already produces (`core/model/CLAUDE.md`'s sample-removal section). The user-facing
  summary says so.
- **`bumpBankGeneration()` on success** (`session.h:108`), so live instances reload.
- **No timeline item is placed. Ever.**
- **A new FOREVER-STABLE command id**, minted the same way T1's is.

**Acceptance criteria.**
- `planImport` is pure and total, and every one of the four collision classes has a test that
  exercises it without a filesystem: colliding ids, colliding file names, a hash already
  present, and a colliding bank name.
- Importing a package built from bank B back into the project that already contains B lands
  a **new** bank named `"B 2"`, with every id reminted, no entry lost, and B itself
  unmutated. Importing it a third time lands `"B 3"`.
- The suffix probe is pinned by pure tests over a name set, covering at minimum: a free seed
  (no suffix applied), a case/whitespace-folded collision (`"drums"` blocks `"Drums"`), a gap
  (`"Drums"` + `"Drums 3"` present ⇒ `"Drums 2"`), a seed that already ends in a number
  (`"Drums 2"` colliding ⇒ `"Drums 2 2"`), an absent/blank recorded name (⇒ `Imported bank`),
  and a package whose source bank was the pool (⇒ `"Pool 2"`, a named bank).
- A degraded ledger (`Unreadable` and `FutureVersion`, both asserted) refuses the import with
  **no picker shown, zero files written, zero index mutation**, and the message names the
  channel-correct ext-state namespace. An undecodable `rsusage_*` key with an otherwise
  `Loaded` ledger **does not** block — asserted, because the tempting reuse of
  `blockedByTracking` would silently make it.
- An entry whose payload fails its `hashBytes` check aborts the import with **zero** files
  landed and **zero** index mutation — asserted on both, since either alone would pass a
  weaker test.
- A write failure injected at entry k of n leaves exactly zero files from this import on
  disk and the index unchanged.
- Every landed file has a ledger birth record with kind `PackageImport`, asserted by reading
  the ledger after the import, not by counting calls.
- `minReaderVersion` above the build: nothing written, message names all three facts.
  `formatVersion` above the build with `minReaderVersion` at or below it: **imports cleanly**,
  unknown keys skipped. Both directions asserted, in this track, against real package bytes.
- No timeline item exists after an import; the arrange is byte-identical.
- **DAW-verification obligation** (to be discharged by Daniel, not by this track): import a
  package produced on another machine, confirm the panel shows every sample with its
  metadata, confirm a live ReaSampler 9000 instance picks up the new bank content on the
  generation bump, and confirm one Ctrl-Z removes the index entries.

**Open questions.** **No [Daniel] questions — Ε-F2 and Ε-F3 are both RULED** (new bank
always with an auto suffix; refuse on a degraded ledger), so this track is dispatchable as
written and ships one action. **[propose at review]** whether the import summary is a console
block, a message box, or both; the recommendation is a console block plus a one-line message
box, so the detail is copyable and the outcome is unmissable. Note the refusal path is
already fixed at a console block by the Ε-F3 spec, so this call is about the *success*
summary only.

---

### Ε-W3 — The compatibility fixtures

**Depends on Ε-W2 for:** both verbs existing. A round-trip claim cannot be tested against one
half of a round trip, and a "this build refuses a future package" claim cannot be tested
against a package this build is incapable of writing.

**One track.** The whole deliverable is one corpus and the harness over it; splitting it
would mean two tracks writing two halves of one fixture set.

#### Ε-W3-T1 — `package-compat-fixtures`

**Goal.** Turn the version-compatibility policy from an assertion in a doc into a property
proven against **frozen bytes**, so a later format change cannot silently break either
direction.

**Spec:** `docs/product/bank-package.md` §"Version tagging: both directions".

**Surface boundary — owns:** a new checked-in fixture corpus under the package modules' test
tree, the harness that decodes it, and one `docs/` verification script for the DAW half.
**Does not own:** any production module — if a fixture reveals a defect, the fix is filed
against the owning track's module and this track carries the failing test, not the patch.

**Behavior.**
- **Frozen bytes, not regenerated ones.** The corpus holds real `.rsbank` bytes committed to
  the repo: a v1 package written by the shipping build, a synthetic
  `formatVersion` = N+1 / `minReaderVersion` = current package (the additive-forward case),
  and a synthetic `formatVersion` = N+1 / `minReaderVersion` = N+1 package (the refuse case).
  A test that regenerates its own fixture proves only that the code agrees with itself —
  which is precisely the failure mode a format ladder exists to catch.
- **A truncation corpus.** The valid package truncated at a spread of offsets, each asserted
  `Malformed` rather than `TooNew`, so the two recoveries never get crossed.
- **A hostile-name corpus.** Packages whose entry names carry `..`, separators, and absolute
  prefixes, each refused.
- **The round-trip anchor.** Export → import → export over the v1 fixture yields
  byte-identical payloads.
- **A standalone DAW verification script** in `docs/`, following the
  `docs/verify-track-scope-multitrack.md` precedent — the cross-machine transfer is the one
  claim no unit test can make.
- **The corpus is append-only.** When a future format version ships, its fixture is added;
  no existing fixture is ever regenerated or edited. Stated in the corpus's own README so the
  rule survives the person who wrote it.

**Acceptance criteria.**
- All three version fixtures classify as specified, and the additive-forward one imports with
  every known field intact and every unknown key skipped.
- Every truncation offset classifies `Malformed`; none classifies `TooNew`, `Readable`, or
  crashes.
- Every hostile-name fixture is refused at decode, before any planner runs.
- The round-trip fixture's payloads are byte-identical after export → import → export.
- The DAW script exists and names its steps concretely enough to run without reading this
  plan.

**Open questions.** **[propose at review]** how large the committed corpus is allowed to be —
the recommendation is one-sample packages with a few hundred bytes of payload each, since the
properties under test are structural and a large payload proves nothing extra.

---

## Phase Ρ — Render in place: a track's output to a new sibling, source to the bench

**Ships:** one bindable action that renders the selected track's output over the current
range to a file outside the bank, drops that file as an item on a brand-new sibling track
at the exact position it was rendered from, clones the source's colour and its name with an
idempotent `Capture ` prefix, moves the source track into Design mode — where Design
View's existing park hides it, takes it out of the mix, and puts its FX offline — and puts
the result track into Arrange, unconditionally. The bank is never read, never written, and
never notified.

**Consolidates: none of the seventeen.** Phase Ρ came from a direct request (Daniel,
2026-08-02) and is scoped in `docs/product/render-in-place.md`. Daniel's framing, verbatim
in substance: *similar to REAPER's "Render selected track time selection to new track
(stereo) and mute original", except instead of muting the original, the source track
stays/goes to design mode, and the resulting new sibling track — which gets the rendered
audio item placed correctly in the timeline — stays in whatever mode was active when the
action was run; the new track clones the source track colour and name with a Capture
prefix; this must NOT put the rendered audio into the ReaSampler banks/pool.* **The
mode-following clause in that framing was superseded by Daniel's own Ρ-F2 ruling the same
day** — *"for this action which is not a capture, the result track should always go to
arrange"* — and the quote is kept verbatim only as the record of the request. It
**supersedes nothing** — a sweep of `docs/TODO.md` and `docs/TODO-1.0.md` for
`render.in.place|render to new track|preserve.source` returns nothing.

### The framing answer — why this does not breach the load-bearing principle

Root `CLAUDE.md`'s rule — *capture and placement are separate acts; any code path that
auto-inserts a capture into the timeline must be rejected in review* — is a rule **about
the bank**, and Phase Ρ does not put the bank on either side of its verb. The full argument
is `docs/product/render-in-place.md` §"The third verb"; the operative summary, which every
review of this phase must apply:

| Verb | Source | Sink | Touches the bank |
|---|---|---|---|
| **Capture** (`RunCapture`, batch, realtime, bake, ingest) | arrange / instrument | bank | writes it |
| **Placement** (`RunInsertSelected`, `performArrangeDrop`) | bank | arrange | reads it |
| **Render in place** (this phase) | arrange | arrange | never |

What Ρ shares with capture is the **render** — `renderOffline`, `FxBypassGuard`, exact
custom time bounds, `RENDER_ADDTOPROJ = 0`, the multi-track refusal — not the capture. A
capture is a render *plus* a bank landing; Ρ takes the mechanism and declines the landing.

**Four boundary conditions, three of them structural, all review-rejectable:**

1. Ρ's shell never calls `session.bank()`, `session.book()`, `session.recordCreated()`, or
   `session.bumpBankGeneration()`. The `Sample` the backend returns is discarded, and on the
   `ProjectMedia` destination its `relativePath` is left **empty** — a Ρ `Sample` is inert
   by construction.
2. **Ρ cannot express "write into the bank folder."** The destination reaches the backend as
   a two-valued enum, never a caller-supplied path string. A `renderDir` string on
   `CaptureRequest` instead of the enum **is** the drift, and is rejected on sight.
3. Ρ's file is never recorded as owned, so prune (`(owned ∩ present) − referenced`) cannot
   reach it — and it lives outside the bank folder, so prune's enumeration never sees it
   either. Two independent layers. The tool deletes only what it owns; a render-in-place
   file belongs to the project.
4. **Traffic is one-way.** Ρ may borrow capture's render; **capture may never borrow Ρ's
   placement.** No `place` flag on `CaptureActionDef`, no "capture and also place" action,
   ever.

### Phase-Ρ acceptance criteria

These bind the track in this phase, in addition to the plan-wide set above.

- **Placement is sample-exact and unsnapped.** The item lands at the render window's
  `startSeconds`, unrounded, with `SnapToGrid` deliberately **not** applied (unlike
  `performArrangeDrop`). Ρ's placement is the null test performed automatically — a render
  of a range re-inserted at its source position nulls to silence against the source — so a
  snapped or rounded placement is a phase failure, not a rough edge.
- **No tempo conform, ever.** `computeInsertMode(InsertOptions{})` only; `insert_plan`
  already guarantees the &4 stretch-to-time-selection bit is never set. No conform variant
  of this action is offered.
- **The bank path is byte-identical to today.** The `CaptureDestination` enum defaults to
  `Bank`; every existing capture entry point must produce exactly the file, path, hash, and
  index entry it produces now. If any capture test changes expectation, the seam is wrong.
- **The new track is bare.** `InsertTrackInProject(proj, p, /*flags=*/0)` — flags&1 adds
  default envelopes/FX (SDK header 3954) and a default chain would process the render a
  second time.
- **Both `I_FOLDERDEPTH` writes, or none.** The sibling-placement arithmetic is pure and
  unit-tested before any DAW work; a render that lands the new track at the wrong nesting
  level is audibly wrong in both directions (double-processed through a folder it re-enters,
  or bypassing the folder bus entirely).
- **The Ρ-F2 ruling survives the auto-tag detector.** The result track and its placed
  item are tagged `kArrangeModeId` **explicitly** (a membership record, not an `untag()`
  to the Arrange default), and `panel_input::detectNewContent` drops added GUIDs that
  already carry a record. Without that filter the detector tags every new track to the
  active mode on its next tick and a Design-fired render silently becomes a Design
  member — the ruling reversed inside a second. This criterion **replaces** the
  tag-before-reapply ordering criterion the phase carried under mode-following: with the
  result track an Arrange member unconditionally, a Design reapply parking it is the
  correct outcome and the ordering is state hygiene, not behaviour.
- **One undo block, `UNDO_STATE_ALL`,** opened before the track is created and closed after
  the mode reapply; the render sits outside it. `persistViewState` runs **after** the block
  closes — it may raise a Save-As dialog, which must not sit inside an open undo block
  (`design_view_actions::doMoveItems`' documented ordering).
- **Every pure module gets a `<module>_tests` target.** All three pure additions land in
  existing `core/capture` modules that already have one.

**Performance posture.** Every surface is cold — one gesture, once. None of the named hot
paths (peaks envelope compute, audition, the realtime-capture tick's single-pointer-test
idle fast path, the instrument's `process()`) is touched.

**Concurrency.** Phase Ρ is extension-side. Γ lives in `core/instrument/` +
`shell/instrument/`; Ε lands in the new `core/package/` + `shell/package/`. The pre-existing
files Ρ edits are named in its track's surface boundary and intersect neither — including
`shell/panel/panel_input.cpp`, which the Ρ-F2 ruling adds: no in-flight track in this plan
touches that file (Phase Ψ's two named regions in it, the footer block and the drag-arm
block, are both landed, and both are functions other than `detectNewContent`).

### Rulings — Daniel's, 2026-08-02. Nothing open.

All three forks this phase opened were ruled the day it was framed. Full statements, the
counter-arguments that made each a fork, and the one unexercised alternative:
`docs/product/render-in-place.md` §"Rulings".

- **Ρ-F1 — multi-track. RULED: refuse.** One selected track per fire, inherited from
  `isMultiTrackStemRender`. A **settled non-goal**, in the same register as the other
  entries under §"What Phase Ρ explicitly is NOT" — not deferred-with-a-plan. There is no
  per-track loop planned, no second wave holding one, and no seam to leave half-open for
  it. Matches the framing recommendation; nothing in the track changed.
- **Ρ-F2 — the result track's mode. RULED: always Arrange.** *"For this action which is
  not a capture, the result track should always go to arrange."* **This overrode the
  framing and the request's own original wording** — the result track no longer follows
  the active mode. The source still goes to Design. Three consequences, all specced
  below: the result track and its item are tagged `kArrangeModeId` explicitly; firing
  from Design produces **no visible change** (the A/B-on-the-bench behaviour
  mode-following would have enabled **does not exist** and must not be cited as a
  benefit anywhere); and the panel's auto-tag detector needs a two-line
  explicit-tag-wins filter, or it re-tags the result track to Design on its next timer
  tick and silently reverses the ruling.
- **Ρ-F3 — tail. RULED: follow the panel tail settings.** Matches the framing
  recommendation; nothing in the track changed. **Two consequences, accepted rather than
  caveated:** under Auto/Manual the placed item is **longer than the window it replaces**
  (correct for a decaying chain, wrong for a butt-joined section — the user's lever is
  the panel's own tail setting), and the exact-bounds gate is **inactive** in those two
  modes because it runs only under `TailMode::None`. Both are inherited from every other
  capture path, not introduced here. The item's **start** is exact in all three modes, so
  the null test holds in all three. The third option floated at framing (run the gate's
  start-alignment check regardless of tail mode) is **not ruled in** and is recorded in
  the product doc as an unexercised alternative.

---

### Ρ-W1 — The verb

**Depends on:** nothing.

**One track, deliberately.** The whole phase is roughly 350 lines: three small pure
additions to existing `core/capture` modules, one bounded edit to the offline backend, one
new shell TU, a two-line filter in `panel_input.cpp`, one `ActionTableRow`, four invariant
amendments. Splitting it would create a
merge dance across `core/capture` for no gain, and the pure half cannot be reviewed
meaningfully apart from the caller that gives it meaning. **Named contingency:** if the
sibling-placement arithmetic balloons in implementation, that function is the natural split
point — it is the only piece with zero dependency on anything else in the track.

#### Ρ-W1-T1 — `render-in-place`

**Goal.** One bindable action: render the selected track's output over the current range to
the project's recording path, place it on a new sibling track at the exact render position,
clone colour and name, move the source to Design and the result track to Arrange —
**always Arrange, whatever mode was active** (Ρ-F2).

**Spec:** `docs/product/render-in-place.md` — §"The render", §"Where the file goes",
§"Placement", §"The new track", §"Mode transitions", §"Undo", §"The action".

**Surface boundary — owns:**
- **New:** `src/shell/capture/render_in_place.{h,cpp}` (the action body) and its
  `CMakeLists.txt` entry. It lives in `shell/capture/` rather than `shell/actions/` because
  it composes `renderOffline` and `ResolveScopeSource` — that directory's `CLAUDE.md` places
  action *bodies* here and reserves `shell/actions` for skins over mutation logic owned
  elsewhere.
- **Extends (existing modules, existing test targets):** `core/capture/track_topology`
  (`siblingPlacement`), `core/capture/capture_name` (`captureTrackName`),
  `core/capture/capture_paths` (`RenderPaths` + `deriveRenderPaths`, with `deriveBankPaths`
  re-expressed over it so the file-stem spelling keeps one owner —
  `bankRelativeForName` already depends on that).
- **Edits, bounded:** `src/shell/capture/capture.{h,cpp}` — the `CaptureDestination` enum on
  `CaptureRequest`, the ~6-line destination branch at path derivation, and
  `CaptureResult::absolutePath`. `src/shell/panel/panel_input.cpp` — two lines inside
  `detectNewContent` only, dropping added GUIDs that already carry a membership record
  (`MembershipIndex::query(guid) != nullptr`) before the `autoTagNewContent` call. That
  edit exists **only because of the Ρ-F2 ruling**. `src/app/main.cpp` — one
  `ActionTableRow`.
- **Does not own:** anything under `core/instrument/`, `shell/instrument/`, `core/package/`,
  `shell/package/`, `core/model/`, `core/tracking/`, `core/reclaim/`, or `shell/persist/`.
  No new directory. No new persisted state, no new ext-state key, no new wire version rung.

**Behavior.**
- **Resolve** via `ResolveScopeSource(CaptureScope::Track, …)` — razor-else-time range,
  selected tracks, canonical GUIDs, source track name. No range → refuse with the reason
  `resolveRange` produced. **Item extent is not a fallback and must not become one.**
- **Refuse multi-track by inheritance.** `renderOffline` fires `isMultiTrackStemRender`
  before touching anything, keyed on the render *source* (`SelectedTracks`), so Ρ adds no
  check of its own and gets `multiTrackRefusalMessage(CaptureScope::Track)` for free.
- **Render** through `renderOffline(CaptureScope::Track, src.sourceTracks, req)` with
  `req.destination = ProjectMedia`, tail from `bankPanelTailSetting()`, `channelCount = 2`,
  `Float32`, `sampleRate = 0`, and the name from `captureNameFor(src.trackNames, 0,
  "capture")`. Inherits the FX-bypass guard, the render-selection guard, the `RENDER_*`
  snapshot/restore, `RENDER_ADDTOPROJ = 0`, the exact-bounds gate, the unsaved-project
  Save-As gate, and the lossless mono collapse — **all unchanged**.
- **Destination resolves in the backend, after its own save gate**, so an unsaved project is
  still prompted before any path arithmetic runs. `ProjectMedia` → `GetProjectPathEx(proj,
  …)` (SDK header 2550; header 3102 names it as the way to get the *effective* recording
  path when `RECORD_PATH` is blank or relative). The relative-paths-only invariant is
  untouched — it binds the `BankIndex`, and Ρ writes to no index.
- **Sibling placement** (pure, in `track_topology`): prefix-sum `I_FOLDERDEPTH` to absolute
  levels; `L = level[srcIdx]`; if `depth[srcIdx] >= 1` the insert position `p` is the first
  `j > srcIdx` with `level[j] == L` (else `count`), otherwise `p = srcIdx + 1`; then exactly
  two writes — `depth[p-1] = L - level[p-1]` and the new track's `depth = level[p] - L`
  (with `level[count] = 0`). Total delta sum preserved, so nothing downstream shifts. The
  five cases and their expected outcomes are tabulated in the product doc; a malformed
  project whose deltas do not sum to zero clamps rather than asserts.
- **Create + dress:** `InsertTrackInProject(proj, p, 0)`, `GetTrack(proj, p)`;
  `SetMediaTrackInfo_Value(new, "I_CUSTOMCOLOR", (double)GetTrackColor(source))` — one line
  clones a colour and the absence of one, since `GetTrackColor` returns the value already
  OR'd with `0x1000000` and `0` means unset; `GetSetMediaTrackInfo_String(new, "P_NAME",
  buf, true)` with `captureTrackName(trackName(source))`.
- **Name rule (pure, tested):** `"Capture " + sourceName`, **idempotent** — if the source
  name already begins with the prefix, the new name is the source name verbatim, so a second
  run yields `Capture MONEY`, never `Capture Capture MONEY`. An unnamed source yields
  `Capture Track N` (`trackName` uses `GetTrackName`, which already answers REAPER's own
  convention — the Ψ-W2-T1 precedent). A counter suffix is rejected: REAPER does not
  uniquify track names either, and it is a treadmill.
- **Place:** snapshot the edit cursor → `SetOnlyTrackSelected(new)` →
  `SetEditCurPos(src.startSeconds, false, false)` → `InsertMedia(absolutePath,
  computeInsertMode(InsertOptions{}))` → restore the cursor. **Leave the new track selected,
  alone** — a deliberate divergence from the restore-the-selection convention every other
  placing path follows, because in the headline case the source is being parked out of sight
  in the same gesture and restoring the selection would leave the user selecting an
  invisible track.
- **Modes (Ρ-F2, RULED — absolute, not mode-following):**
  `membership().tag(sourceGuid, kDesignModeId)` (covers stays *and* goes — `tag` replaces
  prior single-mode membership); `membership().tag(newTrackGuid, kArrangeModeId)` —
  **`kArrangeModeId` unconditionally, never `view.activeModeId()`**; `membership().tag(…,
  kArrangeModeId)` for **each item on the new track** (enumerate after `InsertMedia`; the
  track is brand new so those are exactly the items just placed — `item_read::itemGuid` is
  the existing GUID seam); then `mintManagedLanes(view, nullptr)`; then `applyMode(view,
  view.activeModeId(), nullptr)` — a reapply, never a switch, so it is not transport-gated
  and does not touch solo.
- **Why explicit tags and not `untag()`:** an untagged GUID is an Arrange member by
  behaviour but carries no membership record, and the record is what the auto-tag
  detector's new filter keys on. Tag, don't untag. **This is the first explicit
  `kArrangeModeId` record in the tree** — the shipped *tag selected tracks → Arrange*
  action dispatches to `doUntag()`, i.e. Arrange-by-absence. The record is well-formed
  and behaviourally identical (`isMember` answers the same for both states, `untag()`
  still clears it); it costs one persisted entry. Unit-test the JSON round-trip;
  `[verify — DAW]` that such a project shows no view-behaviour difference.
- **Fired from Design, the result track is parked and nothing is visible.** That is the
  ruled behaviour, not a bug: the result track is an Arrange member, so a Design reapply
  parks it, and it appears in the source's place on the next switch to Arrange. The
  tag-before-reapply ordering that mode-following made load-bearing is now state hygiene
  only.
- **Selection stays absolute too.** The result track is left selected, alone, in both
  cases — even fired from Design, where that selects a parked track. Making selection
  conditional on the active mode would reintroduce exactly the mode-relative behaviour
  Ρ-F2 removed.
- **Inherited and deliberately not fought:** show-both on the source is not cleared (it is
  the user's pin); a folder-parent source is not hidden by tagging, because parents are
  derived (`core/view/CLAUDE.md`) — the existing tag action behaves identically. **Do not
  invent a cascade that tags the children.**
- **Feedback:** silent on success (the new track is the feedback), `ShowConsoleMsg` on every
  refusal — matching `RunInsertSelected`. **[propose at review]** whether the Design-fired
  path breaks that silence with a one-line `ShowConsoleMsg` naming the track it created:
  under Ρ-F2 that path produces no visible change, so a silent success is
  indistinguishable from a no-op. Recommendation: yes, one line and one string.
- **Action:** suffix **`RENDER_TRACK_IN_PLACE`** — FOREVER-STABLE per channel, minted as a
  new `RENDER_*` verb family rather than a `CAPTURE_*` member, deliberately: the id is
  permanent and is the most durable statement the codebase makes about which pillar a
  feature belongs to. Phrase: **`"render selected track to a new track (source moves to
  Design)"`**. Main section only; one `ActionTableRow`; no `custom_action`/`hookcommand2`.

**Invariant amendments — deliverables of this track, not follow-ups** (the Phase Ψ
precedent, where three such amendments were acceptance criteria of the tracks that broke
them). A track that lands Ρ without these reads as an invariant breach in review.

1. `src/shell/capture/CLAUDE.md` §Invariants — *"`RunInsertSelected` is the one deliberate
   exception to capture-never-places."* Amend to state that this directory now hosts two
   placing paths and give the discriminator: `RunInsertSelected` places a *bank sample*;
   `render_in_place` places a render that never entered the bank. Neither is a capture
   placing itself.
2. `src/shell/actions/CLAUDE.md` §Invariants — *"`arrange_drop_win` is the only
   timeline-placing shell in this directory."* Scope the sentence explicitly to that
   directory and cross-reference the third verb.
3. Root `CLAUDE.md` §"The load-bearing principle" — **one sentence, not a rewrite**: a
   render that never enters the bank and never leaves it is a third verb outside the rule,
   with the two-way boundary named. The prohibition must not be softened; the exception must
   be named precisely.
4. `src/core/view/CLAUDE.md` §Invariants — *"New tracks are tagged to the active mode at
   creation."* **Added by the Ρ-F2 ruling.** That rule now applies only to a GUID carrying
   no membership record: an explicit tag wins over the detector. Amend the sentence and
   state the reason in one clause — the detector classifies content the *user* made, not
   content the tool made and already classified. `src/shell/panel/CLAUDE.md` describes
   `panel_input` only as "the new-content auto-tag timer" and does not restate the rule, so
   it needs no amendment.

**Acceptance criteria.**
- `siblingPlacement` is unit-tested over all five cases in the product doc's table (normal
  mid-folder, last-in-folder `-1`, last-in-two-folders `-2`, folder parent, last track in
  the project) plus a malformed non-zero-sum delta list, with no DAW.
- `captureTrackName` is unit-tested for plain, already-prefixed (idempotence), empty, and
  `Track N` sources.
- `deriveRenderPaths` round-trips the same stem spelling `deriveBankPaths` produces for the
  same inputs, and the existing `capture_paths` tests pass unchanged.
- Every existing capture test passes with **no expectation change** — the proof that the
  destination seam is inert on the `Bank` path.
- `render_in_place.cpp` contains no reference to `session.bank()`, `session.book()`,
  `recordCreated`, or `bumpBankGeneration` — checkable by grep, and the review gate for
  boundary condition 1.
- The action registers, dispatches, and mirror-unregisters through the single
  `ActionTableRow` — no separate registration mechanism.
- `render_in_place.cpp` contains no reference to `activeModeId` in the tagging path —
  checkable by grep, and the review gate for the Ρ-F2 ruling. The result track and its
  item are tagged `kArrangeModeId`; `activeModeId()` appears only in the `applyMode`
  reapply.
- `detectNewContent` no longer auto-tags a GUID that already carries a membership record,
  and the existing `view_mode_model` / `guid_diff` tests pass unchanged (the filter is
  shell-side; `autoTagNewContent`'s pure contract does not move).
- A membership index carrying an explicit `kArrangeModeId` record JSON-round-trips
  unchanged, and `isMember` answers identically for that record and for an absent one —
  one added `view_mode_model` test, no DAW.
- All four invariant amendments are in the diff.

**DAW-verification obligation** (stated up front; nothing past the pure functions is
unit-testable):
- **The null test on Ρ's own output** — render a track over a range, polarity-invert the
  source against the new track, confirm silence. The phase's trust anchor.
- **The three folder cases**, each confirming the new track's nesting level and that the
  render feeds (or correctly bypasses) the folder bus. `[verify — DAW]` whether
  `InsertTrackInProject` plus the two `I_FOLDERDEPTH` writes settle without an intermediate
  `TrackList_AdjustWindows(false)` (SDK header 7735; header 2721 notes some attribute writes
  need a manual panel update, and the `isMinor` semantics are undocumented).
- **The collapsed-mono placement** — render a dead-centre source, confirm a mono item, and
  confirm it sums at the same level the stereo source did. This is root `CLAUDE.md`'s
  existing `[verify — DAW]` on mono-item-on-stereo-track summing, **promoted to
  load-bearing by this phase**: Ρ is the first path where a collapsed render is placed into
  the mix by the tool itself.
- **Both mode transitions under the Ρ-F2 ruling** — fired from Arrange (source parks;
  result track visible and in the mix) and fired from Design (source stays on the bench;
  result track parked, then present in the source's place after a switch to Arrange).
  **In each case wait out at least one panel timer tick and re-check the membership** —
  that is the auto-tag-detector regression, and it is what catches a missing
  explicit-tag-wins filter or an untagged item, either of which silently reverses the
  ruling. The old ordering check (that the new track is never momentarily parked) no
  longer applies.
- **Undo** — one Ctrl-Z removes track and item and reverts the folder-depth write; the file
  survives (REAPER's undo deletes no files, and prune cannot reach this one); the source
  stays tagged Design, whose way out is the existing *tag selected tracks → Arrange* action.
  All three residuals are inherited from the documented model-vs-undo split
  (`src/shell/view/CLAUDE.md` §Gotchas), not introduced here.
- **Name and colour clone**, including a second run over an already-prefixed track (must not
  stack) and an unnamed source (must read `Capture Track N`).
- **`GetProjectPathEx` against a project with a non-default recording path**, confirming the
  render lands where the project's media lives.

**Open questions.** **No [Daniel] questions — Ρ-F1, Ρ-F2 and Ρ-F3 are all RULED**
(2026-08-02), so nothing here is gated. Two **[propose at review]** items, both
recommendation-carrying. First, whether the Design-fired path emits a one-line
`ShowConsoleMsg` (see the Feedback bullet) — recommendation: yes. Second, **[propose at
review]** whether `render_in_place` should refuse when the source track is the master —
`ResolveScopeSource` collects via `CountSelectedTracks`/`GetSelectedTrack`, which skip the
master (SDK header), so a master-only selection already resolves to zero tracks and refuses
with "nothing selected"; the recommendation is to leave that inherited behaviour alone
rather than add a message for a case the existing path already handles correctly.

---

## Phase Λ — ReaSampler on Linux: both artifacts, shipped

**Ships:** `reaper_reasampler.so` and `reasampler_9000.vst3` built, installed and documented
on Linux. The extension compiles under GCC/Clang, builds optimized by default, docks its
panel, writes a bank index that survives a comma-decimal locale, and prunes without lying
about what it reclaimed. The instrument loads, scans, instantiates and processes audio in
**any** Linux host, and opens its editor under **REAPER only** — a stated contract, not a
shortfall. macOS is out.

**Four named exceptions to platform parity**, stated up front rather than discovered:
prune deletes permanently (no trash); the OS drag-out's copy-only guarantee is conventional
rather than structural, because SWELL's file-list drag takes no effect mask; the type faces
are DejaVu rather than Segoe UI and Consolas; and the ReaSampler 9000 editor comes up under
REAPER only, with every other host getting its generic parameter UI.

**Consolidates: none of the seventeen.** Phase Λ came from a direct request (Daniel,
2026-08-02) and is scoped in `docs/product/linux-readiness.md`, which is itself downstream of
two landed audits: `docs/product/audit-notes/lambda-w1-t1-build-toolchain.md` (findings
Λ-01…Λ-10, verify items V1…V10, forks D1…D7 — build system, toolchain, vendored deps,
resources, test harness, packaging) and
`docs/product/audit-notes/lambda-w1-t2-source-runtime.md` (L2-01…L2-12 — source portability
and runtime behaviour). **Grep those for a cited finding number; do not read either whole.**
This phase **supersedes nothing**, and corrects one standing promise rather than inheriting
it: `docs/product/versioning-and-release.md` already commits in writing to "three platform
artifacts per channel per release" (T1 §1e), which Λ-D4 makes **two** for now, with macOS
named as deferred rather than silently dropped.

**Nothing in this phase has been verified on a Linux machine.** Every `[verify — Linux]`
mark below is load-bearing and none may be laundered into settled voice. The structural
consequence is stated once here because it shapes the whole wave order: **Λ-W2's edits are
authored blind from the audits' citations, and Λ-W3 is the session that discharges Λ-W2's
acceptance criteria.** Λ-W2 is not "done" in the usual sense until Λ-W3 runs, and a track
that reports otherwise has laundered unverified work into landed work — which is exactly
what the audits' `[verify]` discipline exists to prevent. Both audits state their own effort
bands as provisional until the sweep runs; this plan does not restate a band the audits did
not give.

### Rulings — Daniel's, 2026-08-02. Six, settled.

Full statements with the reasoning Daniel gave: `docs/product/linux-readiness.md`
§"Settled decisions (Daniel, 2026-08-02)". **Do not re-litigate these.**

| Ruling | What it settled | Specified in | Bound into |
|---|---|---|---|
| **Λ-D1** | **The VST3 instrument is in scope** (audit fork D1 = **Fork B**). Both artifacts ship, not just the extension. **Reverses D5's platform clause in writing** — "Windows-only, VST3-only, REAPER-only" becomes "Windows and Linux; the editor is REAPER-hosted"; the VST3-only and REAPER-only clauses survive verbatim | §Λ-D1 | Λ-W6…Λ-W8; the D5 rewrite is Λ-W6-T1's first act |
| **Λ-D2** | **SWELL is reached by `dlopen`ing REAPER's own `libSwell.so`** (route **B3a**). Route B3b — building SWELL into the module, and with it GDK/GTK3, FreeType, Fontconfig, OpenGL — is **rejected**, not deferred | §Λ-D2 | Λ-W7-T1 |
| **Λ-D3** | **Non-REAPER hosts must be protected** — *"REAPER-only is good for now, but we need to protext other hosts."* A first-class acceptance criterion of two tracks, not a footnote on one | §"The non-REAPER-host safety criterion" | Λ-W6-T2 (contract), Λ-W7-T1 (runtime), Λ-W8-T1 (regression) |
| **Λ-D4** | **macOS is out** (D3 = **out**) — *"I don't have a mac to compile on."* Shared macOS/Linux edits are still made **in their shared form and noted as shared**; what is out is macOS as a deliverable, any macOS verification, signing/notarization, and L2-11's APFS half. **Do not "fix macOS while you're in there"** — an unverifiable edit to the APPLE branch is worse than none, because it looks tested | §Λ-D4 | phase-wide |
| **Λ-D5** | **The Linux artifact is shipped, not developer-only** (D6 = **shipped**), which promotes Λ-02 (a real optimized build) and Λ-08 (a documented install path) to must-fix. **And a hard `unlink` prune is acceptable**, with a **platform-aware confirmation string** saying so | §Λ-D5 | Λ-W2-T2, Λ-W4-T1, Λ-W5-T1 |
| **Λ-D6** | **The X11 editor is real work and is sequenced last.** Not a separate ruling — the direct consequence of D1+D2+D3. Everything before it ships something; halting Λ-W8 still leaves a shipped extension and a loading, processing, generic-UI instrument | §Λ-D6 | the wave order itself |

### Open forks — four, NONE ruled

Stated with the evidence for both sides at `docs/product/linux-readiness.md` §"Open forks —
Daniel's". This is the one phase in this plan with unanswered [Daniel]-class questions; see
"Decision state" above for how that reconciles against the plan-wide claim.

| Fork | Question | Recommendation | What it blocks |
|---|---|---|---|
| **Λ-F1** | Does CI get built in this phase, and on what runner? (audit D4) | **No recommendation — genuinely a resourcing call.** For: two toolchains and one is not on the developer's machine, so every Windows-only commit becomes a latent Linux regression. Against: CI is infrastructure, its value is highest *after* the first Linux build works, and a runner forces Λ-F4 immediately | **Nothing.** Decides only whether Λ-W5-T1 writes a pipeline paragraph or a "deferred to dev-ops" one |
| **Λ-F2** | The dialog-resource route: **resource-id-0** (SWELL's documented escape hatch, `swell-functions.h:606–608`; deletes the whole resgen pipeline from non-Windows builds) or **wire up resgen properly** (PHP, `add_custom_command`, an include-shim TU — the M end of Λ-01's band)? (audit D7) | **No clean recommendation; the asymmetry neither audit stated is the reason it is worth ruling.** Route A is cheaper and structurally simpler and stakes the panel's file-drop ingest on an *unverified* `ChildWindowFromPoint` descent, because with no resource there is no `WS_EX_ACCEPTFILES` bit to set (L2-07). Route B can set that bit explicitly. **A third option, if the cheap route is wanted without the exposure:** take Route A and make the file-drop check a hard gate in Λ-W3's sweep, with Route B as an additive follow-up track rather than a rewrite — it spends a second Linux session in the bad case | **Λ-W2-T3's dispatch — the ONLY fork here that gates a track.** Unruled, that track slips to Λ-W4 and Λ-W3's sweep splits into two Linux sessions |
| **Λ-F3** | Does "copy-only is structural" survive as a shipped invariant? (T2 [Daniel] 4) | **Keep the invariant, scope the *enforcement* claim to Windows** — "structurally enforced on Windows via `DoDragDrop`'s copy-only mask; advertised, not enforced, on SWELL, which takes no effect mask." The real counter: an invariant one platform cannot enforce is arguably not an invariant, and weakening the global wording would also correctly warn a Windows reader off relying on it in shared code | **Nothing.** Λ-W4-T3 makes the edit either way; only the sentence changes. Answerable at implementation review |
| **Λ-F4** | What is the declared support floor? (new — the audits raised the inputs Λ-07, V8, V10, not the decision) | **Declare a narrow floor — current-stable-distro glibc, GCC ≥ 9, `x86_64-linux` only — and widen it on request.** Counter: REAPER's Linux reputation is partly built on modest and non-x86 hardware, `aarch64` is no longer exotic, and adding it later means a second bundle directory and a second verification pass on every release | **The ship wave, not a dispatch.** Non-gating for Λ-W2 (a recorded floor can be widened); **gating for Λ-W5-T1** — a shipped artifact has to say what it runs on. Also feeds Λ-W6-T1's bundle directory set and Λ-F1's runner image |

### Phase-Λ acceptance criteria

These bind every track in this phase, in addition to the plan-wide set above.

- **Windows behaviour does not change. Every track carries that criterion explicitly.** This
  is a port; a port that improves Windows by accident has also changed Windows by accident.
- **`[verify — Linux]` is a status, not a decoration.** A criterion carrying it is discharged
  by a recorded observation in Λ-W3's verification record — never by inspection, never by
  "should work". A track whose Linux criteria are all unrun is not landed; it is authored.
- **No vendored file is patched.** `git status` under `vendor/` stays clean through the whole
  phase, including the SWELL bootstrap — which is precisely the constraint that shapes it.
- **No new third-party dependency surface.** Λ-D2 rejected route B3b for this reason: no
  vendored SWELL build, no GDK/GTK3, no FreeType, no Fontconfig, no OpenGL, no `pkg-config`
  in this tree. **A track that finds itself reaching for one of those has drifted and stops.**
- **Shared macOS/Linux edits are made in shared form and labelled shared** (Λ-D4). Λ-01,
  Λ-03, Λ-04, Λ-07, Λ-09 and L2-09 are all shared by the audits' own marking; an
  `#ifdef _WIN32` / `#else` that is right for both costs nothing and needs no mac. No macOS
  verification is claimed for any of them.
- **`core/` stays pure and stays platform-neutral.** Every `#include` under `src/core/**` is
  a `core/` sibling, one of 26 standard headers, or the generated `version_generated.h` (T2
  §1.1) — the audits verified this rather than assuming it, and no Λ track may be the one
  that breaks it. The one platform fork in the whole directory
  (`capture_paths.cpp:18–20`, the Windows case-fold) is already correct for Linux with both
  branches asserted by `tests/test_capture_paths.cpp`.
- **Every pure module gets a `<module>_tests` target** that runs without REAPER or a DAW.
  Λ-W2-T4 is the phase's proof that this is not ceremonial: it is a `core/`-only fix, fully
  testable on the current Windows box, and it lands before any Linux session.
- **The ~600-line ceiling and the structural heuristics bind unchanged.** Λ-W8-T1 is the
  track most likely to strain them; its own criteria name the seam vocabulary to reuse.

**Performance posture.** No named hot path is touched by any track in this phase — not the
`peaks` envelope compute, not audition, not the realtime-capture tick's single-pointer-test
idle fast path, not the instrument's `process()`. Two consequences are stated as criteria
rather than left implicit: **nothing is added to `process()`** (no `dlopen`, no probe, no
platform branch on the per-voice-per-sample path — the SWELL availability probe is computed
once, lazily, off the audio thread), and **Λ-W2-T4's locale fix stays on the JSON/persist
path**, which root `CLAUDE.md` already declares off all hot paths. Λ-02 is the phase's one
genuine performance item and it runs the other way: today a Linux build carries **no `-O`
flag at all**, on a tree whose `peaks` path is documented as presuming an optimizing build.

### The non-REAPER-host safety contract

Λ-D3 stated as something a person can check. **This is an observable contract, and it is the
specification the two instrument waves are graded against** — the full argument is
`docs/product/linux-readiness.md` §"The non-REAPER-host safety criterion", which a brief for
Λ-W6-T2, Λ-W7-T1 or Λ-W8-T1 must be written against.

**The finding that makes it urgent, and it is a fact rather than a risk.** The vendored
`vendor/WDL/WDL/swell/swell-modstub-generic.cpp` declares a **file-scope static** at `:125`,
so its constructor runs when our `.so` is `dlopen`ed — i.e. **during the host's plugin
scan** — and `:102` calls **`exit(2)`** when `dlopen` of `libSwell.so` fails, `:117`
**`exit(1)`** on an incomplete API table. Compiling the vendored `SWELL_LOAD_SWELL_DYLIB`
path unmodified therefore means **a Bitwig or Ardour plugin scan on a machine without
`libSwell.so` terminates the host process** — a killed DAW mid-scan, with the user's
session. The mitigating detail that shapes the fix: `doinit` substitutes a zero-returning
`dummyFunc` for each unresolved name, so a *partial* table degrades rather than crashes. A
partial load is survivable; `exit()` is not.

**In any Linux host, with or without `libSwell.so`:** the module scans and enumerates its one
class with no crash, hang, process exit or blacklist entry; it instantiates, produces audio,
plays MIDI, and round-trips component state byte-identically with the Windows build; when no
usable platform surface exists `isPlatformTypeSupported` returns `kResultFalse` for **every**
type — including `kPlatformTypeX11EmbedWindowID` — and `createView(kEditor)` returns
**`nullptr`**, never a view that then fails to attach and never one that draws nothing; the
host falls back to its generic parameter UI; exactly **one** diagnostic line per process
names why the editor is unavailable, through the SDK's logging or stderr, never a modal;
teardown crashes nothing and leaves no partially-initialised SWELL table reachable.

**Verified with a harness that is already on disk and that neither audit named**, because
neither swept the SDK's `samples/` tree:
`vendor/vst3sdk/public.sdk/samples/vst-hosting/validator/` is a scriptable, headless,
REAPER-free host, under `public.sdk/` and therefore pulled by the documented narrow submodule
init. The four checks, all `[verify — Linux]`: `validator` completes with **no `libSwell.so`
on the filesystem** (the direct negation of the `exit(2)` finding, and the load-bearing one);
a real Ardour or Bitwig scan reaching browser, instantiation and generic UI; a REAPER-on-Linux
load where the editor opens; and a **negative control** — rename `libSwell.so` beside a Linux
REAPER and confirm the generic-UI fallback plus the single diagnostic line.

### Cross-phase boundary — Γ-W4-T1, and the frozen parameter table

**Λ must never register a VST3 parameter.** Γ-W4-T1's `ParamID` table is FOREVER-FROZEN from
the moment it ships, on the same footing as the command-id strings and the class UIDs. A
parameter minted in Λ to make a Linux fallback look better would collide with a table Λ does
not own. **This is a hard boundary and it is the only Γ↔Λ interaction that could actually go
wrong.**

Neither phase blocks the other. The host's fallback for a plug-in reporting no usable editor
is its generic parameter UI, and today the plugin registers zero parameters — so "no editor"
currently degrades to *nothing* rather than to *controls*. **Λ-W6-T2's acceptance criteria are
written to pass with zero parameters registered**: an empty generic UI is a pass. Once
Γ-W4-T1 lands its 44 derived parameters the identical Λ code path degrades to a usable
generic UI instead — a better result, not a different criterion. **Λ assumes no schedule for
Γ**; a Λ track needing a Γ fact reads `dev` at dispatch time.

**Concurrency.** Λ is the widest phase in this plan by file surface, and it is disjoint from
the others by *kind* rather than by directory: its edits are platform guards, CMake, docs and
one new `shell/instrument/` TU. Γ owns `core/instrument/` + `shell/instrument/` **sources**
(Λ-W6…Λ-W8 own that directory's CMake, its `CLAUDE.md` files, and `editor_platform`'s
non-Windows branch); Ε lands in the new `core/package/` + `shell/package/`; Ρ touches
`core/capture`, one new `shell/capture` TU, `panel_input.cpp` and `main.cpp`. **The genuine
adjacency to watch is CMake**: Ε appends two `add_subdirectory` lines to the root
`CMakeLists.txt` while Λ-W2-T2 owns that file's language and target settings — append-only
lines against property blocks, textual adjacency rather than semantic contention. Λ's own
shared files are named in the wave sections below.

---

### Λ-W1 — The audits *(complete)*

Two tracks, both landed: **T1 `build-toolchain-audit`** (Λ-01…Λ-10, V1…V10, D1…D7) and
**T2 `source-runtime-audit`** (L2-01…L2-12). Recorded so the wave numbering matches the audit
filenames and branch names. Nothing to re-spec; nothing to dispatch.

---

### Λ-W2 — Make it buildable, and make it honest

**Depends on:** nothing. **Four tracks, disjoint at the file level.** Three are authored
blind against the audits and verified in Λ-W3; **T4 is the one track fully verifiable on the
current Windows box.**

| Track | Owns |
|---|---|
| **T1** `linux-compile-blockers` | two source blockers + `main.cpp`'s API-load failure branch |
| **T2** `toolchain-floor` | the CMake files' language, property and platform blocks |
| **T3** `panel-dialog-resource` | `panel_window.cpp`'s dialog creation, `resource.rc` / `resource.h` — **gated on Λ-F2** |
| **T4** `locale-independent-numerics` | the number codec in four `core/` TUs |

**Shared file in the wave, named rather than discovered at merge:**
`src/app/CMakeLists.txt` — T2 takes the property and platform blocks, T3 takes **one
`target_sources` line under the resgen route only**. Disjoint regions; whichever lands second
rebases. The wave's severity question is dissolved rather than adjudicated: T2 grades L2-03 a
Blocker on failure-mode quality with no direct evidence and L2-04 a Major with a confirmed
mechanism, and flags its own inconsistency — both land here, in different tracks, so the
relative grade never has to be settled.

#### Λ-W2-T1 — `linux-compile-blockers`

**Goal.** The extension compiles and links under GCC/Clang, and when it refuses to load it
says why instead of vanishing.

**Spec:** `docs/product/linux-readiness.md` §Λ-W2-T1; findings L2-01, L2-02, L2-03.

**Surface boundary — owns:** `src/shell/panel/draw_kit.cpp` (`loadFont`, `:70–77`),
`src/shell/actions/instrument_drop_win.cpp` (`writeTempPreset`, `:50–61`), `src/app/main.cpp`
(**the `REAPERAPI_LoadAPI` failure branch only**). **Does not own:** any `CMakeLists.txt`,
`panel_window.cpp`, or any `core/` file.

**Behavior.**
- **`FF_DONTCARE` (L2-01).** `draw_kit.cpp:73` passes `DEFAULT_PITCH | FF_DONTCARE` to
  `CreateFont`; the symbol has **zero occurrences anywhere in `vendor/WDL/`**, and the file is
  not platform-guarded, only its include is. `draw_kit` links into **both** modules, so
  nothing builds until this is fixed. Drop the term or define it locally in the non-Windows
  include branch. **Do not add `windows.h`** — L2-01's stated direction. The family bits are
  advisory to Windows' font mapper and meaningless to fontconfig.
- **`GetCurrentProcessId()` (L2-02).** `instrument_drop_win.cpp:59` calls it with no platform
  branch anywhere in the TU; SWELL exports `GetCurrentThreadId` and not this. The PID exists
  only to keep two concurrent REAPER instances from colliding in the shared temp dir, and the
  atomic counter at `:52` already carries the intra-process half. Replace with a
  platform-neutral uniqueness source behind a guard.
- **The silent load failure (L2-03).** Either switch `main.cpp` to `REAPERAPI_MINIMAL` plus an
  explicit `WANT` list — the pattern `panel_window.cpp` and `panel_audition.cpp` already use,
  and the honest inventory of what this extension actually needs — or keep the full load and
  print the failure count via `rec->GetFunc("ShowConsoleMsg")` before returning 0.

**Acceptance criteria.**
- `cmake -B build -S . -G Ninja && cmake --build build` produces `build/reaper_reasampler.so`
  with no errors. `[verify — Linux]` = **V1**.
- `ctest --test-dir build --output-on-failure` passes all 91 test targets, **no `-C` flag
  needed** on a single-config generator. `[verify — Linux]` = **V2**.
- A deliberately misspelled `WANT` entry (or a forced non-zero `failcnt`) produces a visible
  REAPER console line naming the count, not a silent refusal.
- Windows build and `ctest` unchanged.

**Prerequisites.** None. **Discharges:** L2-01, L2-02, L2-03; enables V1, V2, V3.

#### Λ-W2-T2 — `toolchain-floor`

**Goal.** The Linux build is optimized when asked, links what it uses, hides what it does not
export, and reports diagnostics no one has seen yet.

**Spec:** `docs/product/linux-readiness.md` §Λ-W2-T2; findings Λ-02, Λ-03, Λ-04, Λ-05, Λ-07,
Λ-09, Λ-10.

**Surface boundary — owns:** root `CMakeLists.txt`, `src/app/CMakeLists.txt` (**the target
property and platform blocks; NOT the source list — that is Λ-W4-T3's**),
`cmake/reasampler_targets.cmake`, `src/shell/instrument/CMakeLists.txt` (**thread linkage
only; the `WIN32` gate is Λ-W6-T1's**), `README.md`, and root `CLAUDE.md` §"Build and test" /
§"Install / reload" / §"One-time submodule setup". **Does not own:** any `.cpp` or `.h`.

**Behavior.**
- **Λ-02** — root `CMakeLists.txt:28–30` is the *complete* list of language settings: no
  `CMAKE_BUILD_TYPE`, no `CMAKE_CXX_FLAGS`, no IPO/LTO, no `target_compile_options` anywhere
  in the tree. Default `CMAKE_BUILD_TYPE` when neither it nor `CMAKE_CONFIGURATION_TYPES` is
  set, and correct the docs' ship instruction: `--config Release` is *accepted and ignored*
  by Ninja and Make, and the README currently sends a Linux user to `build/Release/`, which
  does not exist there — the module lands at `build/reaper_reasampler.so`.
- **Λ-03** — `CXX_VISIBILITY_PRESET hidden` + `VISIBILITY_INLINES_HIDDEN` on both module
  targets. SWELL's own build already uses `-fvisibility=hidden`, and the symbols that must
  stay exported carry their own `visibility("default")` attributes
  (`reaper_plugin.h`'s `REAPER_PLUGIN_DLL_EXPORT`, `fplatform.h`'s `SMTG_EXPORT_SYMBOL`,
  `swell-modstub-generic.cpp:135`'s `SWELL_dllMain`).
- **Λ-04** — `find_package(Threads REQUIRED)` + `Threads::Threads`. Correct on all three
  platforms, costs nothing on Windows.
- **Λ-09** — `-Wall -Wextra` and `CMAKE_CXX_EXTENSIONS OFF`. **No `-Werror` in this change**
  (the audit is explicit): the diagnostic-set size over this tree is not estimable from
  Windows. Separately `-Wl,--no-undefined` on the module targets, restoring the
  fail-at-link-time behaviour MSVC gives and GNU `ld` does not — **or** a recorded reason why
  `SWELL_PROVIDED_BY_APP`'s function-pointer design makes the gap moot.
- **Λ-05** — pin the `reaper_plugin.h` → `../WDL/swell/swell.h` include coincidence with a
  comment or an `INTERFACE` target carrying both include dirs as one unit. Invisible on
  Windows, load-bearing off it.
- **Λ-07** — record the compiler floor (**Λ-F4's input**), or add `-lstdc++fs` and document
  why.
- **Λ-10** — one sentence in the platform-support docs: on Linux `vendor/vst3sdk` is optional
  until Λ-W6, so `git submodule update --init vendor/reaper-sdk vendor/WDL` is the complete
  extension-only prerequisite.

**Acceptance criteria.**
- `compile_commands.json` or a verbose build log shows an explicit `-O` flag on a bare
  `cmake --build build`. `[verify — Linux]`.
- `nm -D --defined-only reaper_reasampler.so | grep -E 'ReaperPluginEntry|SWELL_dllMain'`
  finds **both** after the visibility preset. `[verify — Linux]` = **V6**.
- The extension links with `Threads::Threads` **removed** — proving the include-only pthread
  dependency needs no flag — or the symbol forcing it is named. `[verify — Linux]` = **V5**.
- `capture_paths_tests` links without an explicit `-lstdc++fs` on the declared floor, or the
  flag is added and the floor documented. `[verify — Linux]` = **V8**.
- **The warning count from the first `-Wall -Wextra` build is recorded, not fixed**, and
  handed to Λ-W4 as an input.
- Windows build unchanged; `--config Release` still behaves as documented there.

**Prerequisites.** None; concurrent with T1, T3, T4. **Discharges:** Λ-02, Λ-03, Λ-04, Λ-05,
Λ-07, Λ-09, Λ-10; enables V5, V6, V8.

#### Λ-W2-T3 — `panel-dialog-resource` *(GATED on Λ-F2 — do not dispatch until it is ruled)*

**Goal.** The docked bank panel opens on Linux, and if it ever fails to, it says so.

**Spec:** `docs/product/linux-readiness.md` §Λ-W2-T3 and §Λ-F2; findings Λ-01, L2-06, L2-07.

**The defect.** `panel_window.cpp:135` is `CreateDialogParam(g_hInst,
MAKEINTRESOURCE(IDD_BANK_PANEL), …)`, which SWELL resolves out of a per-module registry
populated by a **resgen-generated source file that is not in the Linux target**:
`src/app/CMakeLists.txt:97` has the `target_sources` line commented out (`:86` for macOS).
The registry head stays null, `SWELL_CreateDialog` returns null, `:137` returns, and the
toggle action is a **silent no-op** — no console line, no Actions-list checkmark. Three
defects stack inside the commented-out instructions themselves: the script named at `:96`
(`mac_resgen.php`) does not exist, the output filename is wrong, and the output is an
`#include`-only artifact that cannot be a `target_sources` entry at all.

**Surface boundary — owns:** `src/shell/panel/panel_window.cpp` (the `CreateDialogParam` call
at `:135–137`, the dialog proc's platform contract, the drop-accept opt-in at `:145–150`),
`src/resource.rc`, `src/resource.h`, and — **under the resgen route only** — one
`target_sources` line in `src/app/CMakeLists.txt`'s `else()` branch plus a new include-shim
TU. **Does not own:** any other panel TU, `draw_kit`, or any CMake target property.
`panel_window.cpp` is **deliberately not split** across tracks: the L2-06 diagnostic and the
Λ-01 resource route are the same function, and under the id-0 route the same *line*.

**Behavior.** Whichever route Λ-F2 picks, plus — **unconditionally, and on both platforms
rather than behind a guard** — a one-line `ShowConsoleMsg` on the `!g_panel.hwnd` path naming
the missing dialog resource (L2-06). That single line converts a mystery into a two-minute
diagnosis and is worth having on Windows too. The fix is shared macOS/Linux either way
(Λ-D4: made in shared form, verified on Linux only).

**Acceptance criteria.**
- The panel toggle action docks a visible, LICE-drawn bank panel in a Linux REAPER.
  `[verify — Linux]` = **V7**.
- With the resource deliberately unavailable, the toggle prints one console line rather than
  doing nothing. **Verifiable on Windows by forcing the branch.**
- Dragging a WAV from the file manager onto the docked panel ingests it — or, if it does not,
  the failure is understood rather than mysterious. `[verify — Linux]`, T2 §5 item 3.
  **This criterion's difficulty depends on the Λ-F2 route** and is the fork's substance.
- Windows panel behaviour byte-for-byte unchanged.

**Prerequisites.** **Λ-F2 must be ruled before dispatch.** If it is not, this track slips to
Λ-W4 and Λ-W3's sweep splits into a panel-independent half (run early) and a panel-dependent
half (run after this lands) — a second Linux session, which is the cost of leaving the fork
open. **Discharges:** Λ-01, L2-06, L2-07; enables V7.

#### Λ-W2-T4 — `locale-independent-numerics`

**Goal.** A persisted float round-trips identically regardless of process locale, so a bank
index written on a comma-decimal machine is not written unparseable.

**Spec:** `docs/product/linux-readiness.md` §Λ-W2-T4; finding L2-04.

**Why it is pulled forward, ahead of everything it looks like it should follow:** it is the
phase's one substantial fix that **needs no Linux box at all** — two writers and three
readers, all in `core/`, all unit-testable on Windows today — and its failure mode is that
the bank index is written unparseable and **the project's whole bank is lost on reload.** It
should land before any Linux user saves a project.

**Surface boundary — owns:** `src/core/json/json.cpp` (the `%.17g` writer and the `strtod`
reader), `src/core/model/provenance.cpp` (its own `%.17g`), `src/core/wire/wire.cpp`
(`Cursor::fieldDouble`), `src/core/capture/render_settings.cpp` (the `std::stod` over
REAPER's `P_RAZOREDITS`), and the corresponding `tests/`. **Does not own:** any shell TU, any
CMake file, `wav_codec` (its byte-order handling is already explicit and correct).

**Behavior.** Make the number codec locale-independent at its two writers and three readers —
`std::to_chars`/`std::from_chars`, or a `std::locale::classic()`-bound stream. **Do not "fix"
this by calling `setlocale`**; an extension must not mutate the host's locale. Why it is not
paranoia: on Windows the CRT starts in the `"C"` locale and nothing here calls `setlocale`,
which is why it has never fired; on Linux SWELL's GDK backend calls
`gtk_init_check`/`gdk_init_check` and never calls `gtk_disable_setlocale`, and any GTK or Qt
plugin in the same process can do the same. The readers are honestly fail-closed — they
require whole-token consumption — so the failure is "the field disappears", not "the field is
silently wrong".

**Acceptance criteria.**
- New pure tests pass **on Windows today**, under a forced comma-decimal `LC_NUMERIC` set
  inside the test. **This is the one Λ-W2 track that does not wait for Λ-W3.**
- The bank index, view model, tracking ledger, provenance blob and tail setting all round-trip
  a fractional value under that forced locale.
- **Byte-for-byte identical output to today's writer under the `"C"` locale** — this is a
  persisted format, and a changed representation is a compatibility event.
- Nothing on a hot path is touched; the JSON/persist path is declared off all hot paths in
  root `CLAUDE.md` and stays there.

**Prerequisites.** None. **Discharges:** L2-04.
**Note:** T2 §5 item 1 (read `LC_NUMERIC` inside a running REAPER-Linux process) stays in the
Λ-W3 sweep, but only to record how urgent this *was* — the work is not gated on it.

---

### Λ-W3 — First light, and the verification sweep

**Depends on:** Λ-W2. **One track, deliberately** — this is a person at a Linux box working a
checklist where each answer reprices the next; splitting it across specialists buys no
concurrency and loses the thread. Precedent: Ρ-W1 and Γ-W4 are both single-track waves for
the same reason.

#### Λ-W3-T1 — `linux-verification-sweep`

**Goal.** Discharge every acceptance criterion Λ-W2 could not check from Windows, answer every
`[verify — Linux]` item in both audits, and reprice the remaining waves against what is
actually true.

**Spec:** `docs/product/linux-readiness.md` §Λ-W3-T1 and §"Why this sequence".

**Surface boundary — owns: no source file and no CMake file.** Its deliverable is a
**verification record** at `docs/product/audit-notes/lambda-w3-verification.md`, one entry per
item: the exact check run, the observed result, and what it changes. **Any fix the sweep
motivates is filed to Λ-W4, not made here.** The one exception: a defect that blocks further
sweeping (the build does not link at all) is fixed in place and recorded as a deviation,
because the alternative is a wasted session.

**Behavior — the sweep set.**

| From | Items |
|---|---|
| T1 | **V1** compile, **V2** ctest, **V3** does REAPER's Linux build call `SWELL_dllMain` and populate the API table, **V4** where `UserPlugins/` actually is and whether `reaper_*.so` is the right glob, **V5** pthread link flag, **V6** visibility vs. the two exported symbols, **V7** the docked panel, **V8** `-lstdc++fs`, **V9** does `libSwell.so` sit beside REAPER's executable, **V10** which `uname -m` values the VST3 bundle must carry |
| T2 | process `LC_NUMERIC`; `REAPERAPI_LoadAPI`'s actual return value and the name of any gap; panel file-drop routing; which SWELL GDI/locale build REAPER ships; fontconfig's substitution for "Consolas"; `SWELL_InitiateDragDropOfFileList` acceptance by common targets, and whether its 500 ms no-motion timeout cancels a slow gesture; prune against an in-use file |
| New | the `-Wall -Wextra` diagnostic set, counted and categorised; whether `-Wl,--no-undefined` links clean or names an undefined set; whether the four-TU LICE slice links without `lice_colorspace.cpp` (T1 §3 leaves this an unreconciled inference) |

**Three items are load-bearing beyond their own answer and must be run FIRST:** **V1**
(nothing else is observable until it passes); **V9** (a negative answer **voids Λ-D2's route
and needs a Daniel re-ruling** before Λ-W6 is dispatched — the entire `dlopen`-REAPER's-SWELL
design rests on it, and it is a one-line `ls`); and the `REAPERAPI_LoadAPI` count (a non-zero
result promotes L2-03 from a diagnostic to a real Blocker and names the gap).

**Acceptance criteria.**
- Every item above has a recorded answer or an explicit "could not determine, because X".
  **A blank is a failure of this track, not a deferral.**
- Λ-W2's four tracks each have their `[verify — Linux]` criteria marked discharged or failed,
  **by name**.
- Λ-W4 and Λ-W5's scope is restated against the answers, with any effort band that moved
  called out. **The audits' bands are provisional by their own statement; this is where they
  stop being.**
- Λ-W6's prerequisites (V9, V10) are answered, or Λ-W6 is explicitly blocked pending a Daniel
  re-ruling on Λ-D2.

**Prerequisites.** Λ-W2-T1, T2, T4. **T3 if Λ-F2 was ruled** — otherwise the panel-dependent
items (V7, file-drop routing, any font check needing a rendered panel) defer to a second
session and that deferral is recorded. **Discharges:** V1–V10 and T2 §5's seven items, **as
answers rather than as fixes**.

---

### Λ-W4 — Correctness and safety, repriced

**Depends on:** Λ-W3-T1. **Three tracks, disjoint by directory.** Everything here is known
work whose *size* the sweep may have moved.

#### Λ-W4-T1 — `prune-deletion-safety`

**Goal.** Prune on Linux deletes only what it means to, tells the truth about what it
reclaimed, and tells the user the deletion is permanent.

**Spec:** `docs/product/linux-readiness.md` §Λ-W4-T1; finding L2-05 (both halves) plus the
symlink row of T2 §4.

**Surface boundary — owns:** `src/shell/persist/prune_fs.cpp` (the deletion authority's
non-Windows branch and the reclaim scan) and the prune confirmation text wherever it is
composed (`shell/actions/prune_action` / `shell/panel/panel_bank_ops`). **Does not own:**
`core/reclaim/`'s orphan computation (pure, portable, unaffected), or any other persist TU.

**Behavior.**
- **The confirmation string becomes platform-aware** (Λ-D5). On Linux it states the deletion
  is permanent and there is no Recycle Bin; on Windows it says what it says today. **Same code
  path, one platform-dependent phrase — not a second dialog.**
- **The dead "locked" branch is reckoned with.** `if (ec) return false; // real failure
  (locked/permission) -> skip` encodes Windows file-sharing semantics; on Linux `unlink` of an
  open file **succeeds**, the audio keeps playing from the open fd, and the bytes are gone
  when it closes — so the branch never fires. Either it is documented as Windows-only in
  place, or the Linux path acquires an equivalent guard. **What it must not do is stay
  silently asymmetric**: the recovery floor root `CLAUDE.md` §"The resample bake" relies on
  ("the superseded file survives on disk until a prune reclaims it") otherwise has nothing
  under it on Linux. **This half is not covered by Λ-D5's trash ruling** — it is not a trash
  question.
- **The symlink hazard is fixed.** `fs::directory_iterator` + `is_regular_file()` follows
  symlinks under C++17; size is read from the target via `file_size()` but `fs::remove`
  deletes the **link**, not the target — so prune reports N bytes reclaimed and reclaims
  zero. Symlinked media folders are far more idiomatic on Linux than on Windows. **This is a
  reporting lie, not a cosmetic issue.**

**Acceptance criteria.**
- A Linux prune of a bank file currently playing behaves as recorded in Λ-W3's sweep, and the
  behaviour matches what the confirmation promised. `[verify — Linux]`.
- A symlinked bank file is either skipped or deleted with its target, and the reclaimed-byte
  figure matches what actually left the disk in **both** cases. Unit-testable for the
  computation; `[verify — Linux]` for the filesystem half.
- The Windows path — `SHFileOperationW` + `FOF_ALLOWUNDO`, Recycle-Bin recoverable — is
  bit-for-bit unchanged.
- **Prune remains the only file-deletion path in the system** (plan-wide product invariant).

**Prerequisites.** Λ-W3-T1 (the in-use-file check). **Discharges:** L2-05 both halves, and the
symlink row of T2 §4.

#### Λ-W4-T2 — `linux-font-faces`

**Goal.** The kit asks for faces that exist on a stock Linux distro, so type is **chosen**
rather than substituted.

**Spec:** `docs/product/linux-readiness.md` §Λ-W4-T2 and §"Calls made here"; finding L2-09.
**This is a product-designer call, not a fork:** silent fontconfig substitution is not
acceptable because the kit's type is part of a deliberate visual identity
(`docs/product/visual-design-language.md`), and the palette-role discipline exists precisely
so a visual direction is a single-file change rather than an emergent property of the host.
The cost is five string literals. Contradict it in review with an argument.

**Surface boundary — owns:** `src/shell/panel/draw_kit.cpp`'s five `loadFont` call sites
(`:154–158`) and the face constants in `draw_kit.h`. **Does not own:** `loadFont` itself
beyond the literals, the palette, any geometry, or the two WCAG `static_assert`s — which are
on pixel height and weight, not on the face, and hold regardless.

**Behavior.** A platform face list at the five call sites: **DejaVu Sans / DejaVu Sans Mono**
as the Linux defaults, following SWELL's own no-fontconfig fallback list
(LiberationSans/DejaVuSans, LiberationMono/DejaVuSansMono) as precedent. **One code path**,
shared with macOS's eventual San Francisco/Menlo (Λ-D4: made in shared form, not verified).
The subtlety to carry into the work: `draw_kit.cpp:74`'s `if (!hf) return` guard does **not**
catch this failure mode — SWELL's `CreateFont` always returns a non-null handle even when the
face never resolved, recording the failure as a null `typedata` internally. **A wrong or
missing face is not observable at the call site, only in the rendering.**

**Acceptance criteria.**
- Every kit string renders in the intended face on a stock distro, and the numeric readouts
  are tabular. `[verify — Linux]`.
- Windows renders Segoe UI and Consolas exactly as today.
- Row heights, ellipsis points and label truncation are unchanged on Windows; on Linux they
  are **measured** rather than assumed correct.

**Prerequisites.** Λ-W3-T1's fontconfig answer (T2 §5 item 5), which decides whether this is
cosmetic or a readability regression. **Discharges:** L2-09.

#### Λ-W4-T3 — `source-partition-and-invariants`

**Goal.** The build's source list stops relying on every TU's own `#ifdef` discipline, and the
invariants Linux weakens are stated where a reviewer will read them.

**Spec:** `docs/product/linux-readiness.md` §Λ-W4-T3 and §Λ-F3; findings Λ-06, L2-10, L2-11.

**Surface boundary — owns:** the `target_sources` list in `src/app/CMakeLists.txt:8–51` (**the
list; the property blocks are Λ-W2-T2's**), any new platform-sibling TU the sweep showed was
needed, `src/shell/actions/drag_out_win.h`'s invariant comment, and the corresponding
`src/shell/**/CLAUDE.md` invariant passages. **Does not own:** any behaviour change in a
shipped code path.

**Behavior.**
- **Λ-06** — partition the source list where the Λ-W3 diagnostic set says a TU needs it.
  `drag_out_win.cpp` is the model (a real `#ifdef _WIN32` / `#else` split);
  `arrange_drop_win.cpp` and `instrument_drop_win.cpp` are `_win`-suffixed for the **surface**
  they serve, not for a platform dependency, and the audits found them portable by inspection
  — **confirm against the actual compile rather than re-inspecting.**
- **L2-10 — the copy-only invariant. Λ-F3 rules the wording; this track makes the edit either
  way.** Make `drag_out_win.h:7–11` the doc a Linux reviewer is pointed at, and treat "MOVE is
  structurally impossible" as a Windows-scoped claim: SWELL's file-list drag takes no effect
  mask, so nothing at the API level forbids a target from treating the drag as a move.
- **L2-11 — no Linux action.** If the predicate is touched at all it becomes
  "case-insensitive filesystem", not "Windows" — but Λ-D4 puts macOS out, so the right move is
  **a comment recording the known macOS defect, not a speculative fix.**

**Acceptance criteria.**
- No TU compiles on Linux only because of an `#ifdef` that happens to be complete; every
  platform-specific TU is either partitioned in CMake or carries a deliberate, commented
  guard.
- The copy-only invariant's text says **the same thing** in `drag_out_win.h`, the owning
  `CLAUDE.md`, and any spec text that cites it.
- Zero behaviour change on Windows.

**Prerequisites.** Λ-W3-T1. **Λ-F3 for the L2-10 wording only — non-gating:** the track can
land the partition and leave the sentence for a follow-up. **Discharges:** Λ-06, L2-10,
L2-11's Linux half.

---

### Λ-W5 — Ship the extension

**Depends on:** Λ-W3-T1 (V4) and **all three Λ-W4 tracks** — shipping means the correctness
fixes are in. **One track. Gated on Λ-F4** (a shipped artifact has to say what it runs on).

#### Λ-W5-T1 — `linux-packaging-and-install`

**Goal.** A Linux user can install a correctly-built `reaper_reasampler.so` by following a
document, and a pipeline can install it by following a rule.

**Spec:** `docs/product/linux-readiness.md` §Λ-W5-T1; finding Λ-08 plus Λ-02's shipped half.

**Surface boundary — owns:** an `install()` rule in `src/app/CMakeLists.txt` — **the first in
the tree** — `README.md`'s platform-support and install sections, root `CLAUDE.md`
§"Install / reload", and `docs/product/versioning-and-release.md`'s artifact and pipeline
paragraphs. **Does not own:** any source file, the toolchain properties (Λ-W2-T2's), or CI
(Λ-F1).

**Behavior.**
- Document the Linux `UserPlugins/` root **as V4 actually found it**, and the
  single-config-generator output path (`build/reaper_reasampler.so`) that no doc currently
  names.
- Add the `install()` rule for **both channels**. The channel fork is platform-independent by
  construction — `REASAMPLER_CHANNEL` threads through `configure_file` into names only — so
  `reaper_reasampler_beta.so` needs no separate mechanism, only a separate destination check.
- **Amend `versioning-and-release.md`**, which already promises three platform artifacts per
  channel per release and mentions no Linux install path, no signing and no CI. Λ-D4 removes
  macOS from the near-term promise: that document should say **two** platform artifacts per
  channel for now, **with macOS named as deferred rather than silently dropped.**
- **The beta channel on Linux is uncosted in both audits.** Almost certainly free — the fork
  is name-only — but "almost certainly" is not a ship criterion. Build and install both
  channels side by side once.
- **Λ-F1 decides only which paragraph gets written here** — a pipeline paragraph, or an
  explicit "deferred to dev-ops". Until it is ruled, **Λ ships nothing that presumes a
  runner.**

**Acceptance criteria.**
- A clean-machine walkthrough **following only the README**: clone, narrow submodule init,
  configure, build, install, restart REAPER, actions present, panel docks. `[verify — Linux]`.
- `reaper_reasampler.so` and `reaper_reasampler_beta.so` coexist in one REAPER with separate
  ext-state namespaces, command ids and dock idents. `[verify — Linux]`.
- The **installed** binary shows an explicit `-O` flag in its build log — Λ-02's criterion,
  re-checked on the artifact that actually ships.
- `versioning-and-release.md` no longer promises an artifact this phase does not produce.
- The declared support floor (Λ-F4) appears in the README and matches what was built.

**Prerequisites.** Λ-W3-T1 (V4), Λ-W4 (all three tracks), **Λ-F4 ruled**. **Discharges:**
Λ-08, and Λ-02's shipped half.

---

### Λ-W6 — The instrument module, and the host-safety contract

**Depends on:** Λ-W2-T2 (thread linkage, visibility, warnings) and Λ-W3-T1 (V10, plus V9 if
Λ-D2 is to survive). **Λ-W6 may run CONCURRENTLY with Λ-W4 and Λ-W5** — it touches
`src/shell/instrument/` and that directory's CMake and `CLAUDE.md` files plus
`src/core/instrument/CLAUDE.md`, none of which Λ-W4 or Λ-W5 opens; the only reason to
serialise is attention, not contention.

**Two tracks — one build + docs, one source — and they are `T1 before T2 — serial`**, the
same shape this plan already records for Γ-W3. They are file-disjoint but not order-free:
there must be a module before it can refuse to show a view.

**At the end of this wave the Linux instrument loads in any host, processes audio, and has NO
EDITOR AT ALL** — a defined, verifiable, shippable state, not a half-done one.

#### Λ-W6-T1 — `vst-linux-module`

**Goal.** `reasampler_9000.vst3` configures, builds and installs on Linux as the directory
bundle a Linux host expects, and the decision that forbade it is reversed in writing.

**Spec:** `docs/product/linux-readiness.md` §Λ-W6-T1; audit findings B1, B2, B5.

**Surface boundary — owns:** `src/shell/instrument/CMakeLists.txt` (the gate, the entry-point
source selection, the bundle POST_BUILD and install rule, `SWELL_PROVIDED_BY_APP`, the modstub
TU) and the three D5 passages in `src/core/instrument/CLAUDE.md`,
`src/shell/instrument/CLAUDE.md` and `src/shell/panel/CLAUDE.md`. **Does not own:** any `.cpp`
or `.h` under `shell/instrument/`.

**Behavior.**
- **B5 first, as a documentation act.** Rewrite the three D5 passages: the platform clause
  becomes "Windows and Linux; the editor is REAPER-hosted"; **VST3-only and REAPER-only
  survive verbatim.** `src/core/instrument/CLAUDE.md` carries it twice (Invariants and
  Non-goals), `src/shell/instrument/CLAUDE.md` once (Non-goals), and
  `src/shell/panel/CLAUDE.md` once (the font/GDI clause). **Nothing else in this wave may land
  before this does** — the invariant files are what a future implementer reads.
- **B1** — swap `public.sdk/source/main/dllmain.cpp` for `linuxmain.cpp` on Linux. `dllmain`
  includes `<windows.h>` with no `SMTG_OS_*` guard; `linuxmain.cpp` exports `ModuleEntry` and
  `ModuleExit`, **both mandatory** — the SDK's own loader refuses the module without either.
  Both files are already vendored; this is a source swap plus a platform `if()`.
- **Split the `WIN32 AND EXISTS` conjunction** at `src/shell/instrument/CMakeLists.txt:9`.
  Today it is a conjunction, so a Linux configure silently omits `reasampler_vst` **even with
  the submodule slice fully initialised.** The `EXISTS` half stays — a fresh clone with no
  VST3 slice must still configure — and the `WIN32` half becomes a Windows-or-Linux predicate.
- **B2** — the artifact becomes a **directory bundle**:
  `reasampler_9000.vst3/Contents/<uname -m>-linux/reasampler_9000.so`, per V10's answer and
  Λ-F4's floor. `Contents/Resources/moduleinfo.json` is **optional** — the SDK's
  `getModuleInfoPath` returns empty when absent rather than failing — **so do not author
  one.** Install roots: `$HOME/.vst3/`, `/usr/lib/vst3/`, `/usr/local/lib/vst3/`,
  `$APPFOLDER/vst3/`.
- **`SWELL_PROVIDED_BY_APP` + the modstub TU** are added to this target in the **default
  (non-`SWELL_LOAD_SWELL_DYLIB`) branch** — the same branch the extension already uses
  (`src/app/CMakeLists.txt:91–92`). The whole modstub file is inside
  `#ifdef SWELL_PROVIDED_BY_APP`, so the VST3 target must define that symbol; today it does
  not. **The `dlopen` itself is Λ-W7's**; what this track lands is the compiled-in, inert
  table plus the exported `SWELL_dllMain`.
- **The channel fork applies:** `reasampler_9000_beta.vst3` with its own class UID. **The UID
  pair is FOREVER-FROZEN and must not change** — a Linux build is a new platform, not a new
  identity, and a saved project rebinds by UID.

**Acceptance criteria.**
- A Linux configure produces the `reasampler_vst` target; a Windows configure is unchanged.
- The built bundle's directory shape matches what the SDK's `module_linux.cpp` opens, and the
  SDK's own `validator` loads it. `[verify — Linux]`.
- Both channels build and install side by side, and **the two class UIDs are byte-identical to
  the Windows build's.**
- The three `CLAUDE.md` files no longer claim Windows-only, and **still** claim VST3-only and
  REAPER-only.
- **No VST3 parameter is registered by this track or any other in this phase** — see the
  Γ-W4-T1 boundary above.

**Prerequisites.** Λ-W2-T2, Λ-W3-T1 (V10; V9 if Λ-D2 is to survive). **Discharges:** B1, B2,
B5; and Λ-10's "vst3sdk is optional on Linux" caveat becomes conditional.

#### Λ-W6-T2 — `vst-host-safety-contract`

**Goal.** The plugin's editor surface refuses **cleanly** on every Linux host, so that adding
a real editor later is a change of branch taken and not a change of contract.

**Spec:** `docs/product/linux-readiness.md` §Λ-W6-T2 and §"The non-REAPER-host safety
criterion"; Λ-D3, L2-08's fallback half.

**Surface boundary — owns:** `src/shell/instrument/editor_platform.cpp` (the
`isPlatformTypeSupported` / `createView` decision and the non-Windows stubs),
`reasampler_processor.cpp`'s `createView` site, `reasampler_embed.cpp`'s
`REAPER_FXEMBED_WM_IS_SUPPORTED`, and a new availability-probe seam under
`shell/instrument/`. **Does not own:** any CMake file, the paint or input families (untouched
— they stay whole-region-guarded), or any pure `core/instrument/` module.

**Behavior.**
- The editor's availability becomes a **tri-state probe** — untried / available / unavailable
  — computed **once, lazily, off the audio thread**. In this wave the Linux answer is
  unconditionally *unavailable*; **Λ-W7 gives it a real computation without changing a single
  call site.**
- `isPlatformTypeSupported` returns `kResultFalse` for **every** type when unavailable;
  `createView(kEditor)` returns `nullptr`. **Never a view that fails to attach.**
- **One** diagnostic line per process, naming the reason. Never a modal.
- `reasampler_embed`'s `REAPER_FXEMBED_WM_IS_SUPPORTED` continues to return 0 off Windows.
  That is correct and stays correct — the TCP/MCP embed strip on Linux is an explicit
  non-goal of this phase.
- **The Windows path is not restructured to accommodate this.** The probe is a Linux branch on
  an existing decision, **not a new abstraction over both.**

**Acceptance criteria.** The full observable contract above, verified by **all four** of its
checks: `validator` with no `libSwell.so` present; a real Ardour or Bitwig scan; a
REAPER-on-Linux load; and the renamed-`libSwell.so` negative control. `[verify — Linux]`.
Plus:
- Windows editor behaviour bit-for-bit unchanged — same window class, same `wndProc`, same
  `CS_DBLCLKS` fall-through.
- **Nothing added to `process()`** — no `dlopen`, no probe, no branch on the
  per-voice-per-sample path.
- **The criteria pass with ZERO VST3 parameters registered.** An empty generic UI is a pass;
  Γ-W4-T1 improves it and Λ must not.

**Prerequisites.** Λ-W6-T1 (there must be a module to load). **Discharges:** Λ-D3's Λ-W6 half;
L2-08's fallback half.

---

### Λ-W7 — The SWELL bootstrap

**Depends on:** Λ-W6-T1, Λ-W6-T2, and **V9 answered affirmatively.** If V9 is negative, **this
track does not exist and Λ-D2 needs a re-ruling** — which is why V9 runs first in Λ-W3 rather
than being discovered here.

#### Λ-W7-T1 — `swell-dylib-bootstrap`

**Goal.** The plugin acquires a working SWELL function table under REAPER on Linux, without
the vendored stub's `exit()` behaviour and without vendoring SWELL.

**Spec:** `docs/product/linux-readiness.md` §Λ-W7-T1 and §"The implementation shape this
obliges"; Λ-D2 (route B3a), audit finding B3.

**Surface boundary — owns:** a new `src/shell/instrument/swell_bootstrap.{h,cpp}`, the
availability-probe computation Λ-W6-T2 left stubbed, and the `SWELLAppMain`-shaped callback.
**Does not own:** any vendored file (**nothing under `vendor/` is patched**), the CMake source
list beyond adding one TU, or the editor.

**Behavior — exactly the four-point shape in the product doc, and it is not a re-litigation of
Λ-D2 but the specification of it.**
1. **Never define `SWELL_LOAD_SWELL_DYLIB`.** Compile `swell-modstub-generic.cpp` in its
   default branch, which exports `SWELL_dllMain(hInst, callMode, GetFunc)` and calls `doinit`
   on the pointer it is handed.
2. **Own the load.** Resolve the host executable's directory, `dlopen` `libSwell.so`, `dlsym`
   `SWELLAPI_GetFunc`, `dlsym` and call `SWELL_set_app_main`, then call our own exported
   `SWELL_dllMain(hinst, DLL_PROCESS_ATTACH, getfunc)` — **using only exported surface, with
   every failure returning a recorded state instead of exiting.** This also dissolves the
   `SWELLAppMain`-as-a-link-requirement noted in T1 B3a: we pass an app-main because we choose
   to, not because the linker demands one. `[verify — Linux]` whether a minimal app-main
   suffices for a plugin that only ever creates child windows inside a host-supplied X11
   window.
3. **Probe the host, not just the library.** A non-REAPER host has no SWELL message loop —
   REAPER's own is what pumps SWELL windows — so even a successful `dlopen` in Ardour would
   produce a window nothing drives. **Gate on the host name via `IHostApplication::getName()`
   AND on the `dlopen` succeeding**; either failing means no editor.
4. **Probe once, lazily, off the audio thread.** Tri-state, computed on first `createView` and
   never recomputed. No `dlopen` from `process()`, no per-`createView` retry, **no
   static-constructor work.**

**The failure taxonomy the probe must distinguish**, because they need different messages:
host is not REAPER / executable path unresolvable / `libSwell.so` not present at the resolved
path / `SWELLAPI_GetFunc` missing or version-mismatched (the stub checks
`SWELLAPI_GetFunc(NULL)==(void*)0x100`) / API table incomplete. **The last is not fatal** —
`doinit` substitutes a zero-returning `dummyFunc` per miss — and whether to accept a partial
table or refuse is **[propose at review]**, with *"refuse if any name the editor actually
calls is missing"* as the starting proposal.

**Acceptance criteria.**
- Under REAPER on Linux the probe reports *available* and the SWELL table resolves with **zero
  misses**. `[verify — Linux]`.
- Under `validator` with **no `libSwell.so` anywhere**, the probe reports *unavailable*, the
  process **exits normally**, and one diagnostic line is printed. **This is the direct
  negation of the `exit(2)` finding and is the track's headline criterion.**
  `[verify — Linux]`.
- Under Ardour or Bitwig **with** a `libSwell.so` reachable, the host-name gate **still
  refuses** — proving the gate is on the host, not only on the library. `[verify — Linux]`.
- No vendored file is modified; `git status` under `vendor/` is clean.
- Windows build unaffected — the whole TU is behind a platform guard.

**Prerequisites.** Λ-W6-T1, Λ-W6-T2, V9 affirmative. **Discharges:** B3 via route B3a; Λ-D3's
runtime half.

---

### Λ-W8 — The X11 editor

**Depends on:** Λ-W7-T1. **One track, and it is the phase's only L-band source item** (L2-08;
T1 B4 costs it "build S, source L"). Λ-D6 sequences it last on purpose: **halting this wave
still leaves a shipped extension and a loading, processing, generic-UI instrument** — a real
product, not a stub.

#### Λ-W8-T1 — `x11-embed-view`

**Goal.** The ReaSampler 9000 editor opens, draws and responds under REAPER on Linux.

**Spec:** `docs/product/linux-readiness.md` §Λ-W8-T1; audit findings B4, L2-08.

**Surface boundary — owns:** a **Linux sibling** to `src/shell/instrument/editor_platform.cpp`
(window creation and parenting, the run-loop timer, the event-driven input path), and the
`#ifdef _WIN32` region **boundaries** in `reasampler_editor.h` and the eight
`editor_input_*` / `editor_paint*` TUs — **boundaries only, not their contents.**
**Does not own:** `draw_kit`, any pure `core/instrument/` module, any painter's drawing logic,
`instrument_bake`, or the processor.

**Behavior.** The audits call this **a new competence rather than a port.** The build-side cost
is nil — the Linux IIDs are already in the vendored slice (`commoniids.cpp` defines
`Linux::IEventHandler`, `Linux::ITimerHandler`, `Linux::IRunLoop` under `#if SMTG_OS_LINUX`,
and that file is already in the `vst3_sdk` source list). What must be written:
- `kPlatformTypeX11EmbedWindowID` instead of `kPlatformTypeHWND`; `attachedToParent` receiving
  an X11 window id.
- A `Linux::IRunLoop`-driven timer replacing `SetTimer`/`WM_TIMER` — **including the editor's
  sync tick, which the bake's arm-then-run discipline rides on.**
- An **event-driven input path** replacing the `wndProc` switch. `RegisterClassW` /
  `CreateWindowExW` / `DefWindowProcW` have **no SWELL analogue at all** — SWELL has no
  window-class model, only `SWELL_CreateDialog` and raw `HWND__` construction.
- Substitutions the audit already resolved by name: `MoveWindow` → `SetWindowPos`;
  `GetWindowLongPtr`/`SetWindowLongPtr` → the non-`Ptr` forms returning `LONG_PTR`;
  `GetKeyState` → `GetAsyncKeyState`; `TrackMouseEvent`/`WM_MOUSELEAVE` → **nothing**, so
  hover-leave needs its own derivation (the panel layer already documents the same gap);
  `DragAcceptFiles`/`DragQueryFileW` → the drop path Λ-W2-T3 settled for the panel.

**A worked reference implementation is already on disk and neither audit named it.**
`vendor/vst3sdk/public.sdk/samples/vst-hosting/editorhost/source/platform/linux/` contains
`window.cpp` (returning `{kPlatformTypeX11EmbedWindowID, …}` and answering
`Linux::IRunLoop::iid` from `queryInterface`), `runloop.cpp`, and `irunloopimpl.h` (a
`RunLoopImpl` implementing `registerEventHandler` / `registerTimer` and their unregisters). It
is the **host** side of the contract rather than the plug-in side, which makes it a precise
specification of what our plug-in side must satisfy, and the documented narrow submodule init
already pulls it. **This does not shrink the L band** — it is a reading input, not a library;
it removes the "we are guessing at the contract" risk from the estimate.

**Acceptance criteria.**
- The editor opens, draws every face identically to the Windows build (compare screenshots at
  the same window size), and every drag, click, wheel and keyboard interaction behaves the
  same. `[verify — Linux]`.
- Hover-leave is correct on every hover surface, without `TrackMouseEvent`.
- **The bake's arm-then-run tick fires under `IRunLoop`, and a bake completes end-to-end on
  Linux:** staged file, extension action invoked over the VST3 host bridge, outcome read back,
  adopt and reset. `[verify — Linux]`.
- **Λ-W6-T2's contract still holds** — a non-REAPER host still gets `nullptr` from
  `createView`. **Re-run all four checks; this is a regression criterion, not a new one.**
- **Nothing added to `process()`**; no dispatch added to any per-sample path.
- Every new file lands under the ~600-line ceiling **with a responsibility seam, not a
  bisection.** The editor's existing band-axis split (`_chrome` / `_waveform` / `_deck` /
  `_browse` / `_curve`) is the seam vocabulary to reuse — the Linux platform TU is a **sibling
  to `editor_platform`**, per L2-08's own direction: *"it needs a sibling, not a rewrite."*
- Windows editor behaviour bit-for-bit unchanged.

**Prerequisites.** Λ-W7-T1. **Discharges:** B4, L2-08.

---

### Shared files across Λ's waves, named rather than discovered at merge

All textual adjacency, not semantic contention, unless marked otherwise.

| File | Tracks | Nature |
|---|---|---|
| `src/app/CMakeLists.txt` | Λ-W2-T2 (property + platform blocks), Λ-W2-T3 (one `target_sources` line, **resgen route only**), Λ-W4-T3 (the source list), Λ-W5-T1 (the `install()` rule) | Four disjoint regions of one file. Λ-W2-T2 and Λ-W2-T3 are the only pair in the same wave; one line each |
| `src/shell/panel/draw_kit.cpp` | Λ-W2-T1 (`loadFont`'s `CreateFont` args, `:73`), Λ-W4-T2 (the five call sites, `:154–158`) | Different waves |
| `src/shell/instrument/CMakeLists.txt` | Λ-W2-T2 (thread linkage), Λ-W6-T1 (gate, entry point, bundle, install), Λ-W7-T1 (one added TU) | Different waves |
| `src/shell/panel/panel_window.cpp` | Λ-W2-T3 alone | **Deliberately not split.** The L2-06 diagnostic and the Λ-01 resource route are the same function — under the id-0 route the same *line*. Splitting them would be semantic contention |
| `src/shell/instrument/editor_platform.cpp` | Λ-W6-T2 (the refusal branch), Λ-W8-T1 (the real branch) | Different waves; the second replaces the first's computation **without touching its call sites** |

---

## Traceability — all seventeen items

The check that nothing was dropped. Every row points at a track that exists above.

| # | Item (short) | Phase-Wave-Track | Worktree slug |
|---|---|---|---|
| 1 | Envelope editor: radio switch, curve dials, overlay recolor | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` |
| 2 | MM preamp Filter — resonant HP/LP stage | Θ-W1-T3 (DSP) **+** Θ-W2-T1 (integration) | `pth-w1-t3-filter-dsp-port`, `pth-w2-t1-filter-voice-path` |
| 3 | Alternative Spline EGs | Θ-W5-T1 | `pth-w5-t1-spline-egs` |
| 4 | Bug: end-of-sample click, Trigger × Preserve | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` |
| 5 | Bug: drag-out sometimes lands without audio | Θ-W1-T2 | `pth-w1-t2-capture-handoff-bugs` |
| 6 | Bug: FX-container drop loses the capture | Θ-W1-T2 | `pth-w1-t2-capture-handoff-bugs` |
| 7 | Stereo waveform shows both channels | Θ-W2-T2 | `pth-w2-t2-stereo-waveform-lanes` |
| 8 | Release anchoring; the Pitch AD becomes AHD | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` |
| 9 | Loop points — regression + Gate loop-sustain | Θ-W4-T1 | `pth-w4-t1-gate-loop-sustain` |
| 10 | Knob/label sizing, ms units, double-click reset | Θ-W6-T1 | `pth-w6-t1-legibility-and-antialiasing` |
| 11 | Preview glyph; VELOCITY deck; bipolar curves | Θ-W4-T2 | `pth-w4-t2-velocity-deck-and-bipolar-curves` |
| 12 | Toolbar cleanup; full-width piano strip; tooltips | Θ-W2-T3 | `pth-w2-t3-toolbar-and-piano-strip` |
| 13 | Antialiased rendering audit for high-DPI | Θ-W6-T1 | `pth-w6-t1-legibility-and-antialiasing` |
| 14 | Trigger amp/filter fade → AHD consolidation | Θ-W3-T2 | `pth-w3-t2-staged-envelope-curves` |
| 15 | One-click in-sampler resample | Ξ-W1-T2 (note model) **+** Ξ-W2-T1 (bake chain) **+** Ξ-W3-T1 (popup abandoned by ruling; window derives itself) | `pxi-w1-t2-note-program-model`, `pxi-w2-t1-resample-bake-chain`, `pxi-w3-t1-capture-signal-popup` |
| 16 | Retire the zone mapping system | Θ-W1-T1 | `pth-w1-t1-zone-retirement` |
| 17 | Consolidate provenance/usage tracking | Ξ-W1-T1 | `pxi-w1-t1-tracking-consolidation` |

### Work in this plan that is not one of the seventeen

The table above is a completeness proof over `TODO-1.0.md` — every row points at a track,
so nothing from the source was dropped. It is deliberately **not** an index of the plan:
work that did not come from the source doc has no row, and inventing one would weaken the
proof it exists to give.

- **Θ-W3-T1 — `live-parameter-delivery`** (`pth-w3-t1-live-parameter-delivery`). Arose
  from Θ-W2-T1's implementation review, not from `TODO-1.0.md`. The first such track in
  this plan; see Θ-W7-T1 below for the second.
- **Θ-W7-T1 — `arc-and-spline-aa`** (`pth-w7-t1-arc-and-spline-aa`). Opened after
  Θ-W6-T1 shipped, when Daniel found two rendering defects by eye in the editor — not
  from `TODO-1.0.md`, and not a track this plan originally scoped. The second such track
  in this plan today; if others appear, they belong on this list rather than in the
  table.
- **All of Phase Γ** (`pg-*`). **Thirteen tracks across four waves** (W1 seven, W2 two, W3
  three, W4 one), from a direct interview with Daniel (2026-08-01) and his four later rulings
  the same day, not from `TODO-1.0.md`. Listed
  here as a block rather than per track, because the whole phase is outside the source doc;
  the product reasoning lives in `docs/product/instrument-control-surface.md` and the
  parameter system's in `docs/product/parameter-automation.md` §§6–10. **Two `docs/TODO.md`
  entries are discharged by this phase, not deferred again:** Γ-W3-T1 discharges the
  deck-rework entry (whose original "one row of taller decks with within-deck stacking" shape
  Daniel explicitly superseded), and **Γ-W1-T1 discharges "Raise the stage-time ceiling above
  2 s"** (Γ-F3 reversed).
- **All of Phase Ψ** (`ppsi-*`). **Seven tracks across three waves**, from a direct list
  of seven defects and refinements (Daniel, 2026-08-01), not from `TODO-1.0.md`. Listed
  here as a block, like Γ; unlike Γ it has no backing product doc — the seven are
  recorded verbatim in the phase header as its provenance (Ψ.1–Ψ.7), and the design
  content lives inline in its tracks. The seventh track, Ψ-W3-T1, is not one of the
  seven defects/refinements itself — it came from a review finding mid-phase; see the
  Phase Ψ section for detail.
- **Γ-W3-T2 `bake-reset-amendment` is a CORRECTION, not a feature**, and belongs on this list
  for a different reason from the others: it exists only because Ξ-W2-T1 shipped ahead of this
  plan's sequencing claim. If more corrections of this shape appear, they belong here rather
  than in the table — the table is a completeness proof over `TODO-1.0.md`, and a correction
  has no source row to point at.
- **Γ-W3-T3 `bake-prints-limiter` is also a CORRECTION, not a feature**, and for the same
  reason as T2: it exists only because T2's own finding disproved the premise §3.4's reset
  classification rested on. Unlike the twelve above, it does not come from the 2026-08-01
  interview or that day's four rulings — it is a separate ruling, one day later (2026-08-02),
  opened after T2 landed and found the gap.
- **All of Phase Ε** (`pe-*`). **Six tracks across three waves**, from a direct request
  (Daniel, 2026-08-02), not from `TODO-1.0.md`. Listed here as a block, like Γ and Ψ; the
  product reasoning lives in `docs/product/bank-package.md`. It **supersedes nothing** —
  neither `docs/TODO.md` nor `docs/TODO-1.0.md` records export, import, or a package format,
  so there is no deferred entry to absorb or contradict. Its three [Daniel]-class forks
  (Ε-F1/F2/F3) were opened and ruled the same day it was framed, so no track here is gated on
  a decision; see "Decision state" above.
- **All of Phase Ρ** (`pr-*`). **One track in one wave**, from a direct request (Daniel,
  2026-08-02), not from `TODO-1.0.md`; the product reasoning lives in
  `docs/product/render-in-place.md`. It **supersedes nothing** — a sweep of `docs/TODO.md`
  and `docs/TODO-1.0.md` for `render.in.place|render to new track|preserve.source` returns
  nothing. The smallest phase in this plan, deliberately: it is a thin third verb composed
  out of machinery that already exists, and the burden was on any new machinery to prove it
  unavoidable. Its three [Daniel]-class forks (Ρ-F1/F2/F3) were opened and ruled the same
  day it was framed, so the track here is not gated on a decision; see "Decision state"
  above. **Ρ-F2 was the one that moved the spec** — the result track goes to Arrange
  unconditionally rather than following the active mode, which is also the only reason the
  phase touches `panel_input.cpp` at all.
- **All of Phase Λ** (`pl-*`). **Thirteen pending tracks across seven waves** (Λ-W2…Λ-W8),
  plus Λ-W1's two audit tracks, which are landed. From a direct request (Daniel, 2026-08-02),
  not from `TODO-1.0.md`; the product reasoning lives in `docs/product/linux-readiness.md`
  and the evidence in the two audit notes it cites. It **supersedes nothing** and **corrects
  one standing promise** rather than inheriting it: `docs/product/versioning-and-release.md`
  commits to three platform artifacts per channel per release, and Λ-D4 (macOS out) makes
  that two for now, with macOS named as deferred. **Λ is the one phase in this plan with
  unanswered [Daniel]-class questions** — four forks, none ruled, of which only Λ-F2 gates a
  dispatch and only Λ-F4 gates a wave; see "Decision state" above. It is also the one phase
  whose acceptance criteria cannot be checked from the current box at all, which is why
  Λ-W3 exists as a wave rather than as a checklist at the end.

### Deliberate compressions

Recorded so a reader of `TODO-1.0.md` can see what this plan did to the source, rather
than discovering it later:

- **Item 2 is split across two waves.** The DSP port (Θ-W1-T3) is deliberately separated
  from the integration (Θ-W2-T1) so the external-input dependency on Daniel's Cortex-M4
  code sits on a standalone, disjoint track instead of blocking a wave. Item 2's
  acceptance criteria are split accordingly — the range/resonance assertions land in
  W1-T3's tests, the audible/pipeline/deck criteria in W2-T1.
- **Item 2's filter envelope ships twice.** W2-T1 ships it in the existing staged AHDSR
  shape; W3-T2 gives it the curve treatment and the mode-driven Gate→AHDSR /
  Trigger→AHD shape. This is deliberate: waiting would put the filter behind the whole
  envelope system.
- **Item 4 lands in Θ-W3, not Θ-W1.** Its fix region is the region item 14 retires, and
  the only earlier instrument-side track owns the entire engine. Its acceptance gate is
  stated as the post-consolidation gate. It is not gated behind the *whole* editor chain
  — three waves of six — and the source doc itself requires re-verification under the
  surviving mechanism either way.
- **Item 11's filter velocity curve is split from item 2's filter velocity
  modulation.** W2-T1 wires the modulation path (following the amp/pitch precedents);
  W4-T2 sets the curve's bipolar domain and default and homes its button. Neither track
  can do the other's half.
- **Item 15's undo/recovery is carried as a note, not a criterion** — Daniel set it at
  exactly "a plus." The guaranteed recovery floor (the superseded file surviving until
  prune) is a criterion.
- **Nothing else was compressed.** Every other item's behavior bullets and acceptance
  criteria are carried at full strength into the track that owns it.

---

## Outline at a glance

```
Phase Θ — ReaSampler 9000: one parameter set, filter, shapeable envelopes, legible editor
  W1  Collapse and re-seam
      T1 zone-retirement ......................... 16
      T2 capture-handoff-bugs .................... 5, 6
      T3 filter-dsp-port ......................... 2 (DSP)      [needs Daniel's M4 code]
  W2  Filter in the voice path; waveform and chrome bands
      T1 filter-voice-path ....................... 2 (integration)
      T2 stereo-waveform-lanes ................... 7
      T3 toolbar-and-piano-strip ................. 12
  W3  Live parameters, then the staged envelope system      [T1 before T2 — serial]
      T1 live-parameter-delivery ................. (not one of the seventeen)
      T2 staged-envelope-curves .................. 1, 8, 14, 4
  W4  Loop sustain and the velocity deck
      T1 gate-loop-sustain ....................... 9
      T2 velocity-deck-and-bipolar-curves ........ 11
  W5  Spline EGs
      T1 spline-egs .............................. 3
  W6  Editor legibility pass
      T1 legibility-and-antialiasing ............. 10, 13
  W7  Arc-and-spline antialiasing fix
      T1 arc-and-spline-aa ....................... (not one of the seventeen)

Phase Ξ — The resample loop            (W1 concurrency-safe with Θ from Θ-W2 onward)
  W1  Consolidated tracking, and the programmed-note model
      T1 tracking-consolidation .................. 17
      T2 note-program-model ...................... 15 (model)
  W2  The bake chain            [ran AHEAD of Γ; its reset list is corrected by Γ-W3-T2]
      T1 resample-bake-chain ..................... 15 (chain)
  W3  The capture-signal popup      [popup abandoned by ruling; bake window derives itself]
      T1 capture-signal-popup .................... 15 (popup abandoned; window derives)

Phase Γ — The instrument's control surface      (none of the seventeen; ends with VST3 params)
  W1  Foundations — landed                   [seven tracks, disjoint by surface]
      T1 knob-interaction-law ....... modifiers + ONE taper module + reset bypass
                                      + 10 s ceiling + AHDSR schematic axis   [Ruling 2]
      T2 master-bus-audio ........... limiter + meter ballistics + dynamic PDC [rung 1]
      T3 contour-trace-curves ....... staged traces draw curved, knot on its trace
      T4 editor-floor-and-row-law ... floor 1190x680 (W3-T1: 1198) + budget constants + row predicate
      T5 preserve-time-stretch ...... real stretcher      [measure-and-report gate]
      T6 exhaustive-switch gate on pure libraries ... /we4062, -Werror=switch on
                                      pure libraries   [no PLAN entry — see COMPLETED.md]
      T7 psola-preserve ............. PSOLA-aligned splice jump; period_detect
                                      [cadence closure — see COMPLETED.md]
  W2  New controls, and the overlay's marks — landed  [2 tracks]
      T1 pitch-rate-deck ............ Rate + Pitch, Varisp/Presrv compounding  [rung 2]
      T2 loop-crossfade-ux .......... four-mark grammar; fade painted where it is heard
  W3  The reflow, and the bake correction    [3 tracks; T1/T2 landed, T3 open]
      T1 deck-reflow ................ two rows + double-height MASTER, by construction
      T2 bake-reset-amendment ....... the Xi correction Gamma owns
      T3 bake-prints-limiter ........ NOT STARTED — bake to print the limiter
                                      through the master stage   [Daniel, 2026-08-02]
  W4  VST3 parameters                        [1 track]
      T1 vst3-parameter-set ......... 44 derived params, frozen id table     [Ruling 1]
                                      [rung 3 RESERVED, spent only if verify says so]
                                      [OPEN: Gamma-F7, the parameter order — Daniel]

  Resequenced 2026-08-01, three times: the reflow split canvas (W1-T4) from arrangement
  (W3-T1); preserve-time-stretch moved W4 -> W1-T5, retiring Rate's interim stand-in; then
  Ruling 1 added W4 and Ruling 2 grew W1-T1.
  Payload rungs are RELATIVE, not absolute. On this branch at 2026-08-01, the first is already
  spent — kParamsPayloadVersion == 15 (Γ-W1-T2, landed, above) — the second is the next free
  rung, v16 (Γ-W2-T1, stated there), and the third remains RESERVED as v17, spent only if
  Γ-W4-T1's storage verification forces it.
  Shared files, named: engine/CMakeLists.txt (W1-T2 | W1-T5), ui/CMakeLists.txt
  (W1-T1 | W1-T3), editor_session.cpp (W2-T1 | W2-T2) — all textual adjacency, not
  semantic contention. W3-T2's disjointness from W3-T1, once a CONDITIONAL risk to confirm
  against what Xi-W2-T1 shipped, is resolved: both tracks landed — see docs/COMPLETED.md.

Phase Psi — The extension trust pass          (none of the seventeen; a direct list of seven)
  W1  Exact bounds, disciplined switches, reachable actions, resolved drops  [4 tracks]
      T1 capture-range-exactness ..... Psi.7   [opens with a DAW repro matrix]
      T2 mode-switch-discipline ...... Psi.2 + Psi.3   [one chokepoint: applyMode]
      T3 media-explorer-section ...... Psi.4   [BOTH sections; new FOREVER-STABLE id]
      T4 drop-target-resolution ...... Psi.5   [gesture law, not a patch]
  W2  Names and channels, over the settled render block   [gated on W1-T1's render block]
      T1 capture-naming .............. Psi.1   [+ the card shows the name]
      T2 mono-collapse ............... Psi.6   [lossless only; ingest excluded]

  Three invariant amendments are track deliverables: never-touch-solo (W1-T2, three
  files), the action-registration contract (W1-T3, root CLAUDE.md), channel-count-
  preserved (W2-T2, root CLAUDE.md:208).
  Shared files, named: panel_input.cpp (W1-T2 footer block | W1-T4 drag-arm block);
  capture.cpp + capture_realtime_finalize.cpp (W2-T1 naming lines | W2-T2 channel
  lines) — all textual adjacency, not semantic contention. main.cpp is W1-T3's
  exclusively.

Phase Epsilon — The bank package        (none of the seventeen; a direct request 2026-08-02)
  W1  The contract, the filesystem, and the ledger's new kind   [3 tracks, disjoint by dir]
      T1 package-format .............. core/package: framing + TWO version ints
                                       [E-F1 RULED: proprietary RSBK. No ZIP, no zlib]
      T2 package-fs-shell ............ shell/package: atomic write, streaming, pickers
                                       [ONE picker: REAPER GetUserFileName, mode 0/1]
      T3 import-origin-kind .......... OriginKind::PackageImport = 5, append-only
  W2  The two verbs                    [2 tracks; disjointness CONDITIONAL — see below]
      T1 bank-export ................. export_plan + verb + action; project untouched
      T2 bank-import ................. import_plan + verb + action + .rsbank drop
                                       [E-F2 RULED: always a NEW bank, auto-suffix, no
                                        merge. E-F3 RULED: REFUSE on degraded ledger]
  W3  The compatibility fixtures       [1 track]
      T1 package-compat-fixtures ..... frozen bytes prove BOTH version directions

  Version policy, both directions: older package in newer build ALWAYS imports (additive
  fields default, unknown keys skipped); newer package in older build REFUSES WHOLE with a
  three-part message (package needs / this build reads / writer semver). The gate is
  minReaderVersion <= kPackageFormatVersion — formatVersion is for the message, not the gate.
  All three E-forks were ruled 2026-08-02, the day the phase was framed: NO track here is
  gated on a decision. Import name collision = first free of seed, "seed 2", "seed 3", ...
  seed never re-parsed, fold is BankBook's own.
  Shared files, named: root CMakeLists.txt add_subdirectory list (W1-T1 | W1-T2, one
  append-only line each); main.cpp + the panel bank menu (W2-T1 | W2-T2, one registration
  line and one menu row each); bank_book.{h,cpp} is W2-T2's alone (one additive public const
  member). W1's three tracks are unconditionally disjoint; W2's two are
  textually adjacent only — serialize T2 behind T1 if zero contention is wanted.

Phase Rho — Render in place             (none of the seventeen; a direct request 2026-08-02)
  W1  The verb                                          [ONE track, deliberately]
      T1 render-in-place ............. render selected track -> new sibling track,
                                       item placed at the exact render position,
                                       colour + "Capture " name cloned, source -> Design,
                                       result track -> ARRANGE always. Never the bank.
                                       [R-F1 RULED: refuse multi-track, settled non-goal.
                                        R-F2 RULED: result track ALWAYS Arrange, never
                                        mode-following. R-F3 RULED: follow panel tail]

  The THIRD VERB. Capture = arrange -> bank (writes it). Placement = bank -> arrange
  (reads it). Render in place = arrange -> arrange (bank on neither side). That is why the
  load-bearing capture/placement rule survives it; the full argument is in the phase header
  and in docs/product/render-in-place.md.
  Structural boundary, not a convention: the render destination reaches the backend as a
  TWO-VALUED ENUM (Bank | ProjectMedia), never a caller-supplied path — so no Rho caller can
  name the bank folder. A renderDir string on CaptureRequest IS the drift.
  Genuinely new: ONE pure sibling-placement function (folder-parent and last-in-folder are
  both audibly wrong if inserted at srcIdx+1), an idempotent "Capture " prefix, a
  deriveRenderPaths sibling, and CaptureResult::absolutePath. Everything else composes.
  R-F2 fallout, the ruling's only cost: the result track AND its item are tagged
  kArrangeModeId explicitly, and detectNewContent must drop added GUIDs that already carry
  a membership record — the auto-tag detector otherwise re-tags a Design-fired result to
  Design on its next tick and reverses the ruling. Firing from Design is therefore
  deliberately invisible: the render waits in Arrange. No A/B-on-the-bench behaviour exists.
  Four invariant amendments are track deliverables (the Psi precedent): shell/capture and
  shell/actions CLAUDE.md placing-path claims, ONE sentence in root CLAUDE.md naming
  the third verb without softening the prohibition, and core/view CLAUDE.md's "new tracks
  are tagged to the active mode" made conditional on carrying no membership record.
  Shared files, named: capture.{h,cpp} (destination enum + result field), main.cpp (one
  ActionTableRow), and panel_input.cpp (detectNewContent only; Psi's two named regions in
  that file are landed and are other functions) — the only pre-existing shell files
  touched. Disjoint from Gamma (core+shell/instrument) and Epsilon (core+shell/package).

Phase Lambda — ReaSampler on Linux      (none of the seventeen; a direct request 2026-08-02)
  W1  The audits                                                          [COMPLETE]
      T1 build-toolchain-audit ....... L-01..L-10, V1..V10, D1..D7
      T2 source-runtime-audit ........ L2-01..L2-12

  W2  Make it buildable, and make it honest             [4 tracks, file-disjoint]
      T1 linux-compile-blockers ...... L2-01, L2-02, L2-03      -> V1, V2
      T2 toolchain-floor ............. L-02..L-05, L-07, L-09, L-10  -> V5, V6, V8
      T3 panel-dialog-resource ....... L-01, L2-06, L2-07       -> V7   [GATED: L-F2]
      T4 locale-independent-numerics . L2-04     [the one track verifiable on Windows]
  W3  First light, and the verification sweep           [1 track, deliberately]
      T1 linux-verification-sweep .... V1..V10 + T2 section 5's seven; the deliverable
                                       is a RECORD, not a fix. V1 / V9 / the API-load
                                       count run FIRST — each reprices what follows.
  W4  Correctness and safety, repriced                  [3 tracks, disjoint by dir]
      T1 prune-deletion-safety ....... L2-05 both halves + the symlink reclaim lie
      T2 linux-font-faces ............ L2-09
      T3 source-partition-and-invariants . L-06, L2-10, L2-11 (Linux half)
  W5  Ship the extension                                [1 track; GATED: L-F4]
      T1 linux-packaging-and-install . L-08 + L-02's shipped half + both channels
  W6  The instrument module + the host-safety contract  [T1 before T2 — serial]
      T1 vst-linux-module ............ B5 (the D5 reversal, FIRST), B1, B2
      T2 vst-host-safety-contract .... D3's observable contract; passes with ZERO params
  W7  The SWELL bootstrap                               [1 track]
      T1 swell-dylib-bootstrap ....... B3 via route B3a, WITHOUT the stub's exit(2)
                                       [prereq: V9 affirmative, else L-D2 re-rules]
  W8  The X11 editor                                    [1 track]
      T1 x11-embed-view .............. B4, L2-08. The phase's one L-band source item.

  NOTHING here has been verified on a Linux machine. W2's edits are authored BLIND from the
  audits' citations and their acceptance criteria are discharged in W3 — W2 is not "done"
  until W3 runs, and this plan says so rather than pretending otherwise.
  W6 may run concurrently with W4 and W5 — file-disjoint; the only reason to serialise is
  attention. Six D-rulings are SETTLED (instrument in scope; SWELL via dlopen of REAPER's
  libSwell.so; REAPER-only editor but other hosts must degrade SAFELY; macOS out; shipped
  not developer-only; hard unlink acceptable with a platform-aware confirmation).
  OPEN, none ruled: L-F1 (CI — gates nothing), L-F2 (dialog-resource route — the ONLY fork
  gating a dispatch, W2-T3), L-F3 (copy-only invariant wording — gates nothing),
  L-F4 (declared support floor — gates W5, not a dispatch).
  Lambda registers NO VST3 parameter, ever — that table is Gamma-W4-T1's and is frozen.
```
