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

## 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`**. So Θ-W1-T1
dispatches into `pth-w1-t1-zone-retirement`, Γ-W1-T1 into `pg-w1-t1-knob-interaction-law`,
and Ψ-W1-T1 into `ppsi-w1-t1-capture-range-exactness`.

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

Θ-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* is self-inflicted (`setActive(true)` calls `reloadInstrument`), so
the cost is ours to reduce and the reduction is filed in `docs/TODO.md` rather than designed
around.

**Γ-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.**

**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 has an unresolved cross-artifact seam, and it is the phase's largest
   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 requires that crossing. Ξ-W2-T1 opens with the
   architecture decision and this plan names the candidates rather than assuming the
   deferred relay shape; see that track. Consequence to hold: if every candidate fails
   verification, item 15's "one click from inside the VST" framing is what gives, not
   the read-only invariant — the fallback is a bindable extension-side action.

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.

---

### Ξ-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 — note length
stays musical-division-only, and an offset stores the denomination it was entered in.

---

### Ξ-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 that the popup programs and its preview must not
diverge from. The acceptance criterion "preview and bake cannot diverge" requires one
render path, which W2 owns — building the preview against a second path is exactly the
defect the criterion exists to catch.

**One track.**

#### Ξ-W3-T1 — `capture-signal-popup`

**Goal.** Let the user program the capture signal — note length, offsets, velocity — and
hear it before committing the bake.

**Consolidates item 15 (the popup sub-feature).**

**Surface boundary — owns:** `core/instrument/ui/curve_popup`'s sibling — a new pure popup
geometry module for the capture-signal sheet (mirror the `curve_popup` precedent: centered
sheet, width/height clamps, title row, Close button rect, outside-sheet dismissal test) —
plus the editor's popup paint/input wiring in the deck or chrome band, and the preview
trigger's call into W2's render path. Reads Ξ-W1-T2's note-program record; does not
re-own it.

**Behavior.**
- **A popup menu programs the capture signal:** **note length**, **start and end offsets —
  in ms AND in beats** — and **velocity**.
- **A preview trigger button** auditions the capture note exactly as currently programmed
  — the user hears the bake before committing it — and the offline pass renders that same
  programmed performance.
- **Note length is a musical-division picker** spanning 1/64th to 64/1 with dotted and
  triplet multipliers.
- **Offsets are anchored** — start to note-on, end to note-off — and each is readable and
  editable in **both** ms and beats, the two views of one stored value.
- **Beat-denominated values resolve against the project tempo under the cursor.** The
  shell reads the tempo; the arithmetic is Ξ-W1-T2's.
- **The programmed velocity is the render velocity** — material because the velocity
  transfer curves modulate amp, pitch, and filter at that velocity.
- House-consistent: drawn through the shared kit by palette role, geometry pure and
  CTest-covered, no decorative animation.

**Acceptance criteria.**
- The popup exposes: note length as a musical-division picker spanning **1/64th to 64/1
  with dotted and triplet multipliers**; start and end offsets, **each readable and
  editable in both ms and beats**, anchored to note-on and note-off respectively; and
  velocity.
- **Its preview trigger auditions the capture note exactly as programmed, and the bake
  renders that same programmed performance — preview and bake cannot diverge.** Verify
  structurally (one render path), not just by ear.
- **Beat-denominated values resolve against the project tempo under the cursor:** the same
  programmed division yields a correspondingly different rendered duration when the tempo
  at the cursor differs.
- The programmed signal persists with the instance and round-trips save/reload.
- The popup's geometry and dismissal test are pure and unit-tested; no hit-test math lands
  in shell code.

**Open questions.** Both of Ξ-W1-T2's residuals (negative offsets; the note-length
denomination seam) surface here as UI consequences — if T2 answered them, this track
implements the answer; if T2 deferred either, this track is where it becomes visible and
must be closed.

---

## 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.
- **Γ-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 1190 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 makes the cycle expensive **here** is entirely
our own doing: `ReaSamplerProcessor::setActive(true)` calls `reloadInstrument()` — a bridge
read plus a full WAV re-decode (`reasampler_processor.cpp:89-97`) — where a typical plugin's
`setActive` only allocates and frees buffers, and the deactivate side's freeing of
`live_`/`draining_`/graveyard (`:98-107`) is likewise our own design. **The cost is therefore
ours to reduce if it ever matters, and the reduction is decoupling reload from activation —
not abandoning dynamic latency.** That improvement is filed as a `docs/TODO.md` entry with its
trigger condition; it is not scheduled in this phase.

**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 1190 × 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 `isLiveDeckParam` 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 **13** (`map/component_state_io.h:154`) 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 v14 / v15 / v16-reserved.** If Ξ-W2-T1 landed
> a bump, every number shifts 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 `isLiveDeckParam` / `liveCommitFor`, and no list that can drift from it.
- **The window floor is 1190 × 680 and must not exceed 1280 × 720.** **Γ-W1-T4 sets it, in
  wave 1; no other track in the phase may move it**, and 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 **90 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. **Five tracks, disjoint by surface** — re-verified
against this membership rather than carried over from the four-wave shape:

| Track | Owns |
|---|---|
| **T1** `knob-interaction-law` | a **new pure taper module** under `core/instrument/ui/`, `ui/deck_values`, `ui/envelope_overlay` + `ui/envelope_edit` (the AHDSR schematic axis and its drag inverse), `ui/param_slider`, the three `shell/instrument/editor_input_*` drag paths, `editor_controls.cpp`'s `envClampBounds` only, the shared modifier helper in `editor_internal.h` |
| **T2** `master-bus-audio` | new pure `engine/limiter` + `engine/meter_ballistics`, `shell/instrument/reasampler_processor` + `processor_state`, `map/component_state_io` + `params_payload` (**the wave's payload rung**) |
| **T3** `contour-trace-curves` | `shell/instrument/editor_paint_waveform.cpp`'s staged trace + a **new pure** tessellation module |
| **T4** `editor-floor-and-row-law` | `ui/sample_bands.h` (the floor), `ui/knob_deck.h` (budget constants + two invalidated header notes), `ui/deck_groups` (the row predicate **only**), five test fixtures |
| **T5** `preserve-time-stretch` | `engine/pitch_shift` + a new pure stretcher module, `engine/voice.{h,cpp}`'s Preserve read path |

**Two shared files in the wave, named rather than discovered at merge.**
`src/core/instrument/engine/CMakeLists.txt` — T2 declares two new pure libraries and their
test targets there, T5 declares one. **And, newly, `src/core/instrument/ui/CMakeLists.txt`** —
T1 declares the taper module and its test target, T3 declares the tessellation module and
its. All four are append-only additions in separate blocks — **textual merge adjacency, not
semantic contention.** Whichever lands second rebases.

**Three near-misses that are avoided by construction, and must stay avoided.**

(a) **T3's tessellation helper lands in a NEW pure module — explicitly NOT
`ui/envelope_overlay`, which T1 now owns**, and not in `editor_internal.h`, which T1 is also
editing. The prior wording offered `envelope_overlay` as an option; Ruling 2 removed it,
because T1's schematic-axis work rewrites that module's whole time→x map. This still
satisfies the phase's geometry-stays-pure criterion, so it costs nothing.

(b) **T1 and T3 are disjoint by file but coupled by data, and the coupling has a stated
resolution.** T1 owns where an AHDSR's vertices *land*; T3 owns the stroke *between*
vertices. T3's tessellation is over φ across a segment's pixel span, so the tapered axis
changes nothing about the curve it draws — **but T3's tests must assert against the returned
vertices, not against absolute pixel literals**, or they break when T1 lands. Whichever
track lands second rebases; expressing T3's assertions relatively makes that rebase free.

(c) **T4 touches `deck_groups` but adds only the new row predicate**; it does not touch
`sampleDeckGroups`, which W2-T1 and W3-T1 own in later waves, and it does not touch
`deck_values`, which is T1's.

**Two consumption boundaries worth stating, because they look like collisions and are not.**
T1 **consumes** `engine/master_gain`'s dB taper for its whole-dB snap and does not edit it;
T2 does not touch it either. And T2's payload rung is the wave's only format change —
T1's taper and ceiling changes are persistence-neutral by construction (the payload stores
raw engine doubles).

**Both of the phase's DSP unknowns are in this wave** — T2's limiter and T5's stretcher. That
is deliberate: they are the two tracks whose gate can fail, and failing in wave 1 is
recoverable in a way that failing in the last wave is not.

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

**Goal.** One consistent, unit-category-driven interaction and taper rule across every
variable control, **over a stage-time range raised 2 s → 10 s**, landed **before** any new
control is added so the new ones are authored into it rather than retro-fitted — and before
any parameter is declared, so the law is what the host is handed rather than something the
host has to be reconciled with later.

**Spec:** `docs/product/instrument-control-surface.md` §4, **§4.3.1 (the 10 s ceiling and the
overlay-legibility design — new, read it before scoping this track)**, and
`docs/product/parameter-automation.md` §8 (the one-way-door sweep this track discharges).

**Surface boundary — owns:** a **new pure taper module** under `core/instrument/ui/` (the
ms/semitone/exponent maps, extracted so they have one home),
`core/instrument/ui/deck_values` (the bindings, the snap-unit table, `resetDeckParam`),
`core/instrument/ui/envelope_overlay` (the ceiling constant **and** the AHDSR schematic
axis) and `core/instrument/ui/envelope_edit` (its drag inverse),
`core/instrument/ui/param_slider` (the drag law), `shell/instrument/editor_input_*`
(modifier read + re-anchor), `shell/instrument/editor_controls.cpp`'s `envClampBounds`
**only** (it reads `kEnvTimeMaxSeconds`), and the modifier-reading helper the three input
paths share. **Does not own** any deck descriptor, any parameter, the waveform painter, or
`engine/master_gain` (consumed, not edited).

**Why this track does NOT split, asked and answered.** Ruling 2 makes it materially bigger —
tapers, modifiers, re-anchor, reset bypass, the ceiling, and the overlay's schematic scale.
Two splits were considered and both are **serial, not parallel**, so neither buys any
concurrency: an *interaction* half (modifiers, snap, re-anchor) needs the *domain* half's
taper and snap-unit table to exist first; and a standalone *overlay-axis* track needs the
taper module and the final ceiling before it can define a stage's slot width. Splitting
would therefore cost a wave and gain nothing, while putting the single most
identity-critical function in the phase across a wave boundary — the same function the host
will normalize against three waves later. **The seam that matters is internal and is a
deliverable: the taper is extracted into its own pure module**, which is what makes "the
taper IS the host-facing normalization" structurally true rather than a comment. The
~600-line ceiling is a per-file bar, and the extraction is what keeps every file under it.

**Behavior.**
- **Shift snaps to whole numbers in the control's displayed unit**; **Ctrl scales the drag by
  0.05**; **Shift+Ctrl = Shift wins** (Ctrl is ignored — with an integer-quantized output a
  finer drag yields the same sequence, so this is identity, not a compromise).
- **Snap unit by category:** ms knobs → whole ms; semitone knobs (incl. Rate, when it
  arrives) → whole semitones; percent/fraction knobs → whole percent; the 12 curve-exponent
  inner dials → whole numbers (which puts 1.0, the linear neutral, one snap away); master
  gain → whole dB; already-integer controls unchanged. Full table in the spec §4.2.
- **Mid-drag modifier transitions re-anchor** — on every press *and* release during an active
  drag, the current value becomes the anchor value and the current cursor position the anchor
  position. The value is continuous across the transition; only the rate changes. Without
  this the grab-anchored absolute drag (`kKnobDragRangePixels = 128`) jumps by
  `(1 − 0.05) ×` the accumulated delta.
- **Millisecond knobs become log-scaled.** Exactly 0 s at norm 0 and exactly
  `kEnvTimeMaxSeconds` at norm 1, monotone throughout; **10 ms lands within 0.12–0.20 of
  travel and 100 ms within 0.42–0.52**.
- **The stage-time ceiling moves 2.0 s → 10.0 s** (Daniel, reversing Γ-F3):
  `kGateStageMaxSeconds` (`envelope_overlay.h:85`) and, through it, `kEnvTimeMaxSeconds`
  (`deck_values.h:22`). **The two move together or not at all** — `deck_values.h` reads the
  overlay's constant rather than restating it precisely so they cannot drift
  (`deck_values.h:19-22`). The taper's landmarks above are fit against the **new** ceiling,
  which is why the ceiling cannot be a follow-up: fitting the taper twice is the only other
  way to get there.
- **`resetDeckParam`'s bypass becomes MANDATORY rather than merely required-anyway.**
  `deck_values.h:42-46` records that exact default recovery depends on the ceiling being a
  power of two; **2.0 is, 10.0 is not**, and the log taper compounds it. Nothing here may be
  "simplified" back into a norm round-trip under any circumstance.
- **NEW, and the sharpest requirement in the track: every default must have an EXACT
  normalized preimage under its taper.** `ParameterInfo::defaultNormalizedValue` (Γ-W4-T1)
  is normalized, so a host's reset-to-default arrives as `toPlain(defaultNorm)` — and **the
  host has no `resetDeckParam` bypass to use**. The bypass fixes the editor's reset and
  cannot fix the host's; only exactness in the map itself makes the two land on the same
  value. This binds the taper's *shape*, so it belongs here and cannot be handed forward.
  Master gain's unity (≈ 0.714 norm) is the case where a hair off is audible.
- **The AHDSR overlay's schematic axis becomes the taper — the ceiling's real cost, and it
  is design work, not a constant change.** Each of the four timed stages gets an equal slot
  and today maps seconds across it linearly (`gatePxPerSecond`,
  `envelope_overlay.cpp:33-34`). At 2 s a 30 ms attack is 1.5 % of its stage's domain; **at
  10 s it is 0.3 %, under a pixel at the floor width.** The fix: a stage's slot width becomes
  `slotPx × taperNorm(seconds)` instead of `slotPx × seconds / ceiling`, so a node's position
  within its slot **is** its knob's needle position. Legibility becomes ceiling-independent by
  construction; the one-model invariant gets stronger rather than strained; and **the drawn
  curve is unaffected**, because the taper decides only where a stage's end node lands while φ
  still runs linearly across the stage's pixel span — so Γ-W1-T3's φ^p trace composes with it
  rather than fighting it. **The AHD policy is untouched**: an AHD maps 1:1 onto the
  waveform's own PCM-aligned time axis and stays linear in seconds. Two alternatives
  (content-fit auto-scale; a minimum drawn stage width) were considered and rejected — spec
  §4.3.1 names why, and neither is to be reintroduced as a "simplification."
- **`envelope_edit`'s drag inverse must remain the EXACT inverse of the draw.** Both read the
  same taper module; a node dragged to a pixel and the knob's value at that pixel are one
  number, not two that agree.
- **The taper is EXTRACTED into its own pure module**, with its own `<module>_tests` target,
  because it now has three consumers in two different dependency layers: `deck_values` (which
  sits above `envelope_overlay`), `envelope_overlay`/`envelope_edit` (which sit below it),
  and — from Γ-W4-T1 — the host. Leaving it inside `deck_values` would force an inverted
  include edge. **Do not solve that by copying the map.**
- **Semitone knobs become log2/centre-expanded.** Symmetric, exactly 0 at centre, exactly
  ±`kPitchDepthMaxSemis` at the ends, monotone; **±7 st reached at 50–58 % of each
  half-travel**.
- **`resetDeckParam` bypasses the taper** — it writes the default value directly instead of
  round-tripping through `norm → value`. This *removes* the power-of-two dependency the
  header currently documents rather than working around it; that comment
  (`deck_values.h:42-46`) becomes wrong and must be rewritten.
- **Scope is the parameter, not the widget.** Deck knobs (outer ring and inner dial),
  envelope stage nodes and curve knots all honour it — they are surfaces onto one model, and
  a snap on one but not the others is a divergence. **Waveform markers are explicitly
  excluded**: they carry a shipped zero-crossing snap on the same modifier space and their
  domain is frames.

**Acceptance criteria.**
- Every taper change is verified **persistence-neutral**: a project saved before the change
  reopens with bit-identical stored values and identical audio; only needle angles move.
- Holding Shift mid-drag on each unit category lands the documented whole unit; releasing it
  does not jump the value.
- Holding and releasing Ctrl mid-drag is continuous — no step at either transition.
- Double-clicking any knob (outer ring and inner dial independently) lands **exactly** on its
  default at every taper, verified against a default-constructed `PlaySeconds` rather than a
  round trip.
- The log/log2 landmark positions above are asserted in the **taper module's** own tests, and
  hold at the **10 s** ceiling — the fit is against the new ceiling, not the old one.
- **Every default round-trips exactly through `norm → value`**, asserted per unit category
  against a default-constructed `PlaySeconds` and against `master_gain`'s unity. This is the
  criterion Γ-W4-T1 will declare `defaultNormalizedValue` from; it fails here, not there.
- **A stage time of several seconds is reachable by hand with no loss of resolution below
  100 ms**, and `kEnvTimeMaxSeconds == kGateStageMaxSeconds` is asserted, not assumed.
- **A project saved at the 2 s ceiling reloads with identical stored seconds and identical
  audio** — the ceiling change is persistence-neutral for the same reason the taper is.
- **The AHDSR overlay reads legibly at both ends of the new range**: a default 3 ms attack is
  a visible, grabbable node at the floor width, and a 10 s decay still lands its end node at
  its slot's edge. Assert the node separation, then judge the result by eye in the DAW.
- **The overlay's drag inverse is the exact inverse of its draw** at the tapered axis —
  `nodeAtPoint` / `resolveNodeDrag` and `buildEnvelopePolyline` round-trip.
- **The AHD 1:1 policy is unchanged**, asserted: a sustain-less envelope's x-axis stays
  wall-clock over the waveform.
- **The taper module is pure, CTest-covered, and is the ONLY definition of each map** — a
  grep finds no second copy in `deck_values`, `envelope_overlay`, or the shell.
- **The filter's four `*Norm` controls are untouched by the taper pass** — cutoff, Q, morph
  and drive are already wire-frozen in payload v9; a regression baseline proves their audio
  is unchanged.
- One shared modifier-read helper serves all drag surfaces; no second modifier grammar exists.

**Open questions.**
- **No [Daniel] questions.** Fork **Γ-F3 is REVERSED**: the ceiling moves to **10.0 s, in this
  track.** Daniel's *"a horrifically long decay with tight exp"* is the case it serves, and
  the `docs/TODO.md` entry that carried it is discharged rather than deferred again. **The
  reversal's cause is Ruling 1** — a range endpoint is host-facing normalization, free to
  move now and permanently expensive after Γ-W4-T1. Both of the prerequisites the deferred
  entry named are in this track anyway: the log taper (which is what makes a higher ceiling
  usable at the low end rather than unusable) and the reset bypass (which retires the
  power-of-two dependency — 2.0 is a power of two, 10.0 is not). The engineer should know the
  reset bypass is now doing triple duty and must not be "simplified" back into a norm
  round-trip under any circumstance.
- **[propose at review]** the exact shape of the taper, subject to the landmark bounds **and**
  the exact-default-preimage requirement. Those two together are tighter than either alone,
  and the second is easy to satisfy by accident and easy to lose in a refactor — **assert it,
  do not observe it.**
- **[propose at review]** whether the tapered schematic axis wants a visible tick or
  gradation cue, now that it is no longer linear in time. The plan's lean is **no** — the ms
  labels carry the number and the editor's no-decoration policy stands — but a reader who
  finds the axis illegible in the DAW should say so rather than silently adding one.

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

**Goal.** The master limiter and the meter's **audio and publication halves**, plus **the
plugin's first latency reporting** — no editor drawing. Landing the audio ahead of the deck is
what lets Γ-W3 draw against real published state instead of a stub.

**Spec:** `docs/product/instrument-control-surface.md` §3.1, **§3.1.1 (latency — read this
first; it was rewritten when Γ-F6 closed, so an older reading of it is wrong)**, §3.2–3.3,
§3.5, §7.10, §7.11, §8.2.

**Surface boundary — owns:** a new pure limiter module and a new pure meter-ballistics
module under `core/instrument/engine/` (each with its own `<module>_tests` target),
`shell/instrument/reasampler_processor` (the chain, the published block state, **and the
`getLatencySamples` / `restartComponent(kLatencyChanged)` path**), and **the phase's FIRST
params-payload rung** (the limiter enable flag). **Does not own** MASTER's deck geometry or
any drawing — that is Γ-W3-T1. **Read `kParamsPayloadVersion` on `dev` and take the next rung
above it rather than assuming the number** — Phase Ξ ran ahead of this plan's sequencing, so
the plan is not the record of what the ladder currently carries. On `dev` as of 2026-08-01
that resolves to **v14**.

**Behavior.**
- **Chain:** `voice mixer → master gain (existing ramped multiply) → limiter (bypassable) →
  output bus`, with the meter tapped at the **bus output, post-limiter**.
- **Limiter: a single toggle, no configurable controls.** Baked ceiling **−0.3 dBTP**.
  Default **off**. **No makeup gain, ever, of any kind** — transparent at rest.
  Stereo-linked detection (max |L|,|R| drives one gain) so the image is not moved.
- **True-peak detection is sidechain-only** — an oversampled detector in the sidechain, never
  oversampling the signal path. Factor is the engineer's call under the measure gate.
- **Lookahead, with DYNAMIC reported latency (Γ-F2, ruled).** `getLatencySamples()` returns
  **0** when the limiter is off and **the lookahead in samples** when it is on; the toggle
  calls `IComponentHandler::restartComponent(kLatencyChanged)`. **None of this exists today** —
  there is no `getLatencySamples` override, no `kLatencyChanged`, and no `restartComponent`
  call site anywhere in `src/`; the plugin ships the SDK default of 0. This track introduces
  the plugin's first latency reporting.
- **The restart is routine; the fencing is against a standing scar, not against the flag.**
  Dynamic latency reporting is ordinary VST3-instrument behaviour 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. **What makes the
  cycle expensive here is this plugin's own `setActive`** — reactivate calls
  `reloadInstrument()`, a bridge read plus a full WAV re-decode
  (`reasampler_processor.cpp:89-97`), where a typical plugin only allocates buffers; deactivate
  frees `live_`/`draining_`/graveyard (`:98-107`) for a documented reason (ghost sustained
  voices). **Γ-F6 is ruled: ship it — the toggle is a patch-design gesture, not a
  during-playback one.** Do **not** build a constant-reported-latency fallback and do **not**
  gate the deliverable on a measurement. The reduction of that self-inflicted cost is filed in
  `docs/TODO.md` ("Decouple the instrument reload from VST3 activation") with its trigger
  condition; it is out of scope here. The four requirements below survive as engineering
  hygiene against the `kIoChanged` scar, and all four are acceptance criteria:
  1. **Verify the whole call sequence against the vendored Steinberg SDK** before writing it,
     including the ordering rule that the new latency is what `getLatencySamples` returns
     *after* `setActive(true)` — so **the reported value must derive from persisted state, not
     from a transient the deactivate clears.**
  2. **Prove the restart does not disturb the output bus arrangement.** The output stays one
     permanently-stereo bus, never renegotiated.
  3. **Ship a regression test in the spirit of `testDualMonoStereoSampleRendersCentered`** —
     a dual-mono capture rendered across a limiter toggle stays centered, L ≡ R.
  4. **`restartComponent` is never called from `process()`.** Main/UI thread only, and
     coalesced so repeated clicks produce one restart per settled state.
  **This is NOT the change `reasampler_processor.cpp:66-68` forbids.** That warning is against
  reintroducing per-mode **bus** renegotiation (`kIoChanged` class), which panned a dual-mono
  capture hard right in the host's pin re-routing; `kLatencyChanged` is a different flag and the
  bus is untouched. But the precedent — mid-session `restartComponent` in this plugin has
  already shipped one real regression — is exactly why (2) and (3) are non-negotiable.
- **Flipping the toggle during playback: apply immediately, do NOT defer to a transport
  boundary** (product ruling, spec §3.1.1). A deferred restart leaves the plugin misaligned by
  the lookahead with no visible cue, which is worse than a visible interruption; and the host,
  not the plugin, schedules the deactivate/reactivate anyway. **Daniel has accepted the
  interruption outright** (Γ-F6) — it is not a case to design for. Two things remain in scope,
  and neither is a mitigation for it:
  - **A short (≤ 10 ms) equal-gain crossfade over the engage/disengage.** Kept as a *quality*
    measure, not a mitigation: a limiter engaging is a gain-path change, and this codebase
    already ramps every gain-path change (`kGainRampSeconds`, `ValueRamp`). It also earns its
    keep independently of the restart, because **we do not control when the host acts on the
    request** — our own transition must be clean in the window before it does.
  - **The limiter enable is classified NOT automatable**
    (`docs/product/parameter-automation.md` §3.8) so nothing can flip it at rate. It is also
    **not** the plugin's `kIsBypass` parameter.
- **Per block the processor publishes, as relaxed atomics:** per-channel peak `max|x|`, a
  latched clip flag, and the block's maximum gain reduction. **No dB conversion, no
  ballistics, no hold timers on the audio thread** — the UI converts and runs ballistics from
  block peaks and elapsed time. This widens the existing advisory-peak pattern
  (`reasampler_processor.h:109-113`), which is not reusable as-is.
- **Meter ballistics (pure, unit-tested):** instantaneous rise; **fall 20 dB/s**; peak-hold
  latched at the running max, **held 1.5 s**, then falling at the same rate; scale **linear in
  dB over −60…+6 dBFS**; clip latches at block peak ≥ 0 dBFS and is cleared on request.
- **The `ComponentState` payload rung** appends the limiter flag as a strict suffix on the
  existing discipline; the preceding version's blob is a strict prefix and lifts to bypassed.

**Acceptance criteria.**
- **With the limiter bypassed the rendered output is byte-identical to the pre-change build**,
  asserted by a regression baseline, not by ear.
- With the limiter engaged, no output sample exceeds the ceiling on program material that
  exceeds it by up to +12 dB; with it bypassed and gain driven, the output does exceed
  0 dBFS (proving the toggle is doing the work).
- **Nothing is louder at rest with the limiter on.** A signal that never reaches the threshold
  is bit-identical engaged and bypassed.
- No allocation, no lock, no transcendental on the per-sample path; the measure-and-report
  gate reports per-voice-block CPU with the limiter engaged at 32 voices.
- The meter-ballistics module is pure and CTest-covered: rise, 20 dB/s fall, 1.5 s hold, clip
  latch/clear, and the dB↔pixel map are all asserted without a host.
- A project saved before this change reopens with the limiter bypassed and sounding identical.
- **`getLatencySamples()` returns exactly 0 with the limiter off**, and the lookahead in
  samples with it on — asserted against the persisted flag, and correct across a
  deactivate/reactivate cycle.
- **A dual-mono capture rendered across a limiter toggle stays centered** (L ≡ R), and the
  output bus arrangement after a latency-change restart is identical to before it.
- **The plugin emits no hard step at the toggle** — the engage/disengage crossfade is asserted
  on a rendered signal, not judged by ear.

**Open questions.**
- **No [Daniel] questions. Fork Γ-F6 is ruled** — dynamic latency ships as specced, the
  deactivate/reactivate is accepted, and there is no fallback design and no measurement gate.
  Do not reintroduce either; the constant-reported-latency option is closed, not shelved.
- **[verify]** `temp_cortex/` has already been assessed and **rejected** (spec §3.5) — do not
  re-litigate it, and do not transplant from it.
- **[record, not a gate]** While the limiter is in REAPER under your hand, note what the
  restart actually costs — do notes cut, is the re-decode perceptible, does transport hiccup —
  and record it in this track's review. It is **not** a gate on shipping and no outcome changes
  the design; it is the trigger-condition evidence for the `docs/TODO.md` entry "Decouple the
  instrument reload from VST3 activation," which is where that cost gets reduced if it ever
  matters. Do **not** restructure `setActive` here: its destructive shape is deliberate and its
  reasoning (ghost sustained voices on reactivate) is documented at the call site.

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

**Goal.** Staged envelope segments draw as the curve their exponent defines, so the
mid-segment knot stops floating off its own trace.

**Spec:** `docs/product/instrument-control-surface.md` §5.

**Surface boundary — owns:** `shell/instrument/editor_paint_waveform.cpp`'s staged-envelope
trace and a **new pure tessellation module** for it. **Does not own** the loop/crossfade
marks (Γ-W2-T2), `envelope_overlay`'s vertex model, or the drawn-EG (spline) trace.

**The helper's home is now constrained, not a choice.** `ui/envelope_overlay` was previously
offered as a candidate home for the tessellation helper; **Γ-W1-T1 now owns that module**
(the AHDSR schematic axis, per Ruling 2), so the helper lands in a **new** pure module under
`core/instrument/ui/`. T1 also owns where an AHDSR's vertices land — this track owns only the
stroke *between* vertices, and tessellates over φ across a segment's pixel span, so the
tapered axis changes nothing about the curve drawn. **Express this track's assertions against
the returned vertices, not against absolute pixel literals**, and the rebase onto T1 is free.

**Behavior.** The defect is verified: `editor_paint_waveform.cpp:218` drops knots
(`if (v.knot) continue;`) and joins the remaining vertices with straight strokes, and
`curveMap` is never called in the paint path even though the exponent is in scope at `:211`.
Knot *positioning* already honours the exponent via `curveMidLevel`
(`envelope_overlay.cpp:94-105`) — that divergence is the visible symptom. The fix draws each
sloped stage through **the same `curveMap` the audio uses**, so trace and sound cannot
diverge; tessellation approach is the engineer's call.

**Acceptance criteria.**
- **At every exponent the knot's centre lies on the trace, within 1 px** — the reported defect,
  stated as the gate.
- **At exponent 1.0 the segment is visually identical to today's straight line.**
- No visible faceting at the widest segment the canvas can produce; a fixed low tessellation
  count is not acceptable at full width.
- All three envelopes, both play modes, all sloped stages (attack/decay/release) — one paint
  path, one fix.
- The established trace grammar is unchanged: one weight, `kEnvTracePx = 2.0`, through the
  analytic stroker. Both overlay layout policies (AHDSR right-anchored schematic, AHD 1:1)
  are honoured unchanged. The spline overlay's own trace is untouched.
- **Audio is unchanged** — this is a drawing defect only; a regression baseline proves it.

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

**Goal.** Commit the **canvas** — the window floor, the width budget it is derived from, and
the row every deck group belongs to — so every other UI track in the phase is drawn, tested and
judged at the final window size. The **arrangement** inside that canvas is Γ-W3-T1's.

**Spec:** `docs/product/instrument-control-surface.md` §1.1 (the two categories), §1.2 (the
floor arithmetic block), §1.6 (the headroom ledger), §7.1 and §7.4 (the two invalidated
`knob_deck.h` notes).

**Surface boundary — owns:** `core/instrument/ui/sample_bands.h` (`kEditorMinWidth`),
`core/instrument/ui/knob_deck.h` (the declared budget constants and the two invalidated header
notes), `core/instrument/ui/deck_groups.{h,cpp}` (**the new row predicate only**), and the five
test fixtures that read the floor — `test_sample_bands.cpp`, `test_deck_groups.cpp`,
`test_knob_deck.cpp`, `test_sample_chrome.cpp`, `test_keyboard_strip.cpp`. **Does not own**
`layoutDeck` / `deckRowCount` / `deckHeight` behaviour, the justification law, any descriptor,
MASTER's inventory or interior, any painter, or any parameter. It changes **no drawing code at
all.**

**Behavior — what it commits.**
- **`kEditorMinWidth` 980 → 1190. `kEditorMinHeight` stays 680** (Γ-F1).
- **Three declared budget constants in `knob_deck.h`:** the row block both rows will justify
  inside (**1020**), the right-anchored spanning deck's reserved width (**MASTER 142**), and
  the ceiling (**1280**). These are *declarations of budget*, not measurements — nothing
  computes them from a descriptor, and Γ-W3-T1's job is to prove its content fits inside them.
- **The floor is derived, not asserted as a literal.** `1020 + kDeckGroupGap(12) + 142 +
  2·kPad(8) = 1190`. `kEditorMinWidth` stays a literal in `sample_bands.h` — **do not add an
  include edge from `sample_bands` to `knob_deck`**, which would invert the allocator's
  deliberate independence from the deck (it takes `deckHeight` as a *parameter* for exactly
  that reason). The identity is asserted in `test_deck_groups.cpp`, which already includes
  both headers. This is the Θ-W6-T1 derived-floor precedent, landed once and never rewritten.
- **Row membership becomes a property of the group id:** `DeckRow { Sound, Contour, Spanning }`
  + `deckRowFor(DeckGroupId)` in `deck_groups`, an **exhaustive switch** on the
  `isLiveDeckParam` discipline, so a future group is a compile error rather than a silent
  default. Partition: **Sound** = PITCH/RATE, FILTER, VELOCITY, VOICE; **Contour** = PITCH ENV,
  FILTER ENV, AMP ENVELOPE; **Spanning** = MASTER. **Nothing consumes it until Γ-W3-T1** — that
  is the seam, and it is why the predicate is safe to land now: **membership is a property of
  the group, width is a property of the descriptor**, and only the widths are still moving.
- **Γ adds no new deck group**, so no later track amends this predicate.

**The seam, stated as what this track can and cannot assert.**

*Can assert today:*
- The derived floor identity above, and `kEditorMinWidth ≤ 1280` with **90 px** of headroom.
- `kEditorMinHeight == 680`, asserted so no later track drifts Γ-F1's ruling.
- `deckRowFor` is total over `DeckGroupId` and yields exactly the partition above.
- **Row 2's natural width already fits the block, in both play modes:**
  252 + 312 + 312 = **876 ≤ 1020**, leaving both its gutters ≥ `kDeckGroupGap`. Mode-stable
  because FILTER ENV's and AMP's reserve slots hold them at 312 in Gate and Trigger alike.
- **MASTER's reserve is not yet spent:** `deckGroupWidth(MASTER) == 72 ≤ 142`.
- At the floor, deck band **216** and waveform band **358** — the reflow's 112 px arrives here,
  two waves early (see the interim layout below).

*Cannot assert yet, and must not force:*
- **Row 1's natural width does not fit the block.** Today it is PITCH 150 + FILTER 524 +
  VELOCITY 192 + VOICE 164 = **1030**, against the 1020 block. The 50 px deficit is exactly
  what the two descriptor changes buy: PITCH → PITCH/RATE **+42** (Γ-W2-T1) and FILTER's
  `Band|Notch` moving to the caption corner **−92** (Γ-W3-T1), netting **980**. Record the
  target and the two contributions as a test comment; **assert the fit in Γ-W3-T1, and do not
  pre-empt either descriptor change to close it early.**
- Gutter distribution, the filter tie-line at x = 636, flush outer edges, MASTER's interior and
  its meter — all Γ-W3-T1. Every one of them measures a descriptor that does not exist yet.

**The interim editor, stated exactly so it is not filed as a defect.** At the new floor,
`availWidth = 1190 − 2·kPad = 1174`, and the **unchanged** greedy whole-group wrap packs:

```
row 1  PITCH 150 · PITCH ENV 252 · FILTER 524        =  950 used, 224 px ragged right
row 2  FILTER ENV 312 · AMP 312 · VELOCITY 192 ·
       VOICE 164 · MASTER 72                         = 1100 used,  74 px ragged right
```

**Two rows, not three** — so the deck band is already 216 and the waveform already draws at its
final 358 px, in both Gate and Trigger. After Γ-W2-T1 lands PITCH/RATE the pack is row 1 = 992,
row 2 unchanged; still two rows. The composition is wrong in exactly the way the reflow exists
to fix — PITCH ENV sits up with the sound decks, VOICE and MASTER sit down with the envelopes,
MASTER is still a single-height 72 px box, and both rows are left-packed with dead space at the
right. **Worse than today in composition, better in proportion.** That is the accepted
transitional state for the rest of the phase.

**Do not convert the two-row interim into a claim.** It is a coincidence of the greedy wrap at
exactly this width, not a guarantee — which is precisely why Γ-W3-T1's criterion is "two rows
**by construction**, asserted against the group inventory, not observed as a wrap outcome."
`testDeckFitsInsideTheEnforcedMinimumWindow` currently asserts `deckRowCount == 3`; relax it to
an **upper bound** (`<= 2`), which is a real regression canary throughout the interim and is
subsumed by Γ-W3-T1's exact claim. An exact `== 2` here is acceptable only with a comment
naming it as a wrap outcome the reflow replaces.

**Acceptance criteria.**
- **The floor is 1190 × 680, reached by a derived test over the three budget constants**, not
  by a literal — and the derivation is the one Γ-W3-T1 later reads rather than a second copy.
- **Headroom is exactly 90 px** against the 1280 ceiling, asserted.
- `deckRowFor` is exhaustive over `DeckGroupId`; adding a group without classifying it fails to
  compile.
- Row 2's natural width and MASTER's unspent reserve are asserted, in **both** play modes.
- **All five floor-reading test fixtures pass at the new floor** — including the chrome row,
  whose title slot gets *more* room at 1190, not less.
- **No drawing code changes, no descriptor changes, no parameter changes, no audio change.**
  A regression baseline proves the last of those trivially.
- The two invalidated `knob_deck.h` notes (§7.1's fourteen-pixel headroom figure, §7.4's
  cells-and-floor pairing) are **re-derived against the new floor, not deleted** — §7.4's
  restatement is *the deck's cell metrics AND its group/row composition both drive
  `kEditorMinWidth`; none of the three may move alone.*

**Open questions.** **No [Daniel] questions.** **[propose at review]** whether the three budget
constants belong in `knob_deck.h` (the deck owns the row block and the spanning-deck reserve)
or in `sample_bands.h` (the allocator owns the floor they derive). The plan's lean is
`knob_deck.h` with the identity in the test, because it adds no include edge; either is
defensible, but the *derivation must live in exactly one place*.

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

**Goal.** A real pitch-preserving time-stretcher for Preserve mode, written from established
state-of-the-art literature — landed **before** the control that drives it, so Rate ships onto a
finished engine rather than onto a disposable stand-in.

**Spec:** `docs/product/instrument-control-surface.md` §2.5.

**Moved from Γ-W4-T1 (Daniel, 2026-08-01).** It is the longest pole in the phase and has zero
dependency on any UI work. **The consequence is the interesting one: it inverts the
relationship with Rate.** Under the old order the stretcher was Rate's quality upgrade and
Γ-W2-T1 shipped an interim resample-and-cancel path to make Rate complete on day one; under
this order the stretcher is Rate's **prerequisite** and **the interim path is not built at
all.** Skipping a stand-in that was only ever going to be deleted is the win; see Γ-W2-T1's
named contingency for what happens if this track's gate slips.

**Precedent for landing a DSP module ahead of its consumer:** Θ-W1-T3 (`filter-dsp-port`)
landed the filter DSP as a standalone pure module a wave before Θ-W2-T1 wired it into the voice
path, for the same reason — the unknown is the DSP, not the wiring.

**Surface boundary — owns:** `core/instrument/engine/pitch_shift` and whatever new pure module
the stretcher needs (each with its own `<module>_tests` target), plus `voice.{h,cpp}`'s Preserve
read path. **Does not own** any parameter, any UI, the varispeed path, or the deck. It adds no
`ComponentState` field and takes **no rung of the payload ladder**.

**Behavior and constraints.** The algorithm is **the engineer's call under a
measure-and-report gate — this plan deliberately names none.** The constraints:
- **The stretch ratio is an argument, not a parameter.** Nothing publishes a non-unity ratio
  until Γ-W2-T1's Rate knob does. Until then the Preserve read path runs at ratio 1.0 and must
  be **bit-identical to the shipped Preserve read** — a stronger and cheaper regression gate
  than the old plan's A/B-against-an-interim-path, because the baseline is a build that exists.
- **CPU stance (Daniel, verbatim intent):** *"we should be efficient but accept the cost of
  high-quality algorithm choices. It's 2026, most people's computers can handle audio with
  ease. Just don't be wasteful."*
- **RT-safe:** no allocation, no I/O, no lock in `process()`; buffers sized at voice
  allocation or at the off-audio-thread reload, on `pitch_shift`'s existing pre-warm
  precedent.
- **Per-voice state, holding up at the 32-voice ceiling.** The gate is 32 simultaneous
  Preserve voices at 50 % and 200 %, not one voice at 100 %.
- **No new third-party dependency** (`pitch_shift`'s standing property).
- **No dispatch on the per-sample path** — concrete, inlineable types; no `IStretcher`.
- **Onset behaviour is a regression surface.** GA2 eliminated Preserve's ~25 ms onset latency
  by priming the ring with the actual upcoming source. **A stretcher that reintroduces an
  onset delay or a first-frame smear is a regression, not a trade-off.**

**Acceptance criteria.**
- **Ratio 1.0 with no shift is bit-identical to the shipped Preserve read**, asserted by a
  regression baseline — the null case, and the criterion that makes landing this ahead of Rate
  safe.
- Preserve speaks on frame 0 — no added onset latency, no first-frame smear, in any
  ratio/shift combination.
- No audible metallic or phasey artefacting on sustained tonal material at ±6 st and
  75–133 % ratio; transient material at 50 % / 200 % is no worse smeared than **varispeed
  playback at the equivalent ratio** — the honest "what does preserving pitch cost" reference,
  and the one that needs **no disposable implementation built to serve the comparison.**
- The Gate sustain-loop contract is unchanged: **loop the source, shift the output** — loop
  points remain source-frame facts.
- **Measure and report before the algorithm is final:** per-voice CPU at 32 voices, added
  latency (must be zero at the onset), and A/B recordings on three material classes (one-shot,
  tonal sustain, full-mix bounce). Report to Daniel; the choice is not final until he has heard
  the A/Bs.

**Open questions.** **[propose, with a measurement step]** the algorithm family itself.
**[verify]** that `core/instrument/CLAUDE.md`'s *"`WDL_Resampler` is not a Preserve engine —
never wire it as the duration-preserving path"* is honoured: under Preserve, Rate legitimately
changes duration, so a resampled read is an explicit duration control — but the
*pitch-preserving* mechanism must not be a resampler. **No [Daniel] questions.**

---

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

**Depends on Γ-W1 for — four dependencies, two of them new:**
1. **T1 ← W1-T1 (taper law).** Rate and Pitch must be authored into the finished
   taper/modifier law, not retro-fitted into it, and the semitone taper must exist before a
   second semitone knob does.
2. **T1 ← W1-T5 (the stretcher) — NEW, and the reason the interim path is gone.** Preserve
   Rate has no engine without it. Under the prior four-wave order this dependency ran the other
   way and was paid for with a disposable resample-and-cancel stand-in; the resequencing
   inverts it. **Rate must not ship before its Preserve engine.**
3. **T2 ← W1-T3 (the contour trace).** Both write `editor_paint_waveform.cpp`; running them
   together is a merge fight in one file.
4. **T2 ← W1-T1 and W1-T4, weakly.** W1-T1 also edits `editor_input_waveform.cpp` (the
   modifier read), which T2 rewrites for marker hit-test routing — serial, so not a conflict,
   but T2 rebases onto it. And T2's "the enable costs no window width" criterion is now
   asserted against **W1-T4's** derived floor test rather than one this track has to write.

T1 additionally inherits `engine/voice.{h,cpp}` from W1-T5 — a **hand-off, not a conflict**:
W1-T5 defines the Preserve ratio seam, and T1 feeds it. Serial across waves by construction.

**Disjointness — re-verified against this wave's membership, not carried over.** Both tracks
stayed in W2 and nothing entered or left it, so the prior finding is re-checked and stands. T1
owns the parameter model, the engine and the deck descriptors; T2 owns the waveform band's
marks and their pure geometry **and the chrome row's loop enable**. The two are disjoint at the
module level with **one named exception: `shell/instrument/editor_session.cpp`.** T1 may touch
it for the third commit tier's routing; **T2 owns `pickedMarkers` and `applyMarkers` there and
nothing else.** The partition is by function and the two do not overlap — **textual merge
adjacency, not semantic contention** — but it is a shared file in a phase whose wave boundaries
are otherwise single-writer surfaces, so it is stated rather than discovered at merge. Whichever
track lands second rebases onto the first. No *new* in-wave adjacency was created by the
resequencing: T2 touches neither `deck_values` nor `deck_groups` nor `voice`.

**One cross-wave hand-off, new with Ruling 2 and NOT a contention.** Rate's taper — linear in
semitones over ±12, the stated exception to the centre-expansion law — belongs in the **taper
module Γ-W1-T1 extracts**, since that module is the one home of every map. T1 therefore
appends a law to a module a previous wave created. Serial across waves by construction, the
same shape as its `engine/voice.{h,cpp}` hand-off from W1-T5. **What would be wrong is a
second taper defined inside `deck_values`' binding** — one home, appended to, not forked.

**The format ladder stays clean.** The loop enable maps onto the existing
`SampleLoop::hasLoop`, which is already persisted and whose `start`/`end` are already written
unconditionally — **no new field, no version bump** — so T1 keeps sole ownership of **the
phase's second payload rung** exactly as specced (v15 on `dev` as of 2026-08-01; read the
ladder rather than assuming the number).

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

**Goal.** PITCH becomes **PITCH/RATE**: three knobs (`Key Trk | Rate | Pitch`) under the
existing Varisp|Presrv toggle, with both new controls wired through the engine.

**Spec:** `docs/product/instrument-control-surface.md` §2.

**Surface boundary — owns:** `core/instrument/engine/play_params.h` +
`core/instrument/map/play_seconds.h` (the two new fields),
`core/instrument/map/component_state_io` + `params_payload` (**the phase's SECOND payload
rung** — v15 on `dev` as of 2026-08-01; read the ladder, do not assume the number),
`core/instrument/engine/voice.{h,cpp}` (the compounding and the note-on latch),
`core/instrument/ui/deck_groups` (the PITCH/RATE descriptor **and** the three-state live
predicate), `core/instrument/ui/deck_values` (the two new bindings). **Does not own** the
deck's row layout — that is Γ-W3-T1 — nor the time-stretcher itself (Γ-W1-T5, already landed
by the time this track runs).

**Behavior.**
- **Rate: 50 %–200 %, default 100 % at true knob centre, exponential taper** — 50 % = −12 st,
  200 % = +12 st, musically symmetric. This is **linear in semitones over ±12** and is the
  stated exception to W1-T1's centre-expansion law (which applies to semitone knobs whose
  throw exceeds ±12).
- **Pitch: a baseline pitch offset, ±24 semitones**, centred, on W1-T1's centre-expanded
  semitone taper. **Reads `kPitchDepthMaxSemis`; does not mint a second constant.**
- **Varispeed:** keytrack ratio × rate ratio × pitch-offset ratio **compound into a single
  read-increment multiply**; the rate offset applies to the varispeed pitch. Composes with
  the pitch envelope's existing per-frame `ratio_` multiply — **no new per-sample stage**.
- **Preserve:** rate is an **absolute** value driving **duration only**; keytrack and pitch
  offset drive the pitch shifter. **Rate drives the stretch ratio Γ-W1-T5's stretcher already
  consumes — there is no interim path.** The stretcher is this track's prerequisite, not its
  successor; the resample-and-cancel stand-in the prior plan carried is retired unbuilt (see
  Open questions for the contingency). `core/instrument/CLAUDE.md`'s "never wire
  `WDL_Resampler` as the duration-preserving path" is honoured by construction.
- **Rate is latched at note-on**, delivered by a **third commit class**: published into the
  live block like any live parameter, read only by `snapLive`, never by `applyLive`.
  `isLiveDeckParam`/`liveCommitFor` widens from two states to three
  (`Live` / `NoteOnLatched` / `Reload`) in that one predicate — **not** a second table, and
  **not** the reload tier (a swept knob must never trigger a WAV re-decode). **Record the
  reason in the header:** loop resolution and contour mapping are note-on folds, so live rate
  means re-folding a resolved loop and re-mapping a contour mid-note.
- **Pitch is live** — under Varispeed one more factor in a multiply the pitch envelope already
  performs; under Preserve an addend to a shift the pitch envelope already modulates.
- **Loop points scale with rate; contours scale with rate.** Neither rewrites stored values:
  the loop is source-frame facts traversed at the new increment (Varispeed) or the new read
  rate (Preserve), and a contour is a function of normalized position. **Staged envelope stage
  times do NOT scale** — 30 ms is 30 ms at any rate. That asymmetry is deliberate: a contour is
  of the sample, a staged envelope is of the performance.
- **Deck descriptor:** three cells; `captionWidth` **70**, hard ceiling **80** (above that the
  caption row overtakes the 180 px knob row and the group exceeds 192). If the text will not
  fit at 80, narrow the `Varisp|Presrv` segments 48 → 44 (ceiling becomes 88) — **do not widen
  the group**.
- **The payload rung** appends both fields as a strict suffix; the preceding version's blob
  lifts to rate 100 % / pitch 0 st, bit-identical playback.
- **Both new `DeckParam`s are classified in the three-state predicate, and that classification
  is what puts them in the VST3 parameter list three waves later** — Rate `NoteOnLatched`,
  Pitch `Live`. Γ-W4-T1 derives the exposed set from this predicate rather than from a list
  of its own, so a mis-classification here is a mis-declared parameter there.

**Acceptance criteria.**
- Rate at 50 % plays an octave down and half speed under Varispeed; at 200 %, an octave up and
  double speed. Under Preserve the same settings change duration only — pitch is unchanged
  within the stretcher's tolerance. **Preserve Rate is a finished feature the day this lands**,
  because Γ-W1-T5 already shipped its engine; a degraded or inert Preserve Rate is a failed
  track, not an acceptable interim.
- Rate at exactly 100 % and Pitch at exactly 0 st render **bit-identical** to the
  pre-change build, in both engines.
- Shift-drag on Rate lands on whole semitones (so an octave and a fifth are reachable by
  hand); Shift-drag on Pitch lands on whole semitones; Ctrl gives cents on both.
- **A Rate change while a note sounds does not alter that note**; the next note-on takes it.
  **It does not trigger a reload or an engine rebuild** — assert the tier, not just the sound.
- A Pitch change **does** move a sounding note, in both engines.
- With a loop set, changing Rate changes the loop's audible period without moving either
  waveform marker.
- The PITCH/RATE group measures **exactly 192 px**; adding the two `DeckParam`s produces a
  compile error in `isLiveDeckParam`'s exhaustive switch until they are classified.
- A project saved at the preceding payload version reopens at rate 100 % / pitch 0 st and
  sounds identical.

**Open questions.**
- **None [Daniel].**
- **[propose at review]** Rate's clamp behaviour at the range extremes as it meets the
  stretcher's own ratio bounds — one clamp, resolved where the two meet, not two that can
  disagree.
- **Named contingency, not a plan item, and not to be taken silently.** If Γ-W1-T5's
  measure-and-report gate has not passed when this track is ready to dispatch, the pre-agreed
  fallback is the **resample-and-cancel composition** spec §2.5 records — a resampled read with
  the resulting pitch change cancelled in the existing SOLA shifter — shipped as an interim
  Preserve path with the stretcher as its later quality upgrade, i.e. a return to the prior
  four-wave order. **Escalate to Daniel rather than taking it:** it revives a disposable
  implementation and re-opens the `WDL_Resampler` guardrail conversation, and the whole point of
  the resequencing was to avoid building it.

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

**Goal.** Give the loop an explicit enable, make the loop and crossfade marks legible, and
paint the crossfade where it is actually heard.

**Spec:** `docs/product/instrument-control-surface.md` §6 — **read §6.1 (the diagnosis),
§6.4 (the enable) and §6.5 (trade-offs) in full before starting.** This is the phase's one
genuinely designed surface; the sections are the brief.

**Surface boundary — owns:** `shell/instrument/editor_paint_waveform.cpp`'s marker/loop draw,
`shell/instrument/editor_input_waveform.cpp`'s marker hit-test routing,
`core/instrument/ui/waveform_view` (cap rects, label boxes, the label-suppression rule — all
pure, all CTest-covered), and — **new, from the Γ-F4 ruling** —
`core/instrument/ui/sample_chrome` (the enable's rect in the toolbar control run),
`shell/instrument/editor_paint_chrome` + `editor_input_chrome` (its draw and hit-test), and
`shell/instrument/editor_session.cpp`'s **`pickedMarkers` / `applyMarkers` only** (the
retention rule — see the wave header for the shared-file partition). **Does not own**
`loop_span`, the crossfade model, any parameter, or any `ComponentState` version. **This
track changes drawing, hit-testing and one editor-state retention rule — no format change.**

**Behavior.**
- **An explicit loop enable on the CHROME ROW (Γ-F4, ruled).** A two-segment `Loop Off|On`
  toggle joins the toolbar row's right-anchored control run, **immediately left of the
  `Mono|Stereo` toggle**, with Browse still rightmost. Loop is a waveform-overlay concept and
  **has no deck** — a deck cell was never the right home. Because the run is right-anchored
  and the title slot absorbs it, **this costs zero window width and none of the 90 px
  headroom**; if the title will not hold its text at the 1190 floor, **the enable's segments
  narrow — the floor does not move.**
- **The enable IS `SampleLoop::hasLoop`. No new field, no version bump.** The field already
  exists (`play_params.h:210`), is already what `resolveLoop` refuses on
  (`loop_span.cpp:12`), and is already persisted in the payload's `loopOverride` block —
  where **`start`/`end` are written unconditionally whatever `hasLoop` says**
  (`params_payload.cpp:31-36`), so the wire can already carry "off, with a span remembered."
  What changes is the field's *provenance*: today it is derived from the marker gesture, and
  after this track it is **user-owned**, with the gestures as shortcuts onto it.
- **Collapse-to-off survives as a shortcut, not as a second state machine.** `hasLoop` is the
  single authority; four gestures reach it:
  | Gesture | Enable | Span | Crossfade |
  |---|---|---|---|
  | Enable → On | on | retained | retained |
  | Enable → Off | off | **retained** | **retained** |
  | Collapse the span onto itself | off | **destroyed**, re-parked at `defaultLoopBounds` | **zeroed** |
  | Drag either loop mark while off | **on** | takes the drag | retained, re-clamped |
- **Two consequent behaviour changes, each with its reason.** (a) `pickedMarkers`'
  re-park (`editor_session.cpp:221-226`) currently triggers on `!hasLoop`; it must become
  conditional on the span being **invalid** (collapsed / inverted / out of range) rather than
  on the enable being off — a toggle whose off→on does not restore what was there is a delete
  button, not a toggle. (b) `applyMarkers`' crossfade zeroing (`:240-243`) moves from "the
  enable is off" to "the span was destroyed." **The original reasoning is preserved, not
  overruled:** it zeroes so a stale length cannot silently re-apply against a span that no
  longer exists; with the span retained, its clamp bound is retained too and there is nothing
  stale.
- **The "drag me" affordance splits into two off-states.** Off with **no span ever set** —
  pair parked at `defaultLoopBounds`, Disabled, caption `DRAG TO SET LOOP`. Off with a **span
  retained** — pair Disabled *at its own positions*, caption `LOOP OFF` (there is nothing to
  "set"). In both, **dragging a mark turns the enable on** — the shipped drag-to-create
  gesture survives and now teaches the enable by demonstration.
- **In Trigger the enable draws Disabled and inert, and does NOT clear `hasLoop`** —
  Disabled-not-hidden, the same grammar as the marks, with its state restored on the return to
  Gate. This transitively covers the drawn-EG case via `enforceGateUnavailableWhileDrawn`
  (`play_params.h:198-205`), which forces Trigger whenever an envelope is drawn — one
  predicate, not a second rule. **Disabled-but-grabbable (the off marks) vs.
  Disabled-and-inert (Trigger) is deliberate:** the user's own off is reversible by the very
  gesture on offer; Trigger's refusal comes from the engine and no drag can talk it out of it.
- **One mark grammar: line + shaped cap + label. The cap IS the grip.** Four marks:
  **START** (`accent/primary`, solid right-pointing triangle cap, solid line — the only
  primary-ink mark, because it is the only one always in effect); **LOOP** (`accent/secondary`,
  L-cap opening right); **END** (`accent/secondary`, L-cap opening left); **XFADE**
  (`accent/secondary` reduced alpha, ramp cap, **dashed** line — a soft boundary). This
  replaces the bare 10 px orphan tab that today marks the crossfade with no line of its own.
- **Labels** in `Font::Micro`/`TextDim`, drawn **beneath** the trace and handles in z-order.
  A mark's label **re-draws on top on hover or drag** of that mark. **A label is suppressed if
  its box would overlap one already placed**; placement order is grabbed/hovered first, then
  START, LOOP, END, XFADE. Occlusion by an envelope node is **accepted and named** — the cap
  shape carries the identity permanently, the label is for learning.
- **The crossfade moves to `[loopEnd − crossfade, loopEnd)`** — where it is audible. The
  handle moves to the loop-end side; **drag direction is unchanged** (left lengthens), so the
  muscle memory survives.
- **The crossfade region draws as a top-and-bottom edge wedge, NEVER as a second fill.** A
  triangular band at the overlay's top and bottom edges growing from zero at
  `loopEnd − crossfade` to ~10 px at `loopEnd`. **This is a hard constraint:** the region is
  now *inside* the loop span, where a translucent fill would stack on the 0.20 loop fill, and
  the envelope trace crossing that fill is a known, accepted under-floor pair at 2.25:1
  (`editor_paint_waveform.cpp:28-34`), whose own note says the FILL is what changes if it is
  ever resolved. **The loop fill's peak alpha must stay exactly 0.20.**
- **The ingredient draws as a ghost.** `[loopStart − crossfade, loopStart)` draws the mirror
  wedge at half alpha, no handle — **a hairline dashed outline at rest, filling in on hover or
  drag of the crossfade handle**. This makes the `crossfade ≤ min(start, loopLength)` clamp
  self-explanatory: the fade stops growing exactly when the ghost's left edge reaches START or
  LOOP, so the user sees the reason instead of hitting an invisible wall.
- **Trigger mode:** the loop pair and the crossfade mark draw **Disabled and are not
  grabbable**, with a dim `LOOP — GATE ONLY` caption — Disabled rather than hidden, matching
  the editor's existing Gate-segment grammar, and because hiding a set loop on a mode flip
  destroys information the user put there. START stays fully live.

**Acceptance criteria.**
- The four marks are distinguishable by ink and cap shape with the labels suppressed, and
  named when they are not.
- **The shaded crossfade region sits over the frames where the fade is audible** — verify
  against a rendered loop, not by reading the code.
- Every mark is grabbable by its cap; grabbing a mark shows its label.
- The crossfade at its clamp shows the ghost's left edge coincident with the bounding mark.
- **The loop fill's peak alpha is unchanged at 0.20** and the accepted 2.25:1 trace pair is
  neither improved nor worsened.
- In Trigger, no loop mark accepts a grab, the chrome enable is Disabled and inert, and the
  reason is on screen. Returning to Gate restores the enable's prior state.
- **Turning the enable off and on again restores the loop exactly** — same span, same
  crossfade, no re-park. Collapsing the span instead turns it off, re-parks at
  `defaultLoopBounds` and zeroes the crossfade. Both paths asserted.
- **Dragging a loop mark while the enable is off turns it on**, in both off-states.
- **The enable costs no window width:** `kEditorMinWidth` is unchanged by this track, asserted
  by the same derived test that guards the floor.
- **No `ComponentState` version moves; no new persisted field; `resolveLoop` is untouched;
  audio is unchanged.** The enable round-trips save/reload through the existing
  `loopOverride` block, in both states, with the span retained across an off.
- All cap/label/suppression geometry is pure and unit-tested; no hit-test math in the painter.
  The enable's rect lands in `sample_chrome` alongside the rest of the control run.

**Open questions.**
- **[propose at review, then verify by hand]** The claim-arbitration inputs change:
  `markerHandleRect` today gives a tab to the crossfade only, and `resolveWaveformClaim`
  breaks ties by smallest nominal target area. Giving every mark a cap-grip changes the
  candidate set **and every nominal area in it**. The arbitration must be re-derived, and
  `docs/TODO.md`'s open entry *"Pre-existing staged-envelope-node shadow at zero-attack"*
  must be **re-evaluated against the new cap geometry and its outcome recorded** — resolved or
  worsened, either is acceptable, silence is not.
- **No [Daniel] questions.** Fork Γ-F4 is ruled — there **is** an explicit enable and it is on
  the chrome row, in this track. The prior framing ("an enable needs a cell, so it is a Γ-W3
  layout decision") was wrong and is retired: loop has no deck, so it never needed one.
- **[propose at review]** every site that currently *infers* `hasLoop` — two in
  `editor_input_waveform` (`:255`, `:258`), two in `editor_session` (`:222`, `:236`) — is now
  writing to a user-visible control rather than to an internal flag. Re-read each in that
  light; "it still compiles" is not a disposition.
- **Named escalation, not a fallback to take silently:** if the top strip reads crowded in the
  DAW, the pre-designed answer is the marker rail (spec §6.2, Direction 2) — a larger build
  that would also dissolve the arbitration problem structurally. Escalate; do not improvise a
  half-rail.

---

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

**Depends on Γ-W2 for:** the PITCH/RATE descriptor (W2-T1) — the reflow measures the real
three-cell group, and laying it out against a forecast of that group means re-measuring
afterward. **This is the whole reason the arrangement is late**, and it is why the canvas was
split out of it into W1-T4. **T2 depends on the same wave for a different reason:** rate and
pitch offset must exist before the bake's reset list can name them.

**Depends on Γ-W1 for:** W1-T2's published meter/GR/clip state, which MASTER's deck draws
(drawing against a stub would mean building the meter twice), and W1-T4's floor, budget
constants and row predicate, which T1 **consumes rather than re-derives**. **T2 depends on
W1-T2 for the limiter enable flag**, the third of the three values it must add.

**Depends on Phase Ξ for T2 — the phase's only EXTERNAL gate.** `Ξ-W2-T1
(resample-bake-chain)` must have landed on `dev` before T2 dispatches. T2 amends what that
track shipped; it cannot amend a branch.

**Two tracks, disjoint by surface — but T2's disjointness is CONDITIONAL and must be
confirmed, not assumed.** T1 owns `ui/knob_deck`, `ui/deck_groups` (row-predicate
consumption, FILTER's caption move, MASTER's inventory) and `shell/instrument/editor_paint_deck`.
T2 owns the bake's reset step wherever Ξ-W2-T1 put it. **T2's first act is to read what
actually shipped and confirm its reset surface touches none of T1's three modules.** If the
shipped reset enumerates controls through `deck_groups` or `deck_values`, the two are not
disjoint and **T2 serializes behind T1 inside the wave** — a named contingency, taken openly,
not discovered at merge. That risk is real precisely because this plan cannot predict the
shipped shape; predicting it is what put the phase in this position.

**Why T1 is one track.** The row law, the group inventory and the double-height deck are one
geometry decision spread over `knob_deck`, `deck_groups` and the deck painter. Splitting it
would put two tracks in the same pure modules. **The window floor is no longer part of it** —
W1-T4 set it two waves ago, and this track must not move it.

**Neither track takes a payload rung.** T1 is layout only; T2 changes a reset list, not a
format.

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

**Goal.** Two categorical rows plus a double-height MASTER bus deck, inside a 1280 × 720
ceiling, returning 112 px to the waveform.

**Spec:** `docs/product/instrument-control-surface.md` §1 (the whole section, incl. the §1.2
measured table **and §1.6, the headroom ledger**) and §3.2–3.3 (what MASTER draws). **§7 lists
the invariants this track invalidates or widens — read it before touching `knob_deck.h`.**

**Surface boundary — owns:** `core/instrument/ui/knob_deck` (the row law, the double-height
group, the justification — **consuming** W1-T4's budget constants, not restating them),
`core/instrument/ui/deck_groups` (consumption of W1-T4's row predicate, FILTER's `Band|Notch`
caption move, MASTER's inventory), and `shell/instrument/editor_paint_deck` (the MASTER
meter/limiter/bubble draw). **Does not own** `kEditorMinWidth` or any budget constant — those
are W1-T4's and are **read**, never moved — nor any parameter, the limiter DSP, or the
waveform band.

**Behavior.**
- **Row 1 (sound), one row, non-negotiable:** PITCH/RATE 192 · FILTER 432 · VELOCITY 192 ·
  VOICE 164 = **980** natural.
- **Row 2 (contour):** PITCH ENV 252 · FILTER ENV 312 · AMP ENVELOPE 312 = **876** natural.
- **MASTER is double-height (216 px) and right-anchored**, outside both rows, 142 px wide.
- **FILTER's `Band|Notch` moves from its row-toggle position to the caption corner**, taking
  the group 524 → **432** (−92 px). It occupies FILTER's currently-unused `captionToggle2`
  slot — **no new geometry is required**.
- **VOICE keeps its `Retrig|Legato` row toggle.** Moving it to the caption makes VOICE
  *wider* (226, not narrower), because its caption row is the binding side. Verified; do not
  "fix" it.
- **Justification law, applied to BOTH rows:** space-between within the row block; slack
  divided equally among the row's (n−1) gutters, integer residue to the leftmost;
  **no gutter narrower than `kDeckGroupGap` (12)**. **Decks are never stretched.** MASTER is
  not part of either row's justification.
- **Row block = 1020 px at the floor**, giving row 1 gutters 12/14/14 and row 2 gutters 72/72,
  at which width **FILTER's right edge and FILTER ENV's right edge both land on x = 636**.
  That tie-line, row 2's equal gutters, and row 1's minimum gutter being exactly
  `kDeckGroupGap` all hold at 1020 and only at 1020 — **this is why the floor is 1190 and not
  1186.** Above the floor the tie-line drifts and that is accepted (spec §1.3).
- **The floor is already 1190 × 680 and the bands are already 216 / 358** — Γ-W1-T4 landed all
  four in wave 1, and the greedy wrap happened to reach two rows at that width. **This track
  changes none of those numbers; it makes them true by construction instead of by coincidence.**
  Row 1's natural width fits the block **only after this track's `Band|Notch` move**: 1030
  today, +42 from W2-T1's PITCH/RATE, −92 here, = **980**. That is this track's fit assertion
  and W1-T4 deliberately left it open.
- **The 90 px of remaining headroom is the budget for the life of this layout**, and one deck
  cell is 60 px. **This is why MASTER's reserved slot is ONE cell** (Γ-F5, ruled): two would
  spend 60 of the 90 up front on a control nobody has named, leaving 30 — which would freeze
  row 1 forever, since any later row-1 addition needs 60. Widening MASTER later costs the same
  60 it would cost now, and by then the trade is against a real control instead of a guess.
  **State this ledger where a future reader will hit it** — spec §1.6 is its home, and a
  reader proposing a new knob needs to see it before they propose.
- **MASTER's interior** (spec §1.4, exact to the pixel): caption row with the limiter toggle
  and a **round** 12 px `warn` GR bubble in the far corner (non-interactive — the same slot the
  envelope decks' radio uses; round so it reads as a lamp, not a control); **gain knob in the
  upper-left cell at box-relative y = 26** and a **reserved empty slot at y = 138** — i.e. the
  two cells land on row 1's and row 2's knob baselines exactly, which is what stitches the
  spanning deck to both rows; **meter column 62 px wide × 186 px tall** on the right.
- **Three rules not to generalise wrongly:** MASTER's left column uses **fixed cell slots at
  the two baselines, NOT the horizontal run-division law** (that law would stretch one knob
  over 186 px); the reserved slot **draws nothing** (blank reads as breathing room, a dashed
  placeholder reads as unfinished); the meter is **one rect spanning both baselines**, not two
  per-row meters.
- **The meter draws W1-T2's published state**, with the ballistics run on the UI timer.
  **Bar count follows the same `LaneSplit` decision `waveformSurface` already folds** (channel
  mode ∧ source channel count) — one wide bar when the waveform draws one lane, two skinnier
  bars when it draws two. Not a second rule: a mono source in stereo mode is dual-mono, and
  two identical bars would be a lie.
- **Meter appearance:** bar in `accent/primary`; peak-hold tick 2 px in `text/primary`; clip
  cap in `warn`, latched, click-to-clear; scale linear in dB over −60…+6 with ticks every
  6 dB and numerals at 0/−12/−24/−36/−48/−60, the 0 dB tick heavier. **No green/yellow/red
  segmentation** — `warn` stays reserved for clip states.

**Acceptance criteria.**
- At the floor width the deck lays out in **exactly two rows plus the spanning MASTER**,
  **by construction** — asserted against the group inventory, not observed as a wrap outcome.
- Every group's width matches the §1.2 table exactly, **in both Gate and Trigger** (row 2's
  natural width is mode-stable at 876 because the reserve slots hold FILTER ENV and AMP at
  312 in both modes — assert it).
- Row 1 and row 2 are **flush left and flush right**; at the floor width the filter tie-line
  is exact (both edges at x = 636) and row 2's two gutters are equal.
- **Row 1's natural width is 980 and fits the 1020 block** — the fit Γ-W1-T4 could not yet
  assert, closed here by the `Band|Notch` move.
- **`kEditorMinWidth` is still 1190 and the floor is still ≤ 1280 × 720** — unchanged by this
  track, verified against Γ-W1-T4's derived test rather than a second copy of it.
- The waveform band is **358 px at the floor**, and the deck band is 216 — **unchanged from the
  interim, now reached by construction**: `deckRowCount` at and above the floor is 2 because the
  row predicate says so, not because a wrap landed there. Assert against the group inventory.
- MASTER's gain knob shares a knob baseline with FILTER's knobs; its reserved slot shares one
  with AMP ENVELOPE's.
- The meter reads correctly in mono and stereo, the peak-hold tick holds 1.5 s, the clip cap
  latches and clears, and the GR bubble lights only while the limiter reduces gain.
- **With the limiter engaged the clip cap never latches** on material the limiter is catching;
  if it does, that is a defect report against W1-T2, not a user error.
- `knob_deck`'s and `sample_bands`' tests are updated to the new law, and the invalidated
  notes in `knob_deck.h` (the fourteen-pixel headroom figure; the cells-and-floor pairing) are
  **re-derived, not deleted** — spec §7.1, §7.4.

**Open questions.**
- **[propose at review]** Whether the greedy whole-group wrap survives at all as a sub-floor
  degrade, or is replaced outright by explicit row assignment. What is **not** optional: at
  and above the floor width the layout is the specified arrangement, reached by construction.
  `DeckLayout::rowCount`/`::height` change meaning either way (spec §7.3).
- **No [Daniel] questions.** Forks Γ-F5 (**one cell**) and Γ-F1 (**680 stays**) are both
  ruled; they are stated in Behavior above, not carried here as options.
- **[verify]** `deck_groups.cpp`'s `kEnvModeSegW = 23` ceiling rises to **47** once PITCH ENV
  is on row 2 (AMP binds at 55). No change is required; the comment stating the old ceiling
  stops being true and must be corrected (spec §7.2).

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

**Goal.** Complete the resample bake's reset list against the control surface that now
exists — the correction Phase Γ owes Phase Ξ because Ξ-W2-T1 shipped ahead of the sequencing
this plan asserted.

**Consolidates:** nothing from the seventeen. It is a **correction obligation**, not a
feature (see "Flagged for awareness" item 2).

**Spec:** `docs/product/instrument-control-surface.md` §3.4, and Ξ-W2-T1's own "Reset scope"
block above — **which is the ratified rule this track applies, not a rule it may reinterpret.**

**Surface boundary — owns:** the bake's parameter-reset step, wherever Ξ-W2-T1 landed it, and
its tests. **Does not own** the bake chain, the crossing architecture, the replace-vs-add
decision, the capture path, any deck module, any painter, any parameter, or any
`ComponentState` version. **It changes what a shipped list contains — nothing else.**

**Behavior.**
- **Three values join the reset list**, all classified against Ξ-W2's own ratified rule
  ("reset what the bake baked in"), all **reset**, none of them a new Daniel decision:
  **rate**, **pitch offset**, and **limiter enabled**. The limiter's reasoning is worth
  carrying rather than re-deriving: master gain is already on the reset list, so the bake
  includes the master stage, so the limiter's effect is in the audio.
- **Verified against what shipped, not against what was predicted.** This plan named three
  values before either the bake or the controls existed. **Read Ξ-W2-T1's landed reset list
  first** and reconcile: if it already anticipated any of the three, say so and drop it; if
  it classified something differently from `docs/product/instrument-control-surface.md` §3.4,
  **the landed code is the fact and this plan is the prediction** — escalate the difference,
  do not silently overwrite either.
- **Re-run Ξ-W2-T1's own "genuinely new parameter" check over everything Phase Γ added**, not
  just the three named. Γ also ships the loop enable (W2-T2) and raises the stage-time
  ceiling (W1-T1). Classify each **against the rule**: the loop enable is a loop fact whose
  effect is in the rendered audio (**reset**, with the loop points it travels with); the
  ceiling is not a parameter at all. State each disposition; silence is not one.
- **Root note still survives.** The bake's most load-bearing exception is untouched by this
  track — capturing at root is what makes root survivable, and resetting it would detune
  every subsequent iteration.

**Acceptance criteria.**
- After a bake, **rate reads 100 %, pitch offset 0 st, and the limiter reads bypassed** — and
  the root note, key-tracking and the VOICE group are still untouched.
- **The bake stays audible and faithful with the new controls dialled in**: dial rate, pitch
  offset and the limiter, bake, and the neutral instrument playing the programmed note sounds
  as the dialled one did — the criterion Ξ-W2-T1 already carries, now actually exercised over
  Γ's controls.
- **A reconciliation note in the track's review** stating, per value, whether the landed code
  already covered it, and recording any difference between what shipped and what §3.4
  predicted.
- **No format change, no new field, no version bump, no change to the crossing architecture
  or the replace-vs-add decision.** A regression baseline proves the bake's audio is
  otherwise unchanged.

**Open questions.**
- **No [Daniel] questions.** The rule is ratified and §3.4's classification is derived from
  it.
- **[verify, FIRST]** the disjointness contingency in the wave header: read the shipped reset
  step and confirm it touches none of Γ-W3-T1's modules. If it does, serialize behind T1 and
  say so.
- **Explicitly NOT this track's:** the two consequences automation adds to the bake — the
  reset having to notify the host, and a host lane re-imposing its curve onto baked audio.
  Both are **Γ-W4-T1's**, because that track creates them. Doing this correction once, before
  automation, and letting Γ-W4-T1 add its own obligation on top is deliberate: the
  alternative is an amendment to an amendment.

---

### Γ-W4 — VST3 parameters

**Depends on every earlier wave, and each dependency is a hard prerequisite rather than a
courtesy:**

1. **← W1-T1.** The taper and the 10 s ceiling **are** the host-facing normalization, and
   W1-T1 is also what extracts them into the one pure module the host reads through. Declaring
   parameters against a taper that is still moving is the one-way door this whole phase is
   ordered around.
2. **← W1-T2 and W2-T1.** Every control that could be a parameter must exist before the list
   is declared. The list is derived from the control inventory; an inventory still growing
   produces a list that has to be re-frozen, and it cannot be.
3. **← W2-T1 specifically.** `isLiveDeckParam` becoming three-valued is the *prerequisite* of
   the classification, not an incidental: the exposed set is exactly `Live ∪ NoteOnLatched`.
4. **← W3-T1.** MASTER's inventory (limiter toggle, GR bubble, reserved cell) is the last
   change to what controls exist at all.
5. **← W3-T2.** The bake's reset list must already be complete, so this track adds the
   host-notification obligation once rather than amending an amendment.

**One track.** The storage decision governs every part of the work — the projection rule, the
migration path, what `getParamNormalized` returns, and what the bake's reset must do — exactly
as Ξ-W2-T1's crossing decision governs its chain. Every candidate split (a pure
model/classification half and a host-wiring half) is **serial**, so it buys no concurrency and
puts the decision on one side of a boundary and its consequences on the other.

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

**Goal.** The instrument reports its automatable parameters to the host, under a frozen id
contract and a logical order — which also hands it REAPER's whole per-parameter modulation
block (LFO, envelope follower, MIDI link, parameter linking) for free.

**Consolidates:** nothing from the seventeen. **Ruling 1** (Daniel, 2026-08-01): *"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."*

**Spec:** `docs/product/parameter-automation.md` — **§§6–10 are the specification; §§1–5 are
the analysis behind it.** Read §6.1 (storage), §6.3 (the freeze), §7 (the classification) and
§8 (the one-way-door sweep) before scoping. **Today the plugin has zero parameters:**
`ReaSamplerProcessor::initialize` (`reasampler_processor.cpp:56-73`) never populates
`SingleComponentEffect::parameters`, so `getParameterCount()` returns the SDK default 0. This
track introduces the whole surface.

**Surface boundary — owns:** a **new pure parameter-identity module** (the frozen id table,
the `DeckParam` ↔ `ParamID` mapping, the derived exposed set, the unit assignment — with its
own `<module>_tests` target), `shell/instrument/reasampler_processor` + `processor_state` (the
`IEditController` parameter surface and the `IParameterChanges` read),
`shell/instrument/editor_controls.cpp` and the editor's drag-commit sites (the
`beginEdit`/`performEdit`/`endEdit` bracketing), and the bake's reset step **for the
notification path only**. **Does not own** the taper (W1-T1's module, consumed), any control's
value semantics, any deck geometry, or the bake's reset *membership* (W3-T2's).

**Behavior.**
- **The blob stays authoritative; a parameter is a THIRD SURFACE onto the one model** —
  a peer of the deck knob and the overlay node, not a second copy of the value.
  `docs/product/parameter-automation.md` §6.1 states the load, host→plugin, plugin→host and
  save rules, and the two verified findings that closed the fork: this plugin is a
  `SingleComponentEffect`, where the SDK itself collapses `IComponent::setState` and
  `IEditController::setState` (`vstsinglecomponenteffect.h:41-47`), so there is one state and
  §3.3's drift hazard describes a split-component design we do not use; and the blob is a
  **cross-artifact contract** the extension's `instrument_drop` writes, which
  parameters-as-truth would silently make partial.
- **`ParamID` is an independent, hand-assigned, FOREVER-FROZEN table** — blocks of 100 per
  deck group **in signal-flow order** (Γ-F7), steps of 10 within a block, a curve dial at its
  outer knob's id + 1, blocks starting at 1000. **The full 44-id assignment is stated at
  §6.2** and is to be transcribed, not re-derived. §6.2 also for why hand-assignment beats
  derivation and why the within-block order is seeded ONCE rather than tracked against
  `cellIds`; **§6.3 for the freeze invariant, which is to be stated in the table's header with
  the same force as the command-id strings, the class UIDs and the payload field order.**
- **The exposed set is DERIVED from `isLiveDeckParam` / `liveCommitFor`, never
  hand-maintained** — a control is a parameter iff its class is `Live` or `NoteOnLatched`.
  **44 parameters** at the end of Γ-W3, enumerated by group in §7.1.
- **Everything else is OMITTED from the list entirely**, not exposed-and-flagged: the reload
  and rebuild tiers, all structural state, and the limiter enable (§3.8, settled). §7.2
  states why omission beats `kIsReadOnly`, and names the limitation plainly — the user cannot
  automate filter on/off, play mode, the pitch engine, Staged↔Spline or polyphony, and the
  unlock is to give the control a live path first.
- **Every exposed parameter reports REAL UNITS to the host** (Ruling 3). Each declares a
  **plain range**, a **`units` string**, and a **display precision**; the complete
  eight-category table covering all 44 — ranges, units, precision, and which taper each
  category carries — is `docs/product/parameter-automation.md` §6.7.1, and it is a
  specification, not a suggestion. VST3's wire format stays normalized (it cannot be
  otherwise); the requirement is met through the **plain-value layer** the SDK provides, whose
  direct precedent in the vendored tree is `public.sdk/samples/vst/common/logscale.h:221-229`
  overriding `toPlain`/`toNormalized` for a log law — the exact shape our log ms and log2
  semitone knobs need.
- **`normalizedParamToPlain` / `plainParamToNormalized` / `getParamStringByValue` /
  `getParamValueByString` route through W1-T1's taper module and the ONE formatter per unit
  category.** Three functions that agree today is a defect; the host's normalization, the
  needle angle and the overlay node must be the same function. **`toNormalized` IS the
  taper** — §6.7.3 — which is what makes the phase's one-way-door ordering structurally true
  rather than a warning someone has to remember.
- **`stepCount = 0` on all 44, and the editor's shift-snap is never exposed as `stepCount`.**
  Snapped drag and parameter continuity are independent axes; `stepCount` quantizes the
  parameter permanently, including for the host's automation, and freezes into the forever
  contract. The sweep is done and clean (§6.7.6) — every discrete control is reload or rebuild
  tier and therefore already omitted, so continuity is structural rather than lucky.
- **`IParameterChanges` is observed at BLOCK boundaries, stated in the header** — the last
  point in a block wins. Sample-accurate application would put a per-sample "did anything
  change" question on the per-voice-per-sample path, which the phase-wide guardrail forbids.
- **A host parameter change takes the control's existing commit tier and no other.** Nothing
  on the automation path may reach `reloadInstrument` or `rebuildVoiceEngine` — which §7.2's
  omissions guarantee structurally rather than by care.
- **`IUnitInfo`: one unit per deck group**, mirroring the group inventory rather than the
  editor's rows. **Order is signal flow — Γ-F7, RULED** (§6.4). Presentation index order is
  ascending id, so identity and presentation agree by construction.
- **The filter's four report plain units WITHOUT being re-tapered.** `toPlain` is read-side
  only; reporting Hz / Q / drive depth means calling `filterCutoffHzFromNorm`,
  `filterQFromNorm` and `filterDriveDepthFromNorm` — the filter module's own frozen laws,
  which `deckValueLabel` already calls today — not restating them. **The one additive piece:
  `filterNormFromDriveDepth` does not exist and must be added in `filter_params`**, beside the
  two inverses that do; the analytic inverse of a frozen law is not a change to it. §6.7.5.
- **No `kIsBypass` on anything.** The plugin is an instrument and exposes no bypass
  parameter; the limiter is a safety device, not a bypass, and binding it there would hand
  the host a control that restarts the component.
- **The bake's reset gains a notification obligation** (§9): every internal writer of a value
  that is an exposed parameter must go through the one `beginEdit`/`performEdit`/`endEdit`
  path, and the bake's reset is the codebase's first non-gesture writer. **Enumerating those
  sites is part of this track**, not a follow-up.
- **Two adjacent SDK surfaces are assessed, with dispositions, so they are not re-surveyed:**
  `IMidiMapping` is **in scope and nearly free** (a CC → `ParamID` map, one function);
  `IParameterFunctionName` is **not implemented** (its vocabulary is compressor/panner
  semantics that name nothing here); `IAutomationState` is **not implemented** (it reports the
  host's automation mode for the whole plug-in, not per parameter, so it cannot answer the one
  question §9 would have wanted it for).

**Acceptance criteria.**
- **The host lists exactly the derived set, in the ruled order, with no parameter the
  predicate does not classify `Live` or `NoteOnLatched`** — asserted against the predicate,
  not against a literal count.
- **Every id in the table is asserted unique, in its group's block, and on its step** — and a
  test fails if any id changes value, which is what makes the freeze mechanical rather than
  cultural. **The asserted values are §6.2's table verbatim**, including the signal-flow block
  sequence; a test that recomputes the ids from `cellIds` would defeat the freeze it exists
  to hold.
- **`toPlain(info.defaultNormalizedValue)` compares EXACTLY equal to the default**, per
  parameter, against a default-constructed `PlaySeconds` (and against `master_gain`'s unity),
  so a host's reset-to-default and the editor's double-click land on the same value.
  **`defaultNormalizedValue` is computed as `toNormalized(default)`, not written as a
  literal** — a grep finds no normalized default constant. If the exactness fails, it is a
  W1-T1 defect surfacing here, not a defect of this track. **Round-trip exactness at
  arbitrary values is NOT asserted** — it is not required (§6.7.7) and asserting it would
  over-constrain the taper.
- **`getParamStringByValue` prints what the editor's knob label prints**, digit for digit, at
  the same stored value, across every unit category — asserted by calling **the same
  formatter** from both sides in one test, not by comparing two independently produced
  strings. **A grep finds exactly one formatter per unit category** and no `snprintf` of a
  parameter value outside it.
- **Every exposed parameter's `units` and plain range match §6.7.1**, asserted per parameter;
  `stepCount` is asserted **zero on all 44**.
- **A host automation lane moving a Live parameter moves a sounding note; a lane moving a
  NoteOnLatched parameter takes effect on the next note and does NOT trigger a reload or an
  engine rebuild** — assert the tier, not just the sound.
- **No automation path reaches `reloadInstrument` or `rebuildVoiceEngine`.**
- **A project saved before this change opens with every parameter reading the blob's value
  and sounds identical**; a project saved by this build opens in an older binary with its
  sound intact; and a project with automation drawn, saved and reopened, replays against the
  same plain values.
- **`process()` takes no new indirection and no new per-sample work** — the parameter read is
  a block-boundary act, on the existing live-publish path.
- **The bake's reset notifies the host**, verified by the host's displayed value following it
  rather than snapping back on next touch.
- **The double-processing limitation is documented, not discovered** — a bake whose
  reset-class parameter carries a host lane is a named boundary of the bake's fidelity claim
  (§9), stated in the product doc and in this track's review.
- **The lane-linearity consequence is stated in the header, not left to be found** — under a
  tapered parameter a straight line drawn in a host automation lane is **not** linear in the
  plain unit (exponential in ms, linear in octaves on cutoff, linear in dB on master gain).
  This is standard and desirable, it follows directly from Ruling 3 plus the taper, and §6.7.4
  gives it per category. Writing it down is the acceptance criterion; changing the taper to
  avoid it is not an option.
- **The filter's four are proven untouched**: a regression baseline shows their audio
  unchanged, and their persisted `*Norm` values are byte-identical across a save/reload that
  passes through the parameter surface. Reporting Hz/Q/depth changed display only.

**Open questions.**
- **No [Daniel] questions. Γ-F7 is RULED — signal flow** (2026-08-01, *"signal flow order."*),
  and Ruling 3 (real units) arrived specified rather than forked. **There is no unanswered
  [Daniel]-class question in this track or anywhere in this plan.**
- **[verify, FIRST]** that REAPER calls `setState` (not `setComponentState`) on a
  single-component plug-in, and the ordering of `setState` against the first
  `IParameterChanges` block after a project load. §6.1 is built on the SDK's own
  name-collapse; **verify it in the DAW before wiring, and do not build on the paragraph
  alone.**
- **[verify]** whether REAPER renders `ParameterInfo::units` beside the string
  `getParamStringByValue` returns, or shows the string alone. **We ship the SDK's own
  convention** — digits in the string, unit carried separately, which is what
  `RangeParameter::toString` and the `Parameter` constructor's signature both express. If
  REAPER shows no unit at all, the fallback is to append the unit **inside the one formatter**:
  a one-line change in one place, touching neither the frozen id table nor the editor, because
  display strings are explicitly not frozen (§6.7.1). Do not discover this after shipping.
- **[propose at review]** promoting **key-track** and **Trigger length** from `Reload` to
  `NoteOnLatched` (§7.4). Both are excluded from the live set *for the note-on-latch reason*
  in the predicate's own words, so the promotion aligns routing with documented semantics —
  and it is what makes them automatable at all. **If either promotion is refused, that
  control simply drops out of the parameter list.** The list follows the predicate; the
  predicate is never bent to fill the list. **Consequence for the frozen table:** a refusal
  drops ids 1000 and 1260 (key-track) or 1450 (Trigger length) and the count falls below 44.
  Those slots are then simply **never issued** — not retired, since nothing shipped under
  them — and remain available to the same control if it is promoted later. No other id moves;
  that is what the block-and-step scheme buys.
- **[propose at review]** whether to ship a default `IMidiMapping` CC table here or leave MIDI
  control to REAPER's host-side learn. Either is defensible; **skipping it silently is not.**
- **[propose at review]** whether this track spends the reserved payload rung. §6.1 says
  nothing new is persisted and therefore it should not; if the `setState` verification says
  otherwise, it takes the reserved rung and says so.
- **Closed, do not reopen:** Rate lifted from latched to live (§3.5 records the cost); the
  limiter enable made automatable (§3.8 — its one reopening condition is the `docs/TODO.md`
  reload/activation decoupling, and the answer is to do that first, not to re-litigate the
  classification).

---

## 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` |

Ψ.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 are DELIVERABLES of this phase, not asides.** Each is
scheduled in — and an acceptance criterion of — its owning track. This spec schedules
them; the implementing track performs them:

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. **Four tracks, disjoint by surface:**

| Track | Owns |
|---|---|
| **T1** `capture-range-exactness` | `core/capture/render_settings`, `shell/capture/capture.cpp`'s render-configuration block, `shell/capture/scope_resolve` (source-mode selection only), `tests/test_render_settings.cpp` |
| **T2** `mode-switch-discipline` | `core/view/view_mode_model` (solo cache), `shell/view/view.cpp` (`applyMode` seams + WANT block), `shell/actions/design_view_actions.cpp` (refusal feedback), `shell/panel/panel_input.cpp` **footer mode-segment block only** (`:300-309`), the mode segment's disabled state in `core/ui/footer_bar` + `shell/panel/panel_render.cpp` |
| **T3** `media-explorer-section` | `shell/actions/ingest.cpp` (register/dispatch/unregister), **the registration block in `src/app/main.cpp`** (the `hookcommand2` hook + unload mirror), the root-`CLAUDE.md` contract amendment |
| **T4** `drop-target-resolution` | `core/ui/drag_out`, `core/wire/instrument_drop`, `shell/panel/panel_drag.cpp`, `shell/actions/instrument_drop_win.cpp`, `shell/actions/drag_out_win.cpp` (hand-off timing), `tests/test_drag_out.cpp` |

**Why these four are parallel.** T1 is capture core/shell; T2 is view core/shell plus one
fenced block of `panel_input.cpp`; T3 is action registration; T4 is the drag chain. No
two tracks own the same function anywhere.

**Two shared-file adjacencies, named rather than discovered at merge.**
(a) `shell/panel/panel_input.cpp`: T2 owns the footer mode-segment block (`:300-309`)
only; T4 may touch the drag-arm block (`:382-402`) only. Separate blocks — textual
adjacency, not semantic contention; whichever lands second rebases.
(b) **`src/app/main.cpp` is T3's exclusively.** T4 must not touch `main.cpp` — its whole
redesign lives in the drag chain and registers nothing. T2 also does not touch it: the
playback gate lives inside `applyMode` conditioned on the real-switch discriminator, so
`main.cpp:173`'s project-load reapply passes through unchanged.

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

**Goal.** A capture over a time selection or razor area on a source item substantially
longer than the selection produces exactly the requested range — both scopes, no
whole-item widening. (Ψ.7)

**What is already correct, so the fix does not wander.** Range resolution is correct end
to end: `scope_resolve.cpp:100-112` (`resolveRange`) → razor union
(`render_settings.cpp:135-170`) or `GetSet_LoopTimeRange` (`scope_resolve.cpp:35-40`),
passed verbatim (`capture_orchestrator.cpp:221-222`), landed as
`RENDER_BOUNDSFLAG=0` + exact `RENDER_STARTPOS`/`RENDER_ENDPOS`
(`capture.cpp:320-322`). There is no item-bounds fallback in `RunCapture`. **The widening
decision point is the source-mode bit**: item scope maps to
`kRenderSelItems (&32) | kRenderSingleFile` (`render_settings.cpp:82-86`), and the repo's
own comments treat `&32` as item-extent-driven (`capture_batch.cpp:64-66`, `:98-99` —
the entire reason batch transiently selects one item per render). The working inference —
**unverified without a DAW** — is that REAPER's "selected media items" render source
overrides the custom time bounds. Track scope uses `&128` (selected tracks via master), a
normal time-bounded render, and **no code path was found that widens it**.

**Behavior.**
- **The track opens with a DAW repro matrix, before any code change** `[verify]`:
  item scope × {time selection, razor} and track scope × {time selection, razor}, over a
  source item substantially longer than the selection, **tail mode None**. This confirms
  the `&32`-overrides-bounds inference, and disambiguates Ψ.7's "item/track both" claim —
  the candidates are: the reporter saw only the item scope; or a non-`None` panel tail
  mode was active (`capture_orchestrator.cpp:217`; Auto adds an 8 s window,
  `render_settings.cpp:28-37`). If track scope reproduces with tail None, the analysis
  above is wrong and the track says so before proceeding.
- **The fix architecture is a design call at implementation review** `[propose]`, with
  the two candidates named now:
  - **(a) Re-source the ranged item capture.** When item scope carries an explicit range,
    render time-bounded (e.g. selected-track source `&128` over the item's own track,
    with the item-scope FX-bypass plan unchanged — it already neutralizes the track's own
    track-FX chain and everything above). Semantic edge to resolve: another item on the
    same track overlapping the range would now be audible in the capture, where `&32`
    excluded it.
  - **(b) Render-then-trim.** Keep `&32` (only the selected item's audio, REAPER's own
    semantics) and trim the rendered file to the requested range afterward with the pure
    `wav_codec` tools, sample-exactly (offset = requested start − rendered start at the
    file's sample rate). Precedent: `trimAutoTailInPlace`
    (`capture_realtime_finalize.cpp:55-125`). Cost: renders more than needed; requires
    knowing the rendered file's start time exactly.
  Whichever candidate lands, the choice is judged against the acceptance criteria below,
  not against convenience.
- **Ripple surfaces if the item-scope source mode changes**, enumerated so none is
  discovered late: `capture_batch.cpp:98-99`'s select-one mechanism, item-scope FX
  fingerprinting reading `CountSelectedMediaItems` (`scope_resolve.cpp:199-207`), and
  `core/model/provenance` recipes storing `sourceMode` as an int
  (`scope_resolve.cpp:186`) — a recorded recipe must replay to the same audio.
- **Batch semantics are deliberately different and stay so:** `RunBatchCaptureItems` is
  one sample per item at item extent (`capture_batch.cpp:177-179`) — that is its meaning,
  not this defect. Stated so nobody "fixes" it.

**Acceptance criteria.**
- **The bounds equality, stated as a number:** the captured file's frame count equals the
  requested range's duration at the project sample rate — exactly, no rounding — for
  BOTH scopes, with both a time selection and a razor area, over a source item
  substantially longer than the selection, tail None. `[verify — DAW]`
- The null test holds for a ranged item capture: re-inserted at the range start, it nulls
  against the source over the range. `[verify — DAW]`
- Bit-identical repeats hold for the ranged capture.
- Full-item captures (no time selection/razor, or bounds equal to the item) are
  byte-identical to today's output.
- Recapture-from-source of a ranged capture reproduces it (the provenance recipe stays
  truthful under the chosen fix).
- `tests/test_render_settings.cpp` pins the chosen mapping (today `:44-47` asserts only
  that the `&32` bit is chosen — never its bounds interaction); any new pure trim/window
  arithmetic lands in `core/capture` with unit tests.
- **DAW-verification obligation:** Daniel observes the four-cell matrix above, plus one
  razor-union case (two disjoint razor areas — the union rule at
  `render_settings.cpp:135-170` is bounds-driven and must not regress).
- Root `CLAUDE.md:208`'s **"Exact bounds"** invariant needs no amendment — this track
  makes the code honor it; the invariant was always the spec.

**Open questions.** `[verify]` the override inference and the track-scope repro (the
matrix above). `[propose]` fix candidate (a) vs (b), including (a)'s overlapping-item
semantic edge and (b)'s rendered-start-time derivation.

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

**Goal.** Switching the active mode caches and clears the outgoing mode's solo state and
restores the incoming mode's — disjoint solo surfaces per mode — and the switch itself is
refused, visibly, while the transport is playing or recording. (Ψ.2 + Ψ.3)

**The invariant amendment comes first, because without it this track is a breach.** The
never-touch-solo rule is stated three times: `src/shell/view/CLAUDE.md:14-17` ("Never
touches master or `B_MUTE`/`I_SOLO`… User mute/solo survives every toggle untouched"),
`src/core/view/CLAUDE.md:10`, and `docs/product/design-view.md:160-165` (framed as the
analog of capture's non-destructive invariant) + `:588-592`. **The amended form:** the
tool never *loses* the user's solo state — solo is cached per mode on a real switch and
restored verbatim on return, the snapshot sense of non-destructive, exactly as the
park/restore snapshots already treat visibility and FX state. `B_MUTE` stays untouched
absolutely; the master track stays untouched absolutely (and survives for free —
`handleByGuid` is built from `GetTrack`'s index space, which excludes master,
`view.cpp:90-91`). All three files are amended by this track.

**Surface boundary — owns:** `core/view/view_mode_model` (the solo cache: storage,
serialize/deserialize, reconcile participation — mirroring the existing `snapshots_` map,
`view_mode_model.h:284-358`), `shell/view/view.cpp` (the `applyMode` seams and its
`REAPERAPI_WANT` block, `:21-41`), `shell/actions/design_view_actions.cpp` (refusal
feedback on the action path), `shell/panel/panel_input.cpp`'s footer mode-segment block
ONLY (`:300-309`), the segment's disabled state in `core/ui/footer_bar` (pure predicate,
transport bool passed IN from the shell) and its paint in `shell/panel/panel_render.cpp`
(`:110-125` region), the three invariant-amendment files, `tests/test_view_mode_model.cpp`.
**Must not touch:** `panel_drag.cpp` (T4's), `src/app/main.cpp` (the gate's discriminator
makes the project-load reapply pass through unchanged), `B_MUTE`, master, and the
park/restore planner's existing semantics.

**Behavior.**
- **The discriminator is the hinge for both halves.** `applyMode` is also the reapply
  path — called with `targetModeId == activeModeId()` from `reapplyActiveMode`
  (`design_view_actions.cpp:106-108`, reached from tag/untag/show-both at
  `:187,197,206`) and from project load (`main.cpp:173`). A real switch is
  `targetModeId != model.activeModeId()`. **Only a real switch caches/clears/restores
  solo, and only a real switch is playback-gated** — reapply during playback (tagging,
  project load) keeps working, and there is no cache/clear/restore flicker on reapply.
- **Solo cache semantics.** On a real switch: read `I_SOLO` for every track in the live
  enumeration (`handleByGuid`, keyed by GUID — all non-master tracks, not just managed
  leaves: a mode's solo surface is the whole project as seen in that mode); store the
  non-zero values into the model keyed by the OUTGOING mode id (`model.activeModeId()`
  is still the outgoing mode at the capture seam); write `I_SOLO = 0` on every track that
  had it set; after `setActiveMode` (`view.cpp:455`), restore the INCOMING mode's cached
  entries verbatim and consume them (clear-on-restore, mirroring
  `storeSnapshot`/`clearSnapshot`). **Values are cached and restored verbatim as the raw
  `I_SOLO` int — never collapsed to a boolean** (solo-in-place and safe-solo variants
  survive the round trip).
- **Seams in `applyMode`:** capture after `handleByGuid` is populated (`:386`) and before
  `Undo_BeginBlock2` (`:399`); the clear and restore writes both ride the existing single
  undo block (clear at its head, restore after `setActiveMode` `:455` and before
  `TrackList_AdjustWindows` `:460`) — one mode toggle stays one Ctrl-Z.
- **Storage is pure, shell touches only the API pair.** The cache lives in
  `ViewModeModel` beside `snapshots_` with the same `deserialize(serialize(x)) == x`
  contract (`view_mode_model.h:347-350`); only the
  `GetMediaTrackInfo_Value`/`SetMediaTrackInfo_Value` pair lives in `view.cpp`, exactly
  as `snapshotTrack` (`:121-134`) does. `core/` includes no REAPER headers.
- **Reconcile participation — decided: the solo cache prunes on dead GUIDs**, like
  snapshots and unlike membership (`view.cpp:389-395`). The hazard it removes: a stale
  entry restoring onto a reused GUID. A pruned entry is simply lost solo state for a
  track that no longer exists — correct.
- **Persistence rides the one blob.** `ViewModeModel::serialize()` →
  `SetProjExtState(proj, "reasampler", "view_state")` (`ext_state_io.cpp:156-158`, key
  `ext_keys.h:31`). `parseModel` skips unknown keys (`view_mode_model.cpp:603`), so the
  new key is backward-safe: an older build reading newer state drops the solo cache
  silently and nothing else breaks. Stated, accepted.
- **The playback gate.** Inside `applyMode`, when the discriminator says real switch and
  `GetPlayStateEx(proj) & (1|4)` (bit 1 playing, bit 4 recording — the precedent is
  `capture_realtime_shell.cpp:284`, project-scoped `*Ex` variants deliberately,
  comment `:192`), refuse before touching anything: return `false`, no partial apply
  (the same fail-closed shape as the mode-exists guard at `:381`). Add
  `REAPERAPI_WANT_GetPlayStateEx` to `view.cpp`'s WANT block.
- **The refusal is visible, not a silent no-op.** The footer `[Arrange|Design]` segment
  renders a disabled state while the transport runs, following the one existing disabled
  precedent end to end: pure predicate (`mode_enable`-style, transport bool passed IN so
  `core/ui` stays REAPER-free) → layout/paint state → no-op click. The action path's
  refusal (`doToggleMode`/`doActivateMode`) reads `applyMode`'s `false` and skips
  persist/invalidate.
- **The panel's direct-call divergence is closed in this track — decided.** The footer
  segment click calls `applyMode` directly (`panel_input.cpp:300-309`, passing `nullptr`
  project), skipping the `persistViewState()` + `bankPanelInvalidate()` the action path
  does (`design_view_actions.cpp:165-180`) — a pre-existing defect: a panel-initiated
  switch does not persist. The segment click now routes through `Main_OnCommand` of the
  activate actions, exactly as the bottom-bar tag buttons already do
  (`panel_input.cpp:74-75`). One path, one gate, one persist.

**Acceptance criteria.**
- **The disjoint-surface matrix, DAW-observed:** solo two tracks in Arrange; switch to
  Design → no track is soloed; solo a third track in Design; switch back → exactly the
  two Arrange solos are restored (raw `I_SOLO` values verbatim, including solo-in-place)
  and the Design solo is gone; switch forward again → the Design solo is restored.
- Reapply paths (tag/untag/show-both, project load) neither clear nor flash solo state —
  before or after this track, during playback or stopped.
- With the transport playing or recording: the switch is refused on EVERY path (action,
  panel segment), the segment visibly renders disabled, and project state is untouched by
  the refused attempt. Stopped → the switch works. Recording gates identically to
  playing.
- A panel-initiated switch now persists (the divergence closure): switch via the footer
  segment, save, reload → the project reopens in the switched mode.
- Solo cache round-trips save/reload: save mid-disjunction, reload → the inactive mode's
  cached solos restore on the next switch.
- One mode toggle remains one Ctrl-Z; `B_MUTE` is never written; master is never touched.
- The three invariant-amendment files land amended, in this track.
- Pure model changes covered in `tests/test_view_mode_model.cpp` (store/consume,
  serialize round-trip, reconcile pruning); the enable predicate is pure and tested.
- **DAW-verification obligation:** Daniel runs the solo matrix, the playback-gated
  refusal (playing AND recording), and the panel-persist case.

**Open questions.** `[verify]` `I_SOLO` is settable via `SetMediaTrackInfo_Value` and its
value domain — against `vendor/reaper-sdk/sdk/reaper_plugin_functions.h`, per root
`CLAUDE.md`'s API-verification rule (settability is inference today). `[verify]` adding
`REAPERAPI_WANT_GetPlayStateEx` to `view.cpp` alone compiles (`main.cpp:15` does not
define `REAPERAPI_MINIMAL`, so it should — inference, one compile answers it).
`[propose]` the repaint trigger for the disabled segment (the panel must notice transport
transitions; lean: poll play state in the panel's existing timer tick and invalidate on
change). `[propose]` restore-onto-parked-track edge: a track soloed in mode B, then
re-tagged so it is parked when B next activates, would on verbatim restore solo a
silent track and mute the mix — lean: restore skips tracks parked in the incoming mode
and drops those entries, with the alternative (retain until the track re-enters) named
and rejected as zombie state.

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

**Goal.** The Media-Explorer import action appears in the Media Explorer action section,
so it can be bound and placed on the Media Explorer toolbar. (Ψ.4)

**Why it lands in Main today (SDK-verified):** the action registers via `"gaccel"`
(`ingest.cpp:457-468`), and `gaccel_register_t` registers into the main keyboard section
only — the struct has no section field (`vendor/reaper-sdk/sdk/reaper_plugin.h:1106-1117`).
**The supported mechanism (SDK-verified):** `custom_action_register_t`
(`reaper_plugin.h:1090-1103`) — `{uniqueSectionId, idStr, name, extra}`, with **Media
Explorer = section 32063** (`:1099`, corroborated `reaper_plugin_functions.h:3467`);
`Register("custom_action", &ca)` returns the command id or 0; `idStr` must be unique
**across all sections** (`:1100`). Dispatch for non-main sections MUST come from
`"hookcommand2"` (`reaper_plugin.h:212-221`) — `"hookcommand"` runs only for the main
section (`:200-205`), and `main.cpp:309` registers only `"hookcommand"` today.
`custom_action_register_t` carries no `ACCEL`, so the non-Main entry ships no default
keybinding — acceptable; toolbar placement is the ask.

**Settled, not a fork: the action registers in BOTH Main and Media Explorer.** Ψ.4 asks
for reach, not relocation, and a bare move would orphan any existing Main-section
keybinding — a half-fix. Because `idStr` is unique across all sections, the Media
Explorer registration **mints a second FOREVER-STABLE command-id suffix:**
**`INGEST_IMPORT_MEDIA_EXPLORER_MX`**, composed per channel via `channelCommandId` like
every other id (`core/version/app_version.cpp:43-47,84-86`) — a new permanent entry in
each channel's id family, never to change after shipping. The existing
`INGEST_IMPORT_MEDIA_EXPLORER` string is preserved unchanged for the Main entry, so root
`CLAUDE.md:180-183`'s never-change contract is honored literally. Both registrations
share the one handler (`doImportFromMediaExplorer`, `ingest.cpp:317-395`).

**Surface boundary — owns:** `shell/actions/ingest.cpp` (the second registration, the
`hookcommand2` claim, the `-custom_action` unload mirror beside the existing mirrors at
`:476-481`), **the registration block in `src/app/main.cpp`** (registering the
`"hookcommand2"` hook beside `"hookcommand"` at `:309`, and its unload mirror at the
`rec == nullptr` path), and the root-`CLAUDE.md` §"REAPER extension contract" amendment
(the second registration mechanism, documented beside the 4-step main-section pattern).
**Must not touch:** any other action family's registration, `panel_*`, `capture*`. T4
must not touch `main.cpp`; this track is why.

**Behavior.**
- Main-section registration byte-identical to today: same id string, same gaccel, same
  keybindings surviving.
- Second registration: `custom_action_register_t{32063, channelCommandId(new suffix),
  channelActionName(same display phrase — decided: identical phrase in both sections,
  for findability), nullptr}` via `Register("custom_action", ...)`. A 0 return is
  tolerated gracefully (the Main entry still works; a console note, not a failure).
- `hookcommand2` claims ONLY the new MX command id and returns `false` otherwise —
  `hookcommand2` fires for every section including Main, and the Main id stays claimed by
  the existing `hookcommand` path (`ingest.cpp:470-474` via `main.cpp:222-230`); the two
  ids differ, so no double dispatch is possible. Stated as a criterion, not an accident.
- Unload mirrors everything: `-custom_action` with the same struct/pointer, alongside the
  existing `-gaccel`/`-command_id`.
- Beta channel forks both ids automatically through `channelCommandId` — each channel
  gains exactly one new permanent id.

**Acceptance criteria.**
- The Actions list, section "Media Explorer", shows the channel-prefixed action; it can
  be added to the Media Explorer toolbar; firing it from that toolbar imports the last
  played Media Explorer file into a ReaSampler on the selected track — identical behavior
  to the Main-section entry. `[verify — DAW]`
- The Main-section entry is unchanged: same command id, existing keybindings intact.
- Extension unload unregisters both entries cleanly; reload re-registers both.
- The new suffix `INGEST_IMPORT_MEDIA_EXPLORER_MX` is recorded as FOREVER-STABLE, per
  channel, in the same breath as the registration lands.
- Root `CLAUDE.md`'s "REAPER extension contract" section gains the non-main mechanism
  (`custom_action` / `hookcommand2` / `-custom_action` mirror) — a deliverable of this
  track.
- **DAW-verification obligation:** Daniel adds the action to the Media Explorer toolbar
  and imports from it; also confirms the Main-section binding still fires.

**Open questions.** `[verify]` re-verify `custom_action_register_t` and `hookcommand2`
argument order/types against the SDK headers at implementation time (root `CLAUDE.md`
rule; the section ids and struct shapes above are already header-verified, the rule
applies regardless). None classified `[propose]` — the ruling above is settled.

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

**Goal.** Dragging cards out of the panel resolves its target from what is actually under
the cursor — continuously, reversibly, with a defined outcome and a visible cue for every
surface, and no silent no-op release anywhere. (Ψ.5)

**The root causes, so the redesign is judged against them.** The current chain locks a
drag's fate at its first processed move outside the client rect
(`core/ui/drag_out.cpp:18-26`): a single-card drag over ANY REAPER surface —
`GetThingFromPoint` returns non-empty info for the arrange too
(`instrument_drop_win.cpp:84-86`) — locks to `InstrumentDrop` and returns before the OS
hand-off is ever evaluated (`panel_drag.cpp:285-294`); over the arrange the info string
fails the FX-hotspot rule, so release falls to a **silent no-op** (root cause A — the
verified headline: single-card arrange drops do nothing, while multi-card drags skip the
FX resolve (`:276`) and work via OS drag, which is exactly Ψ.5's "sometimes"). The
`OsDrag` branch is irreversible — `ReleaseCapture` + `resetDragState` + modal
`DoDragDrop` (`:299-335`) — and `WM_MOUSEMOVE` coalescing makes the deciding exit pixel
vary with drag speed (root cause B). The FX "hotspot" is only the FX-button glyph family
(`fx_*`/`tcp.fx*`/`mcp.fx*`, `core/wire/instrument_drop.cpp:90-95`) — TCP body/name, or
a TCP too narrow to draw the button, yields `"tcp"` and a cue-less no-op (root cause C).
Multi-card drags can never instrument-drop (root cause D). One unresolvable hand-off
evaluation sets `dragOsHandoffBlocked` for the remainder of the drag (`:320`, cleared
only in `resetDragState` — root cause E). Coordinate spaces and DPI are NOT causes
(verified); bank-to-bank never leaves the client and is CTest-covered
(`tests/test_drag_out.cpp:33-112`) — consistent with "dragging between banks seems
fine", and it must stay byte-identical.

**The redesign is a gesture law, not a patch.** The law: **the target class is resolved
from what is under the cursor on every move, every class transition is reversible until
release or until the pointer leaves REAPER entirely, and the OS hand-off is reserved for
leaving REAPER** — every REAPER-internal target is executed natively on release. The
minimal patch (re-evaluating the hotspot per move but keeping the early irreversible
`OsDrag` fork) is named and rejected: it re-creates root cause B — a drag that crosses
the arrange en route to an FX button would still lose the instrument drop forever.

**The class enumeration and its defined outcomes (single-card / multi-card):**

| Under the cursor | Single card | Multi card |
|---|---|---|
| Inside the panel client | Internal drag (unchanged, byte-identical) | Internal drag (unchanged) |
| FX hotspot (`fx_*` window, or track panel — see below) | **Instrument drop** (existing `.vstpreset` path) | **Refuse-with-cue** (no instrument drop for a multi payload — root cause D becomes a defined, cued outcome) |
| TCP/MCP, whole panel (not just the FX glyph; `*.fxembed` still excluded) | **Instrument drop — decided:** the whole track panel is the hotspot. Root cause C IS the FX-glyph-only rule; a capture dropped on a track's panel means "sampler on this track." The rejected alternative — TCP drop = item at edit cursor — is silent-placement-adjacent and has no time coordinate; the arrange owns placement. | Refuse-with-cue |
| Arrange | **Timeline item at the pointer's track/time** — native insertion on release | Native insertion, shape at review (see open questions) |
| Elsewhere in REAPER (ruler, transport, docker chrome) | Refuse-with-cue | Refuse-with-cue |
| Outside REAPER entirely | OS drag-out (`DoDragDrop`, CF_HDROP, copy-only — unchanged mechanics, new trigger condition) | OS drag-out |

- **Every release either performs the resolved action or visibly refuses** — the cue
  (cursor via the existing `applyDragCursor` precedent, plus the panel's drag paint
  state) tracks the resolved class continuously, so "will this work" is visible before
  release, and no cell in the matrix is a silent nothing.
- **The blocked flag is retired as a drag-lifetime latch** (root cause E): an
  unresolvable evaluation refuses that evaluation, not the rest of the drag.
- **The arrange outcome does not violate the load-bearing principle, and the spec says so
  where a reviewer will look:** the item is placed because the USER dragged it to that
  spot — user-initiated placement, the same class of act as `RunInsertSelected`, not a
  capture auto-insert. Root `CLAUDE.md`'s "capture and placement are separate acts" is
  about capture never placing; a deliberate drop is placement on demand.
- **The pure decision stays pure.** The class enumeration, its precedence, and the
  single/multi split land in `core/ui/drag_out` (the law) +
  `core/wire/instrument_drop` (the info-string classification), CTest-covered over the
  full matrix; the shell reads live pointer/window state and executes outcomes, exactly
  the discipline `card_drag` already follows.
- **Hot-path discipline:** the inside-client path stays free of SDK hit-tests
  (`GetThingFromPoint` is evaluated only outside the client rect, as today —
  `panel_drag.cpp:273-274`); per-move resolution outside the client now runs for multi
  payloads too, which is new but still per-mouse-move cold.

**Surface boundary — owns:** `core/ui/drag_out` (the gesture law, rewritten),
`core/wire/instrument_drop` (classification), `shell/panel/panel_drag.cpp` (routing +
release dispatch), `shell/actions/instrument_drop_win.cpp` (hit-test shell),
`shell/actions/drag_out_win.cpp` (hand-off trigger timing), `tests/test_drag_out.cpp`.
May touch `panel_input.cpp`'s drag-arm block (`:382-402`) only — T2's footer block is
fenced. **Must not touch:** `src/app/main.cpp` (T3's — this track registers nothing),
`core/ui/card_drag` and the in-grid reorder/replace semantics (bank-to-bank stays
byte-identical), the capture pillar, the insert action's own semantics.

**Acceptance criteria.**
- The full matrix above, DAW-observed, single AND multi: every cell produces its defined
  outcome with its cue, and in particular — a single-card drop onto the arrange lands an
  item at the pointer's track and time (the headline defect); a single-card drop onto a
  track panel (not just the FX glyph) loads the instrument; a multi-card drop onto the
  arrange still lands items; every refuse cell visibly refuses.
- **Reversibility:** leave the client, cross the arrange, reach an FX window →
  instrument drop still available; return into the panel client → internal drag resumes.
  One unresolvable evaluation does not change any later evaluation's outcome.
- **Drag speed does not change outcomes:** fast flicks and slow drags to the same release
  point resolve identically (the coalescing hazard is designed out with the early lock,
  not mitigated).
- Bank-to-bank drags are byte-identical to today; `tests/test_drag_out.cpp:33-112` (or
  their successors) stay green with unchanged semantics.
- The pure law's CTest matrix covers every class × single/multi combination, including
  the `*.fxembed` exclusions and the null-track/non-empty-info cases the SDK documents
  for `GetThingFromPoint` (`reaper_plugin_functions.h:3440-3446`).
- No new SDK hit-test inside the client rect.
- **DAW-verification obligation:** Daniel runs the matrix, explicitly including the
  narrow-TCP case (root cause C's trigger), fast-flick drags, and a drag that crosses the
  arrange before reaching an FX window.

**Open questions.** `[verify]` pointer→(track, time) for the native arrange insertion —
candidate API `GetSet_ArrangeView2` (pixel-column → time mapping) plus
`GetThingFromPoint`'s track; verify signatures and behavior against the SDK header
before building on them; if no exact pixel→time mapping exists, the arrange outcome's
mechanism (not its existence) is re-proposed. `[verify]` what REAPER does with a CF_HDROP
drop re-entering its windows during our `DoDragDrop` (the accepted residual once the
pointer has left REAPER and returns mid-modal-loop — confirm it is REAPER's own file
import, then state it). `[propose]` the multi-card arrange insertion shape (lean: one
item per file mirroring REAPER's own multi-file drop convention; alternative: retain the
OS hand-off for the multi-arrange cell only — costs a second delivery mechanism and a
modal fork, named to be rejected on uniformity unless the native shape fails
verification). `[propose]` the exact refuse-with-cue rendering (cursor-only vs cursor +
panel status text).

---

### Ψ-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.)

**Two tracks, parallel, disjoint by field family:** T1 owns the naming fields
(`baseName` / `uniqueTag` / `displayName` / `capture_paths`), T2 owns the channel fields
(`channelCount` / PCM / `wav_codec`). **The known collision seam, named now:** both
tracks touch `shell/capture/capture.cpp` and
`shell/capture/capture_realtime_finalize.cpp` at adjacent lines. In `capture.cpp`, T1
owns the tag/paths mint (`:310-314`) and the `Sample` id/label lines (`:398-402`); T2
owns `RENDER_CHANNELS` (`:353-354`), the new post-render collapse step inserted between
the exists-check (`:390-395`) and the `Sample` population (`:398`), and
`stampCaptureSample`'s channel echo (`:207`). In `capture_realtime_finalize.cpp`, T1 owns
the label site (`:195`); T2 owns the layout-parse/channel lines (`:188-198`) and the
collapse insertion. Adjacent lines, disjoint fields — textual merge adjacency, not
semantic contention; whichever lands second rebases.

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

**Goal.** Captures are labeled after their source track's name plus a discriminator, on
every mint site — and the name is visible where the user looks. (Ψ.1)

**Today, and why it reads as broken:** every offline capture's `displayName` is the
literal `"item"` or `"track"` (`CaptureActionDef` → `render_settings.cpp:175-183` →
`capture.cpp:402`), the filename is `"item_<epoch>-<counter>.wav"`
(`makeUniqueTag`, `capture.cpp:187-199`; `deriveBankPaths`,
`core/capture/capture_paths.cpp:48-72`) — and **the source track name is read nowhere in
the tree** (repo-wide: zero hits for `P_NAME`/`GetTrackName`; the capture path holds
`MediaTrack*` and GUIDs but never a name). Worse, **the label barely surfaces:** the
panel card draws no name at all (`panel_render.cpp:23-63` — waveform, bars.beats,
seconds.ms only); the sample name appears only inside the VST3 editor. A good name
nobody can see is a half-fix, so this track also surfaces it.

**Surface boundary — owns:** `shell/capture/scope_resolve` (the ONE place already
walking source tracks before the FX-bypass guard — gains the name read, plumbed through
`ResolvedSource`, `scope_resolve.h:33`), `shell/capture/capture.h` (`CaptureRequest`),
`capture_orchestrator.cpp`'s naming lines (`:229`, `:405`), `capture.cpp`'s mint/label
lines (`:310-314`, `:398-402`), `capture_batch.cpp`'s mint sites (`:220`, `:284`,
`:395`), the realtime label chain (`capture_realtime_shell.cpp:309-310`,
`capture_realtime_finalize.cpp:195`, `core/capture/capture_realtime.cpp:31-32`),
`core/capture/render_settings` only for `CaptureActionDef::baseName` semantics
(`render_settings.h:170-175` — the literals become fallbacks), and the card name
surface: pure geometry in `core/ui` (`bank_grid` / `card_meta`) + its draw in
`shell/panel/panel_render.cpp`. **Must not touch:** `channelCount`/PCM/`wav_codec`
(T2's), the render-configuration block W1-T1 landed (read-only here),
`core/capture/capture_paths`'s purity — it stays REAPER-free path arithmetic with **no
filesystem access** (`core/capture/CLAUDE.md`); any collision handling stays
`makeUniqueTag`'s, shell-side.

**Behavior.**
- **The name is resolved shell-side in `scope_resolve`,** before the FX-bypass guard,
  alongside the GUID walk it already does (`scope_resolve.cpp:53-72`, `:114-127`):
  track scope → the selected track's name; item scope → the selected item's owning
  track's name. `[verify]` the read API (`GetSetMediaTrackInfo_String` with `"P_NAME"`
  vs `GetTrackName`) against the SDK header before use.
- **Label composition:** `displayName = "<TrackName> <discriminator>"`. The
  discriminator's exact format is `[propose]` (lean: a compact date-time derived from the
  capture's own `createdTimestamp`, e.g. `Bass 08-01 1432` — Daniel's "date, etc."
  names it); `displayName` remains explicitly NOT unique, per the existing rule
  (`core/model/CLAUDE.md` §`resample_name`) — the discriminator serves legibility, not
  uniqueness.
- **The filename becomes meaningful too:** `baseName` = the sanitized track name, so the
  stem is `"Bass_<tag>.wav"` via the existing `sanitizeStem` + `makeUniqueTag` pipeline —
  stem uniqueness remains entirely `makeUniqueTag`'s job, unchanged. A name the sanitizer
  reduces to nothing (non-ASCII) falls back to `"capture"` for the stem
  (`capture_paths.cpp:24-46`'s existing rule) while `displayName` keeps the real name.
- **Fallback for an unnamed track:** deterministic, matching REAPER's own display
  convention (`"Track N"` by index at capture time) — never the old literals on the
  interactive paths.
- **All five mint sites covered, each stated:** offline (above); **realtime** — same
  resolution at `begin` time (the action has a selected track); **batch** — per-unit
  owning-track name, the batch ordinal retained as an additional discriminator
  (`"Bass 1"`, `"Bass 2"`… exact composition settled with the discriminator format);
  **recapture** — decided: preserves the existing entry's `displayName` (it regenerates
  an entry, it does not mint a new identity; only the file stem re-mints);
  **ingest** — unchanged (already meaningful: the source file's stem,
  `ingest.cpp:249-266`); **bake** — unchanged (`model::nextIterationName`, the
  `Kick r2` precedent). **The bake's naming-and-lineage open question is Ξ's, jointly
  held with Ξ-W1-T1, and this track does not close or touch it.**
- **The card shows the name — decided in-scope:** one truncated name line on the panel
  card, geometry pure (`bank_grid`/`card_meta`, CTest-covered), drawn by palette role
  through the kit. Truncation is display-only; the stored value is never shortened. If
  review finds the card too crowded at the smallest cell size, the fallback is
  name-in-tooltip — `[propose]` at review, with on-card as the lean.
- **Existing bank entries are untouched** — new captures only; no retroactive rename.
- `displayName` already flows to the instrument (component-state copy + one-way refresh,
  `component_state_io.cpp:74-88`, `sample_map.cpp:96`) — no instrument-side work.

**Acceptance criteria.**
- An item capture from a track named `Bass` yields `displayName` `Bass <discriminator>`
  (format as settled at review) and a file `Bass_<tag>.wav`; a track capture likewise;
  the literals `"item"`/`"track"` never appear as labels on any interactive capture path
  again. `[verify — DAW]`
- Realtime and batch captures follow the same scheme (batch retains per-unit ordinals);
  recapture preserves the entry's existing name; ingest and bake naming are unchanged.
- An unnamed source track produces the deterministic `Track N` fallback.
- The panel card shows the name, truncated to its cell, pure-geometry tested; the
  instrument's browse list shows the same name with no mechanism change.
- File-stem uniqueness is still solely `makeUniqueTag`'s; `capture_paths` gains no
  filesystem access; existing entries' labels are byte-identical after the track lands.
- **DAW-verification obligation:** Daniel captures from a named track, an unnamed track,
  and a multi-item selection; sees the labels on the card and in the instrument.

**Open questions.** `[verify]` the track-name read API against the SDK header.
`[propose]` the discriminator format (lean above). `[propose]` multi-track item
selections (items spanning several tracks in one capture — lean: first source track's
name plus a `+N` marker; alternatives: joined names, or the scope literal as fallback).
`[propose]` card-name fallback to tooltip only if the card proves too crowded.

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

**Goal.** A new capture whose channels are bit-identical is collapsed losslessly to one
channel — one channel of data on disk, a mono arrange item on insert, the instrument
loading in Mono mode. (Ψ.6)

**The invariant amendment is part of this track.** Root `CLAUDE.md:208` — "channel count
preserved (no silent stereo fold)" — is honored in spirit (the parenthetical forbids a
LOSSY fold; this collapse is lossless by predicate) but contradicted in text. The
amendment: channel count is preserved except that bit-identical channels may collapse
losslessly to mono; a lossy fold remains forbidden. Noted in the amendment: the old text
was already untrue in the other direction — a mono source renders at `RENDER_CHANNELS=2`
today (`capture_orchestrator.cpp:227`, hardcoded and never measured).

**Surface boundary — owns:** `core/capture/wav_codec` (the pure bit-identity predicate +
collapse plan, with `wav_codec` unit tests), `shell/capture/capture.cpp` (the post-render
collapse step between the exists-check `:390-395` and the `Sample` population `:398`,
plus `stampCaptureSample`'s channel echo `:207` — derive from the FILE, the
`bake_land.cpp:105,131,177` / `ingest.cpp:199,268` precedent),
`shell/capture/capture_realtime_finalize.cpp` (the collapse on the realtime path, and
the same echo fix — it parses the layout at `:188-198` and STILL echoes the request
value), the root-`CLAUDE.md:208` amendment, and — **minimally, because Phase Γ is live in
`shell/instrument/`** — `shell/instrument/processor_reload.cpp` only for the stale
"always 2 for extension captures" comment (`:64-77` vs `:150-157`) and any index/file
consistency fix that falls out; nothing else instrument-side. **Must not touch:** the
naming fields (T1's), `capture_paths`, the panel, the instrument's channel-mode logic
(`sample_map.cpp:58-62` already maps `channelCount == 1` → Mono, explicit user toggle
winning — no change needed).

**Behavior.**
- **The predicate is pure and bit-exact:** all channels bit-identical per frame (float
  bit patterns, never epsilon) → collapse to 1 channel. Generalized to N channels
  (all-identical → mono; no partial collapse, e.g. never 4→2) — N-channel captures are
  not reachable today (`channelCount` hardcoded 2), so the generalization is
  future-proofing the predicate, stated as such.
- **The collapse mirrors the one existing rewrite precedent** — `trimAutoTailInPlace`
  (`capture_realtime_finalize.cpp:55-125`): `parseWavLayout` → `extractFloatFrames` →
  `buildFloat32Wav(1, …)` → truncating rewrite, all with the existing `wav_codec` tools
  (`wav_codec.h:48-89` — `buildFloat32Wav` already takes arbitrary `nch`). There is no
  PCM assembly on the offline path today (REAPER writes the file; the extension only
  stats and hashes it) — this step is the first, and it lives shell-side at the named
  insertion point with its plan pure.
- **Scope — decided: every extension capture path** — offline, realtime, batch,
  recapture (batch and recapture route through the same backends). **Ingest is
  excluded** — imported files are the user's bytes, not our capture; rewriting them is a
  mutation this tool has no license for. **Unconditional, no user opt-out** — Ψ.6 asks
  for the behavior, not a preference; dual-mono stereo carries zero information the mono
  file lacks. **New captures only, never retroactive** — existing bank entries and files
  are untouched.
- **The file and the index value are written together — the stated hazard.** A collapsed
  file with `Sample::channelCount` left at 2 still plays (the instrument's
  `extractChannel` clamps out-of-range to the last channel, `sample_map.cpp:174-186`,
  yielding dual-mono) but the mono/stereo toggle and the waveform lane count would read
  Stereo — so the criterion is equality with the file's `fmt` on every path, not absence
  of crashes.
- **Insert needs no change, stated so nobody invents work:** `insert.cpp:129-152` passes
  only a path to `InsertMedia`; REAPER derives the item's channel count from the file —
  a 1-channel WAV yields a mono item for free.
- **Behavior change, stated and accepted:** `hashWavContent` covers the `fmt ` body +
  `data` payload, so a collapsed capture will NOT hash-dedup against a pre-existing
  stereo twin of the same audio (`bank_model.h:143-148`). Accepted — the predicate is
  deterministic, so repeats of the same request still dedup against each other.
- **Bit-identical repeats survive:** a deterministic predicate over deterministic bytes;
  identical requests still produce identical files (now identically-collapsed ones).

**Acceptance criteria.**
- A capture of dead-center mono content yields a 1-channel float32 WAV whose PCM is
  bit-identical to either source channel and whose frame count is unchanged — losslessness
  asserted by `extractFloatFrames` equality in the pure tests, observed in the DAW on the
  real path. `[verify — DAW]`
- A capture with ANY differing sample pair is byte-identical to today's 2-channel output
  — the not-collapsed path is unchanged, the same discipline as "bypassed means
  byte-identical."
- `Sample::channelCount` equals the produced file's `fmt` channel count on EVERY capture
  path — including realtime, whose finalize currently parses the layout and echoes the
  request anyway; that defect is fixed here.
- Inserting a collapsed capture yields a mono arrange item; the instrument loads it in
  Mono channel mode with a single waveform lane, explicit user toggle still winning.
  `[verify — DAW]`
- The null test holds for a collapsed capture re-inserted at its source position.
  `[verify — DAW: REAPER's mono-item-on-stereo-track summing at unity is the thing to
  confirm]`
- Bit-identical repeats hold across the collapse; realtime and batch behave identically
  to offline; ingest is demonstrably untouched (an imported dual-mono file stays stereo).
- The predicate + collapse plan are pure with `wav_codec` test coverage, including the
  N-channel all-identical case and the one-sample-differs case.
- Root `CLAUDE.md:208` lands amended, in this track; the `processor_reload.cpp` touch is
  the comment + consistency fix only.
- **DAW-verification obligation:** Daniel captures a dead-center source and a true-stereo
  source, inserts both, loads both into the instrument, and runs the null test on the
  collapsed one.

**Open questions.** `[propose]` whether bake landings collapse too (lean YES — the bake
"writes a file plus an index entry, like every other capture", `bake_land` already
derives channel count from the file, and a dead-center instrument render is exactly the
dual-mono case; it is `[propose]` rather than decided only because `bake_land` is Ξ-W2's
freshly-landed surface and the collapse there should be confirmed against what actually
shipped). `[verify]` REAPER's mono-item summing for the null test (above). Decided, not
open: unconditional; new-captures-only; ingest excluded; N-generalized predicate; no
partial collapse.

---

## 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) | `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-*`). **Ten tracks across four waves**, 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-*`). **Six tracks across two 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.
- **Γ-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.

### 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
      T1 capture-signal-popup .................... 15 (popup)

Phase Γ — The instrument's control surface      (none of the seventeen; ends with VST3 params)
  W1  Foundations                            [5 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 + budget constants + row predicate
      T5 preserve-time-stretch ...... real stretcher      [measure-and-report gate]
  W2  New controls, and the overlay's marks  [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    [2 tracks]
      T1 deck-reflow ................ two rows + double-height MASTER, by construction
      T2 bake-reset-amendment ....... the Xi correction Gamma owns  [needs Xi-W2-T1 on dev]
  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 — read kParamsPayloadVersion on dev and take the
  next three above it. On dev at 2026-08-01 that is v14 / v15 / v16-reserved.
  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 is CONDITIONAL: confirm it against
  what Xi-W2-T1 shipped, and serialize behind T1 if it does not hold.

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