docs: retire Phases Theta, Xi, Psi and Epsilon from the plan

All four are landed in full. Their narratives live in COMPLETED.md; the plan now carries only Gamma and Lambda. Cross-references and VERIFICATION.md citations repointed.
This commit is contained in:
2026-08-02 18:27:54 -04:00
parent 4231b2321c
commit 02319041e3
3 changed files with 63 additions and 872 deletions
+23 -4
View File
@@ -818,9 +818,28 @@ own refs table. Pre-existing — `bakeWindowNeedsHold` is only a new *consumer*
Seven tracks across three waves, code-complete, reviewed, remediated, and integrated on Seven tracks across three waves, code-complete, reviewed, remediated, and integrated on
this branch: 89/89 tests passing, a clean build. Phase Ψ came from a direct list of this branch: 89/89 tests passing, a clean build. Phase Ψ came from a direct list of
seven defects and refinements (Daniel, 2026-08-01) rather than a backing product doc seven defects and refinements (Daniel, 2026-08-01) rather than a backing product doc, so
see `docs/PLAN.md`'s Phase Ψ section for the Ψ.1–Ψ.7 provenance list this phase traces the list is carried here verbatim rather than by reference, now that `docs/PLAN.md`'s
back to. Phase Ψ section (its original home) is retired:
> **Ψ.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.
**Ψ-W1-T1 — `capture-range-exactness`.** A ranged item capture now renders the **Ψ-W1-T1 — `capture-range-exactness`.** A ranged item capture now renders the
requested window instead of the whole item, by re-sourcing through the selected-tracks requested window instead of the whole item, by re-sourcing through the selected-tracks
@@ -884,7 +903,7 @@ has been confirmed in a running REAPER. Several rest on a **shared unverified
inference** about how REAPER's selected-tracks render source interacts with custom inference** about how REAPER's selected-tracks render source interacts with custom
time bounds — and Ψ-W3's refusal now rests on it too, meaning if the inference is time bounds — and Ψ-W3's refusal now rests on it too, meaning if the inference is
wrong that refusal costs a working capture. Each track's DAW-verification obligation wrong that refusal costs a working capture. Each track's DAW-verification obligation
is recorded in `docs/PLAN.md`'s Phase Ψ section; `docs/verify-track-scope-multitrack.md` is recorded in `docs/VERIFICATION.md`; `docs/verify-track-scope-multitrack.md`
is a new standalone verification script on this branch, for Ψ-W3-T1's multi-track is a new standalone verification script on this branch, for Ψ-W3-T1's multi-track
refusal specifically. No human has observed any of these seven behaviors in a DAW. refusal specifically. No human has observed any of these seven behaviors in a DAW.
+14 -842
View File
@@ -242,345 +242,6 @@ touches them. The instrument adds a sixth surface that binds every Θ track:
--- ---
## 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.110 exponent, an inner dial paired with an overlay
knot), the release-right-anchored AHDSR layout against the pitch envelope's 1:1 AHD,
and the Trigger amp/filter fade pair folded into a Trigger AHD, consolidating what
were two staged-shape mechanisms into one. The Trigger × Preserve end-of-sample click
is fixed; the landed fix is wider than scoped, also ringing out Gate × Preserve ×
source-exhaustion, previously a hard cut. The ordering was deliberately serial — T1's
live-delivery mechanism landed first so T2's new curve exponents and Trigger AHD
fields were authored directly into it rather than backfilled afterward.
---
### Θ-W4 — Loop sustain and the velocity deck
**Depends on W3 for:** the envelope system both tracks compose with — T1's loop-sustain
is the Gate-mode sustain the AHDSR releases out of, and T2's bipolar pitch/filter curves
modulate targets whose envelopes W3 just reshaped. T2 additionally depends on W2-T1 for
the filter's existence and on W2-T3 for the preview button's toolbar position.
**Both tracks have landed** — Θ-W4-T1 (`gate-loop-sustain`) and Θ-W4-T2
(`velocity-deck-and-bipolar-curves`) — see `docs/COMPLETED.md` for the full narrative of
each. Between them: loop points are now a usable feature, with a Gate-mode loop acting
as the sustain and a parameterized crossfade at the seam; and the three velocity-curve
popups (amp, pitch, filter) now live together in a new VELOCITY deck group, with the
pitch and filter curves bipolar and flat-by-default so their modulation is off until
drawn, while the amp curve stays unipolar and unchanged, and the preview button's text
is replaced by a drawn play-triangle glyph. Params payload reached v11 with T1's loop
block and v12 with T2's velocity→pitch curve appended after it.
---
### Θ-W5 — Spline EGs
**Depends on W4 for:** the bipolar velocity-curve domain (W4-T2) — the spline algorithm
is **singly implemented and multi-referenced**, so its enhancement must land against the
final consumer set, and the last consumer to change domain is the pitch/filter velocity
curve; and for the Gate-mode loop (W4-T1), since "Gate is unavailable in Spline mode" is
only a real, testable rule once Gate has something to be unavailable *for*. It also
depends on W3's radio switch, which is how a spline contour reaches the overlay at all.
**Θ-W5-T1 has landed** — `spline-egs` — see `docs/COMPLETED.md` for the full narrative.
It shipped a free-drawn alternative to every staged envelope: pitch, filter, and amp
EGs can switch Staged → Spline and have their contour drawn directly on the waveform
overlay. The one shared monotone-spline implementation gained hard points — sharp
corners, no smoothing on either adjacent segment — flowing to every consumer including
the existing velocity→amp transfer curve, no fork. Both Staged and Spline state persist
simultaneously (saved-but-inactive, lossless round-trip); params payload reached v13,
and v12 projects still load. Gate is unavailable while a Spline EG is active; the
contour is a pure time function over the full sample length, normalized and drawn 1:1
with the sample's time axis. Point grammar converged on left-click add / right-click
delete / control-click hard-smooth toggle, one grammar across both spline consumers.
Staged segment knobs and their inner curve dials render disabled and reject edits while
Spline is active. Point-count ceiling: 128, a musical bound rather than a performance
one. A follow-on change in the same track reworked deck cell width: `-1` in `cellIds`
now means one cell's width, reserved and redistributed, rather than a blank cell holding
geometry — a Trigger face that drops Sustain and Release gets wider cells instead of
dead slots; group widths, row packing, deck height, and Gate-mode cell widths are
unchanged.
---
### Θ-W6 — Editor legibility pass
**Depends on W5 for:** the last change to a drawn surface. Item 13 is an audit whose
output is a disposition list over "every class of drawn surface," and item 10's sizing
pass is judged by eye over the finished layout — running either while the spline contour,
the disabled-knob state, or the deck inventory was still moving would have meant
auditing and then re-auditing. Θ-W5-T1 has landed (see `docs/COMPLETED.md`), so that
surface has stopped moving and this dependency is satisfied. The source doc names this
sequencing as an observation (item 13 after the layout/knob work); this plan adopts it
as the boundary.
**One track.** Both items repaint essentially every surface in the editor; concurrent
tracks would collide everywhere.
**Θ-W6-T1 has landed** — `legibility-and-antialiasing` — see `docs/COMPLETED.md` for the
full narrative. It shipped both halves together: knobs grew 28→40 px (inner curve dial
14→20), the deck cell 48×58→60×74, and the label band 12→16 px, now drawn in
`Font::Label` rather than `Font::Micro` — group captions and toggle segments deliberately
stayed `Font::Micro`, since bumping them would outgrow row 1's headroom at the floor
width. The editor's default/minimum size grew 840×620→980×680 to fit the wider deck at
floor width; an existing saved instance's window grows on open, and the floor is
validated by a derived test rather than literals. All fourteen time-constant labels now
read in ms (display-only; internal representation untouched) — `holdFraction` knobs,
`Len %`, and the bank panel's duration readout stayed out of scope. Double-click resets
each ring independently — outer ring resets the value, inner dial resets the exponent to
1.0 — reaching the chrome's preview-velocity knob too via a shared `inKnobFace` rule.
The antialiasing audit fixed knob arcs and needle, the inner dial arc and needle, staged
and spline envelope slopes, the spline contour, the velocity-popup trace, the waveform
outline, and the preview triangle; node handles, curve knots, knob discs, buttons, piano
keys, loop markers, borders, gradients, and text were already clean. The disposition
table is a standing artifact in `docs/product/visual-design-language.md` §8. The
piano-key open question is answered: not aliasing — every key is an axis-aligned
`LICE_FillRect`, so the earlier width defect was integer-division residue in the tiling,
not a sloped edge. High-DPI host scaling itself is unverified (deferred, see
`docs/TODO.md`).
All visual outcomes remain pending Daniel's by-eye sign-off on `dev`.
---
### Θ-W7 — Arc-and-spline antialiasing fix
**Depends on W6 for:** a drawn surface to find a defect on — this wave did not exist in
the plan; it was opened after Daniel found two rendering defects by eye once Θ-W6-T1
shipped, so the audit's own output is what surfaced them.
**One track.**
**Θ-W7-T1 has landed** — `arc-and-spline-aa` — see `docs/COMPLETED.md` for the full
narrative. The stacked-`LICE_Arc` knob and dial rings never reached an opaque core (peak
alpha measured 138/255), and the staged/spline envelope and velocity-curve traces were
fully aliased rather than gapped, from integer `cy` quantizing the slope. Both defects,
plus the two needles, now route through one pure analytic thick-stroke rasterizer —
coverage in a new `core/ui/stroke_aa`, the single LICE blend in a new
`shell/instrument/editor_stroke` — replacing `LICE_Arc` and `LICE_ThickFLine` outright.
Measured: arc peak alpha 138/255 → 255/255, arc perpendicular-weight ripple 67% → 5%,
spline weight ripple 29% → 3%, at a cost of +0.09 ms per full editor repaint. Daniel then
ruled every sub-2 px stroker width up to 2 px, since the stroker only guarantees an
opaque core at width ≥ 2 px; the knob track arc, the inner-dial needle, and the deck's
mini velocity trace each moved 1.0 → 2.0 px. `docs/product/visual-design-language.md` §8
is corrected — a false "keeps every ring antialiased" claim is deleted, the rows are
re-dispositioned with measurements, and the audit's methodological lesson (verifying
which primitive was called is not verifying what it rasterized) is recorded as a standing
blockquote. All visual outcomes remain pending Daniel's by-eye sign-off on `dev`.
---
## Phase Ξ — The resample loop
**Ships:** one consolidated, fully robust provenance/usage tracking system, and on top of
it the one-click in-sampler resample — dial → bake → dial again, with the bank as the
medium each iteration passes through.
**Consolidates items 15, 17.**
**Why these two and not more.** Item 17 is a **prerequisite of meaning** for item 15's
bank-side half: the replace-vs-add rule is "does provenance-tied usage exist," which
denotes nothing until the consolidated lineage records exist. Item 17 is otherwise
independent of the editor chain — which is what makes Ξ-W1 concurrency-safe with Θ.
**Ξ-W2 onward requires Phase Θ complete.** Item 15 presupposes the processing surface it
bakes — "filtering, pitching, amp all set up nice" is the instrument items 1, 2, 3, and 14
build — and its settled reset scope enumerates the filter parameters, the spline contours,
and the loop points by name. Baking a processing chain that does not exist yet is not a
schedule preference; the feature is not expressible. Phase Θ landed as of Θ-W7-T1 (see
`docs/COMPLETED.md`); this gate is satisfied.
**All three waves have landed — Phase Ξ is complete.** W1 through W3 each carry their own
landed note below; see `docs/COMPLETED.md` for every track's full narrative.
---
### Ξ-W1 — Consolidated tracking, and the programmed-note model
#### Ξ-W1-T1 — `tracking-consolidation`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The provenance/usage
territory is now one system: a new `src/core/tracking/` directory holds `origin_ledger`
(the record family — `OriginRecord`/`OriginKind`, the insertion-ordered `OriginLedger`,
its JSON codec, and the `Fresh`/`Loaded`/`Unreadable`/`FutureVersion` load
classification) and `tracking_authority` (the one decision surface: `pruneProtection`
and `tiedUsageExists`), retiring `core/model/owned_manifest`. Both prune's protected set
and the resample's replace-vs-add decision are computed from one borrowed
`TrackingState`, so the two safety-critical consumers cannot drift apart.
`sample_usage` deliberately **stays in `core/wire`** — the consolidation is of the
decisions, not the codecs. The deferred persisted-instance-identity fix was **not**
folded in — the deferral is restated in `docs/TODO.md`, its one home. This track
landed `OriginRecord`'s birth-time `parentSampleId` chain as the lineage mechanism, but
**Ξ-W2-T1's "naming and lineage" open question (jointly held with this track) is
unaffected and still theirs to close** — display naming and user-readable iteration
lineage were not decided here.
#### Ξ-W1-T2 — `note-program-model`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The programmed-capture-
signal model is a new pure module directory, `src/core/instrument/note/` — a fourth
peer of `engine/`/`map/`/`ui/` under `core/instrument/` — holding `musical_division`,
`tempo`, and `note_program` (`Velocity`, the denominated `OffsetAmount`, the anchored
`StartOffset`/`EndOffset`, `NoteProgram`, `resolveNote`). **Both open questions below
are answered, for Ξ-W3-T1:** negative offsets are legal in both directions (sign
uniform, positive is later in time; only an inverted window is refused, reported via
`ResolvedNote::windowCollapsed`), and the denomination seam is confirmed as of this
landing — note length stays musical-division-only, and an offset stores the denomination
it was entered in. **Superseded by Ξ-W3-T1's landed work** (see `docs/COMPLETED.md`):
`note/CLAUDE.md`'s musical-division-only rule is amended — a note length now carries
EITHER an exact derived duration or a musical division, not division-only. The offset
denomination rule is unaffected.
---
### Ξ-W2 — The bake chain
**Depends on Ξ-W1 for:** the consolidated lineage records that make replace-vs-add
computable (T1) and the programmed-note record the offline pass renders (T2). **Also
depends on all of Phase Θ** — see the phase note above.
**This wave ran AHEAD of Phase Γ, and that was not a choice.** The plan asserted Γ must land
first so the bake's reset list would be complete on the day it shipped; Ξ-W2-T1 was already
live. The consequence is owned, not absorbed: **Γ-W3-T2 `bake-reset-amendment`** completes
the list afterwards, and **Γ-W4-T1** adds the host-notification obligation once parameters
exist. Neither is this wave's work, and neither is a defect report against it.
**One track.** The bake is one gesture and one chain; the architecture decision at its
head governs every step after it.
#### Ξ-W2-T1 — `resample-bake-chain`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The architecture decision
(this track's first deliverable) is ratified: **Decision 1 = (1b)**, the editor invokes
the extension's bake action directly over the VST-host bridge
(`NamedCommandLookup`/`Main_OnCommandEx`), no poller/nonce, dissolving the S13 DEGRADED
verdict; **Decision 2 = (2c)**, the instrument renders in-process and the extension
banks the file, taken over the plan's leaning toward (2a) on an engine-version-skew
argument. **Extension presence** resolved to cleanly-unavailable, not silently lossy —
the affordance refuses up front when the extension is not loaded. **Reset-scope edge
cases** classified against the ratified rule: play mode → RESET to Trigger, start point
→ RESET, channel mode and preview velocity → SURVIVE. **Phase Γ's additions remain
NOT this track's** — Γ-W3-T2 `bake-reset-amendment` still amends the reset list for
Γ's own new values, unaffected by this landing. **Naming and lineage** is proposed
(`Kick``Kick r2``Kick r3`) but not itself ratified — still open jointly with
Ξ-W1-T1's lineage-record question, which Ξ-W1-T1 answered on its own side
(`OriginRecord::parentSampleId`). Nothing was verified in a live REAPER session;
Daniel's manual verification is still owed.
---
### Ξ-W3 — The capture-signal popup
**Depends on Ξ-W2 for:** the bake chain this track was originally scoped to build a
preview against, under the acceptance criterion "preview and bake cannot diverge." **That
motivation is retired, not satisfied** — see below.
**One track.**
**Ξ-W3-T1 has landed** — `capture-signal-popup` — see `docs/COMPLETED.md` for the full
narrative, and it diverges substantially and deliberately from this section's spec. The
popup this track was scoped to build (a musical-division note-length picker, ms/beat-
editable offsets, velocity, and a preview trigger) was built (~1500 lines) and then
**abandoned unmerged** on Daniel's ruling: *"I didn't realize you had already derived a
usable window. The manual stuff for baking a specific midi length was just an idea, if we
have a smarter, fewer-clicks way of doing it, that is ideal. I just don't want to lose
anything when we bake. We can abandon the whole parameterized bake window if we can
safely derive the window in gate and trigger modes."* An audit, backed by executable
tests, established the window derives losslessly everywhere except Gate over an active
sustain loop, which has no intrinsic duration to derive. What shipped instead: the bake
window derives itself in Trigger and loop-less Gate; one control, a musical-division
**Hold** picker, covers the one irreducible case and is shown only for Gate-with-active-
loop (`bakeWindowNeedsHold`, reading the engine's own loop fold); bake velocity now reads
the instance's persisted preview velocity rather than a hard-coded value; and the
chrome-row play button stays a pure MIDI trigger — Daniel's ruling: *"play button is pure
MIDI trigger, Bake parameters are their own thing."* **This retires this section's
acceptance criterion 2 ("preview and bake cannot diverge") and its preview-trigger
behavior bullet by ruling, not by shortfall** — there is no popup and no preview trigger.
Three pre-existing truncation bugs were found and fixed along the way, and
`note/CLAUDE.md`'s musical-division-only invariant is amended (see Ξ-W1-T2 above): a note
length now carries either an exact derived duration or a musical division, not
division-only.
---
## Phase Γ — The instrument's control surface ## Phase Γ — The instrument's control surface
**Ships:** the deck reflowed into two categorical rows with a double-height MASTER bus deck, **Ships:** the deck reflowed into two categorical rows with a double-height MASTER bus deck,
@@ -1820,7 +1481,8 @@ this plan asserted.
feature (see "Flagged for awareness" item 2). feature (see "Flagged for awareness" item 2).
**Spec:** `docs/product/instrument-control-surface.md` §3.4, and Ξ-W2-T1's own "Reset scope" **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.** block, now in `docs/COMPLETED.md` (Phase Ξ has landed and its `PLAN.md` section is retired) —
**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 **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 its tests. **Does not own** the bake chain, the crossing architecture, the replace-vs-add
@@ -2088,503 +1750,6 @@ value semantics, any deck geometry, or the bake's reset *membership* (W3-T2's).
--- ---
## Phase Ψ — The extension trust pass: exact bounds, disjoint solo surfaces, reachable actions, honest drops, real names, true mono
**Ships:** capture ranges that mean what was asked — a time selection or razor area over
a longer item captures the selection, both scopes; per-mode solo surfaces that cache,
clear, and restore across the Design/Arrange switch, with the switch itself refused while
the transport runs; the Media-Explorer import action registered into the Media Explorer
action section so it can live on that toolbar; a drag-out gesture law that resolves its
target from what is actually under the cursor, continuously and reversibly, with no
silent no-op release anywhere; captures labeled after their source track instead of the
literal `"item"`/`"track"`, surfaced on the panel card; and lossless mono collapse for
new captures whose channels are bit-identical.
**Consolidates: none of the seventeen.** Phase Ψ came from a direct list of seven defects
and refinements (Daniel, 2026-08-01). There is no backing product doc — the design
content lives inline in the tracks below, and the seven are recorded here as the phase's
provenance, cited throughout as Ψ.1–Ψ.7:
> **Ψ.1** — item and track captures should be named (labeled) after their source track
> name, plus a discriminator (date, etc.); currently they aren't named anything useful.
> **Ψ.2** — Design vs. Arrange modes: any SOLO state in one mode is cached and removed
> when switching to another mode, disjoining the solo surfaces.
> **Ψ.3** — the Design/Arrange active-mode toggle is gated if playback is running; only
> allow the switch when the project is not playing.
> **Ψ.4** — the action that moves a Media Explorer item to a new ReaSampler on the
> selected track is not in the Media Explorer action category, so it cannot be added to
> the Media Explorer toolbar; fix this.
> **Ψ.5** — drag-and-drop targets are inexact: sometimes dropping into the arrange
> doesn't work, sometimes dropping into the FX area doesn't work; dragging between banks
> is fine.
> **Ψ.6** — capture into mono: if the left and right channels of a new capture are
> bit-identical, collapse to mono — one channel of data, mono arrange items, ReaSamplers
> load in mono mode.
> **Ψ.7** — capture item / capture track with a small time selection on a large item
> captures the entire item, not the selection/razor; make capture regions consistent and
> correct.
**Where the seven land:**
| Ψ-item | Track | Worktree slug |
|---|---|---|
| Ψ.7 | Ψ-W1-T1 | `ppsi-w1-t1-capture-range-exactness` |
| Ψ.2 + Ψ.3 | Ψ-W1-T2 | `ppsi-w1-t2-mode-switch-discipline` |
| Ψ.4 | Ψ-W1-T3 | `ppsi-w1-t3-media-explorer-section` |
| Ψ.5 | Ψ-W1-T4 | `ppsi-w1-t4-drop-target-resolution` |
| Ψ.1 | Ψ-W2-T1 | `ppsi-w2-t1-capture-naming` |
| Ψ.6 | Ψ-W2-T2 | `ppsi-w2-t2-mono-collapse` |
**Ψ-W3 is not one of the seven** — opened mid-phase, after Ψ-W2's review surfaced that
the track scope carried the same multi-track stem-collapse hole Ψ-W1-T1 had just closed
for item scope. It consolidates none of the original seven and carries no `Ψ-item` row
above; see Ψ-W3 below.
**All three waves have landed — Phase Ψ is complete.** W1 and W2 each carry their own
landed notes below; W3 carries its own too. See `docs/COMPLETED.md` for every track's
full narrative. **None of the seven tracks is DAW-verified** — all are code-complete and
unit-tested, several resting on a shared unverified inference about how REAPER's
selected-tracks render source interacts with custom time bounds, which Ψ-W3-T1's
refusal now also rests on; each track's DAW-verification obligation is restated inline
below.
Ψ.2 and Ψ.3 share one track deliberately: they share one chokepoint — `applyMode`, the
sole mode mutator (`shell/view/view.cpp:380-469`) — and one discriminator
(`targetModeId != model.activeModeId()`, the test that distinguishes a real switch from a
reapply). Splitting them is two tracks fighting over the same function.
**Concurrency with Γ and Ξ.** Phase Ψ is extension-side. Γ and Ξ-W3 live in
`core/instrument/` + `shell/instrument/`; the surfaces are disjoint with ONE named
exception — Ψ-W2-T2 touches `shell/instrument/processor_reload.cpp` for a stale comment
and an index/file consistency check, and that touch is bounded to the minimum in its
surface boundary precisely because Γ is live in that directory.
**Three invariant amendments were DELIVERABLES of this phase, not asides.** Each landed
in its owning track, as an acceptance criterion of that track:
1. **The never-touch-solo rule** (Ψ-W1-T2): `src/shell/view/CLAUDE.md:14-17`,
`src/core/view/CLAUDE.md:10`, and `docs/product/design-view.md:160-165` + `:588-592`.
Ψ.2 requires writing `I_SOLO`; the amended form is stated in the track. A track that
lands solo writes without the amendment reads as an invariant breach in review.
2. **The action-registration contract** (Ψ-W1-T3): root `CLAUDE.md`
§"REAPER extension contract" documents only the main-section 4-step pattern; the
second, non-main mechanism (`custom_action` + `hookcommand2` + `-custom_action`
mirror) must be added beside it.
3. **The channel-count-preserved rule** (Ψ-W2-T2): root `CLAUDE.md:208` — "channel count
preserved (no silent stereo fold)" — is contradicted in text (not in spirit) by a
lossless bit-identical collapse, and was already untrue in the other direction (a mono
source renders at `RENDER_CHANNELS=2` today). The amended form is stated in the track.
**Performance posture.** Every Ψ surface is cold — per-capture, per-click,
per-mode-switch, per-mouse-move. None of the named hot paths (peaks envelope compute,
audition, realtime-capture tick, instrument `process()`) is touched. Two disciplines
carry anyway: the realtime tick's single-pointer-test idle fast path is untouched by
Ψ-W1-T1's render work, and Ψ-W1-T4 keeps the inside-client drag path free of SDK
hit-tests, exactly as today (`panel_drag.cpp:273-274` — "costs nothing on the common
internal-drag path").
---
### Ψ-W1 — Exact bounds, disciplined switches, reachable actions, resolved drops
**Depends on:** nothing in this phase.
#### Ψ-W1-T1 — `capture-range-exactness`
**Landed** — see `docs/COMPLETED.md` for the full narrative. A ranged item capture now
renders exactly the requested window instead of the whole item, by re-sourcing through
the selected-tracks render when the item extent does not already print the window.
**Deviation:** the spec named two candidate architectures (re-source vs. render-then-
trim); the engineer shipped a conditional form of the re-source candidate — the
full-extent case runs literally unchanged code, keeping the byte-identity regression
floor structural and the fix cheap to revert if the override inference proves wrong.
Also landed: a transient isolation guard (cutting `B_MAINSEND` on direct folder
children, muting receives) so an item capture stays true to item scope, and a
post-render frame-count gate (±1 tolerance, tail-None only) that refuses a widened
render and retains it outside the bank for diagnosis rather than deleting it. New modules `core/capture/render_window`, `core/capture/track_topology`,
`shell/capture/render_selection`, `shell/capture/render_isolation`. The whole fix rests
on the unverified inference that REAPER's selected-tracks render source overrides
custom time bounds — Ψ-W3-T1 (below) now also depends on it. **DAW-verification
obligation, unmet:** the four-cell scope × selection-type matrix over a source item
substantially longer than the selection (tail None), plus one razor-union case — none of
it run in a live REAPER session yet.
#### Ψ-W1-T2 — `mode-switch-discipline`
**Landed** — see `docs/COMPLETED.md` for the full narrative. Per-mode SOLO surfaces now
cache, clear, and restore across a Design/Arrange switch, with the switch itself
refused, visibly, while the transport is playing or recording. The never-touch-solo
invariant — stated three times (`src/shell/view/CLAUDE.md`, `src/core/view/CLAUDE.md`,
`docs/product/design-view.md`) — is amended, in this track, to the snapshot sense of
non-destructive: solo is cached per mode on a real switch and restored verbatim, not
left untouched absolutely the way `B_MUTE` and the master track are. Also closed: a
pre-existing bug where a footer mode-segment click never persisted view state. New
`core/view/solo_cache`, `shell/view/view_solo`. **DAW-verification obligation, unmet:**
the disjoint-surface solo matrix, the playback-gated refusal (playing and recording),
and the panel-persist case — none run in a live REAPER session yet.
#### Ψ-W1-T3 — `media-explorer-section`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The Media Explorer import
action now registers into REAPER's Media Explorer action section (32063) via
`custom_action` + `hookcommand2`, alongside its existing Main-section entry so existing
keybindings survive — a second FOREVER-STABLE id, `INGEST_IMPORT_MEDIA_EXPLORER_MX`,
minted per channel. Root `CLAUDE.md`'s "REAPER extension contract" is amended, in this
track, with the second, non-main registration mechanism beside the original four-step
main-section pattern. **DAW-verification obligation, unmet:** adding the action to the
Media Explorer toolbar and firing it from there; confirming the Main-section binding
still fires; and confirming unload/reload does not leak a duplicate Media Explorer entry
(the `-custom_action` unload mirror is unconfirmed against the SDK header) — none run in
a live REAPER session yet.
#### Ψ-W1-T4 — `drop-target-resolution`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The drag-out gesture is now
a per-move, stateless law: target class resolves from what is under the cursor on every
move, every class transition is reversible until release or until the pointer leaves
REAPER, and the OS hand-off is reserved for leaving REAPER entirely — the whole TCP/MCP
is now an instrument-drop hotspot, and a single-card arrange drop lands a timeline item
at the pointer's track and time. New `shell/actions/arrange_drop_win`.
**DAW-verification obligation, unmet:** the full target-class matrix (single- and
multi-card), reversibility across a drag that crosses the arrange en route to an FX
window, drag-speed independence, and the narrow-TCP case — none run in a live REAPER
session yet.
---
### Ψ-W2 — Names and channels, over the settled render block
**Depends on Ψ-W1 — specifically Ψ-W1-T1**, which rewrites the capture
render-configuration block in `shell/capture/capture.cpp` and
`core/capture/render_settings.cpp`; both W2 tracks edit adjacent regions of those same
TUs, so W2 dispatches only after W1-T1 is on `dev`. (T2T4 of W1 gate nothing here; the
wave boundary is the file collision, not a semantic dependency.)
#### Ψ-W2-T1 — `capture-naming`
**Landed** — see `docs/COMPLETED.md` for the full narrative. Captures are now named
after their source track's name plus a discriminator
(`<Track> [+N] [#ordinal] MM-DD HHMM`) at every interactive mint site, with the name
shown on the panel card over a scrim clearing the 4.5:1 contrast floor. New
`core/capture/capture_name`. Recapture, ingest, and the bake deliberately keep their own
naming — the bake's naming-and-lineage open question stays Ξ-W1-T1's/Ξ-W2-T1's to close,
untouched here. **DAW-verification obligation, unmet:** capturing from a named track, an
unnamed track, and a multi-item selection, and confirming the labels show on the card
and in the instrument — none run in a live REAPER session yet.
#### Ψ-W2-T2 — `mono-collapse`
**Landed** — see `docs/COMPLETED.md` for the full narrative. A capture whose channels
are bit-identical now collapses losslessly to one mono channel, written via temp file
plus atomic rename, with `Sample::channelCount` measured off the landed file rather than
echoed from the request on every capture path — including realtime, which previously
parsed the layout and echoed the request anyway. Root `CLAUDE.md:208`'s
channel-count-preserved invariant is amended, in this track, to the landed wording:
channel count preserved, except that bit-identical channels may collapse losslessly to
mono; a lossy fold remains forbidden. The `shell/instrument/processor_reload.cpp` touch
stayed to the minimum named in its surface boundary (Phase Γ was live in that
directory). **Open, not closed here:** whether bake landings collapse too — `[propose]`,
leaning yes, still deferred to be confirmed against what Ξ-W2-T1 actually shipped.
**DAW-verification obligation, unmet:** capturing a dead-center mono source and a
true-stereo source, inserting both, loading both into the instrument, and running the
null test on the collapsed one (REAPER's mono-item-on-stereo-track summing at unity is
the specific thing to confirm) — none run in a live REAPER session yet.
---
### Ψ-W3 — Closing the track-scope stem-collapse hole
**Depends on Ψ-W2 for:** existing at all — this wave did not exist when the phase was
scoped. Daniel opened it after Ψ-W2's review surfaced that the track scope carried the
same multi-track stem-collapse hole Ψ-W1-T1 had just closed for item scope.
**One track. Consolidates none of the seven** — it came from a review finding, not from
Ψ.1–Ψ.7.
#### Ψ-W3-T1 — `track-scope-range`
**Landed** — see `docs/COMPLETED.md` for the full narrative. Any multi-track
selected-tracks render now refuses, in both scopes, keyed on the render *source* rather
than the capture scope — closing the hole Ψ-W1-T1 left open for track scope. Realtime
deliberately diverges and was left untouched, because it sums correctly. The refusal
rests on the same unverified inference Ψ-W1-T1 rests on — that REAPER's selected-tracks
render source overrides custom time bounds — so if that inference is wrong, this refusal
costs a working capture. `docs/verify-track-scope-multitrack.md` is a new standalone
verification script on this branch, for this track's multi-track refusal specifically.
**DAW-verification obligation, unmet:** confirmed nowhere in a live REAPER session yet.
---
## Phase Ε — The bank package: one file that carries a bank between projects
**Ships:** a bank exported to a single version-tagged `.rsbank` file — audio bytes
byte-exact, index metadata intact — and imported into another project's bank folder and
index, with a compatibility policy that names both directions concretely: an older package
in a newer build always imports, a newer package in an older build refuses whole with an
actionable message, and neither direction is ever a partial landing.
**Consolidates: none of the seventeen.** Phase Ε came from a direct request (Daniel,
2026-08-02) and is scoped in `docs/product/bank-package.md`. Nothing in `docs/TODO.md` or
`docs/TODO-1.0.md` records export, import, or a package format — a sweep of `docs/` for
`export|package|portable` returns only unrelated matches (prune's portable move-to-trash,
MIDI-playback prose) — so this phase **supersedes nothing and absorbs nothing.**
**Three forks were opened at framing and all three are RULED** (Daniel, 2026-08-02). The
rationale for each — including why two went against the framing recommendation — is
recorded at `docs/product/bank-package.md` §"Rulings"; the binding specification for each
is in the section of that doc named below.
| Fork | Ruling | Specified in | Bound into |
|---|---|---|---|
| **Ε-F1** | **Proprietary container** — the hand-rolled `RSBK`. ZIP via the vendored MiniZip64 (`vendor/WDL/WDL/zlib/`) and a hand-written stored-only ZIP shape are both rejected and are not to be revisited in this phase | §"The container" | Ε-W1-T1 |
| **Ε-F2** | **Import always lands as a new bank**, with an automatic numeric suffix on a display-name collision — no prompt, no overwrite. Merge-into-existing is **out of scope for Phase Ε**, not deferred | §"Identity and collision on import", incl. the auto-suffix rule | Ε-W2-T2 |
| **Ε-F3** | **Refuse** the import when the tracking ledger is degraded. No confirm-and-proceed path, no opt-out | §"Import under a degraded tracking ledger" | Ε-W2-T2 |
**No Ε track is gated on a decision.** Every track in this phase is dispatchable as
written.
### Phase-Ε acceptance criteria
These bind every track in this phase, in addition to the plan-wide set above.
- **Byte-exact round-trip is the phase's trust anchor.** Export → import → export yields
byte-identical payloads, and the `hashBytes` digest of every landed file equals the digest
recorded at export. Frame count, sample rate, bit depth, and channel count are untouched on
both sides. **No re-encode anywhere:** `wav_codec` may be called to hash and to read
metadata already recorded, never to rebuild, trim, normalize, or collapse. The mono collapse
is a capture-path behaviour and must not reach the import path — the same exclusion ingest
already carries (root `CLAUDE.md`, exact-bounds invariant).
- **The format cannot express a path.** Manifest entries are bare file names — no directory
component, no `..`, no drive letter, no leading separator — validated on encode *and*
decode. Relative-paths-only becomes structural rather than remembered, and the
archive-traversal bug class closes by construction.
- **Import places no timeline item.** Capture and placement stay separate acts; import is a
capture-shaped act, not a placement one.
- **Import never overwrites and never deletes an existing bank-folder file.** The one
deletion path is the rollback of files *this call wrote* that no index ever referenced —
the documented carve-out at `src/shell/persist/prune_fs.cpp:5-11`, which every track
touching it must **cite, not restate**.
- **Export is read-only against the project.** No ext-state write, no `bumpBankGeneration()`,
no undo point. Import does the opposite: it bumps the generation
(`src/shell/persist/session.h:121`) so live ReaSampler 9000 instances reload, and batches
its index mutation into one Ctrl-Z through `persistBankOp`.
- **All-or-nothing on both sides.** No partial export, no partial import. A truncated
`.rsbank` must never exist on disk (temp file + atomic rename, the Ψ-W2-T2 precedent); a
half-imported bank must never exist in the index (rollback).
- **At most one entry's payload in memory at a time**, on both paths. The pure codec owns
framing and offset arithmetic; the shell owns the stream. A whole-package
`vector<uint8_t>` on either side is a rejected shape, not an optimization opportunity.
- **The pure planners take value inputs, never a handle.** The decoded manifest, the
destination `BankBook`, and the set of names present in the bank folder cross the seam as
values. No `ReaSamplerSession&`, no service container, no "pass the thing that has
everything" reaches `core/package/`. If a circular dependency appears during the build, the
fix is a service split or a thin interface — **not** parameter propagation, and **not** a
base class gaining a dependency that grows its subclasses' constructors.
- **Every pure module gets a `<module>_tests` target** that runs without REAPER or a DAW.
The whole collision/version rule set is expressible as pure functions over strings and
hashes; if a rule can only be tested through the shell, the seam is in the wrong place.
**Performance posture.** Every surface in this phase is cold — per-gesture, once. None of
the named hot paths (peaks envelope compute, audition, realtime-capture tick, instrument
`process()`) is touched by any track here. The one performance fact that *is* load-bearing is
the memory criterion above, and it is stated as a structural constraint rather than a
guardrail because exceeding it does not slow the feature down, it makes it fail.
**Concurrency with Γ and Λ.** Phase Ε is extension-side and lands almost entirely in **two
new directories** (`src/core/package/`, `src/shell/package/`) that no other phase touches.
Γ lives in `core/instrument/` + `shell/instrument/`; Λ is being specced concurrently and is
not read here. The only pre-existing files any Ε track edits are named per track below —
`core/tracking/origin_ledger` (W1-T3, exclusively), the root `CMakeLists.txt`
`add_subdirectory` list (W1-T1 and W1-T2, one line each), `src/app/main.cpp` and the panel's
bank menu (W2-T1 and W2-T2, one registration line and one menu row each),
`core/model/bank_book.{h,cpp}` (W2-T2 only — **one additive public `const` member**, required
by the Ε-F2 auto-suffix rule so the name fold keeps its single home), and
`shell/persist/session.h` (W2-T2 only — one additive public accessor, `ledgerStatus()`, so the
import gate can key on `LedgerStatus` alone without going through `pruneDryRun()`'s
enumeration+scan). **No Ε track touches `core/instrument/`, `shell/instrument/`, or any
capture backend.**
**All three waves have landed — Phase Ε is complete.** W1 through W3 each carry their own
landed note below; see `docs/COMPLETED.md` for every track's full narrative.
---
### Ε-W1 — The contract, the filesystem, and the ledger's new kind
**All three tracks have landed** — Ε-W1-T1 (`package-format`), Ε-W1-T2
(`package-fs-shell`), and Ε-W1-T3 (`import-origin-kind`) — see `docs/COMPLETED.md` for
the full narrative of each.
**Depends on:** nothing in this phase. **Three tracks, disjoint by directory** — the split is
by *what each track's inputs are*, which is why they genuinely parallelize: T1 knows only
bytes and structs, T2 knows only paths and bytes, T3 knows only the ledger.
| Track | Owns |
|---|---|
| **T1** `package-format` | the whole of the new `src/core/package/` **except** `export_plan` / `import_plan` (W2's), plus its `CMakeLists.txt` and `CLAUDE.md` |
| **T2** `package-fs-shell` | the whole of the new `src/shell/package/` **except** `export_bank` / `import_bank` (W2's), plus its `CMakeLists.txt` and `CLAUDE.md` |
| **T3** `import-origin-kind` | `src/core/tracking/origin_ledger.{h,cpp}` and its tests, exclusively |
**One shared file in the wave, named rather than discovered at merge:** the root
`CMakeLists.txt` `add_subdirectory` list — T1 appends `src/core/package`, T2 appends
`src/shell/package`. Two append-only lines in one list: **textual merge adjacency, not
semantic contention.** Whichever lands second rebases.
**All three tracks are dispatchable now.** T1 carries the Ε-F1 ruling — the container is the
hand-rolled `RSBK`, decided, not a candidate T1 chooses among. T2 and T3 never depended on
that ruling anyway: T2's API is bytes-in/bytes-out regardless of what those bytes mean, and
T3 touches no package code at all.
#### Ε-W1-T1 — `package-format`
**Landed** — see `docs/COMPLETED.md` for the full narrative. The pure `src/core/package/`
codec for the hand-rolled `RSBK` container (Ε-F1, ruled — no ZIP, no compressor, no link
edge to `vendor/WDL/WDL/zlib/`): a fixed little-endian header carrying two version
integers — `formatVersion` (what the writer emitted) and `minReaderVersion` (the oldest
reader that can read it safely) — a length-prefixed JSON manifest, and payloads
concatenated in manifest order. `classifyPackageVersion` answers `Readable`/`TooNew`/
`Malformed`; a `TooNew` header refuses whole, producing no manifest, so the refusal can
still name the writer's semver rather than half-succeeding. The ladder's one
counterintuitive rule rides with the contract: **growing a persisted enum's vocabulary is
structural, not additive** — `BankModel::deserialize` *rejects* an out-of-range `SourceMode`
or `Tier` rather than degrading it (`bank_model.cpp:232-239`, `:339-346`), and every enum a
package carries rides inside the nested `BankModel` blob, so a new value bumps both
integers, where a new `Sample` field with a defined absent-value bumps `formatVersion`
alone. Landed as three modules: `package_format` (the contract, the version ladder, and
three name-validation rules —
`isValidEntryName`, `sameEntryName`'s ASCII-case fold, `isValidNestedSamplePath`),
`package_manifest` (the manifest model + JSON codec, carrying the bank's `slot_map` and a
whole-file `hashBytes` digest per entry — deliberately not `hashWavContent`, which skips
chunks and so cannot answer "did these bytes survive"), and `bank_package`
(framing/layout arithmetic: `encodePackage`/`decodePackage`/`requiredPrefixSize`, never
holding or hashing a payload itself). Hostile input is refused, never UB, at every byte
offset.
#### Ε-W1-T2 — `package-fs-shell`
**Landed** — see `docs/COMPLETED.md` for the full narrative. `src/shell/package/`:
streaming, atomic package filesystem I/O (`package_io`'s `PackageFileWriter`/
`PackageFileReader`, at most one entry's payload materialized at a time, backed by a
`.rsbanktmp` sibling that reaches the destination only through a `commit()` rename —
process-crash atomic, not power-loss atomic, deliberately, since an `fsync` over a whole
sample bank is a real stall) and the rollback journal (`package_rollback`'s
`LandedFileJournal`, citing the `prune_fs.cpp` carve-out rather than restating it,
disarmed only after the caller's own write has returned success). `package_pickers` is the
one picker seam, REAPER's own in both directions as specified — `GetUserFileName` with
`mode=0` for export's destination and `mode=1` for import's source, so no platform `#ifdef`,
no SWELL `BrowseForSaveFile`, no Win32 `GetSaveFileNameW`, and no `GetUserFileNameForRead`,
which the SDK header marks superseded by `GetUserFileName`. No fallback path was needed
either: `main.cpp` aborts the extension load if any API pointer fails to resolve, so no
REAPER build that loads the extension can lack it. The picker is `[verify — DAW]` in both
directions, never exercised in a live REAPER session.
#### Ε-W1-T3 — `import-origin-kind`
**Landed** — see `docs/COMPLETED.md` for the full narrative. `OriginKind::PackageImport`
appended to the tracking ledger as value 5 — package-sourced vs `Ingest`'s user-picked.
Append-only, per `core/tracking/CLAUDE.md`'s persisted-integer rule; an unrecognized kind
degrades to `Unknown` rather than failing the parse, and `kLedgerVersion` stays at 2 — a
vocabulary addition, not a document-version bump. No decision surface changed:
`pruneProtection`'s output is unaffected for every existing kind.
---
### Ε-W2 — The two verbs
**Both tracks have landed** — Ε-W2-T1 (`bank-export`) and Ε-W2-T2 (`bank-import`) — see
`docs/COMPLETED.md` for the full narrative of each.
**Depends on Ε-W1 — all three tracks.** T1 for the format the verbs speak, T2 for every
filesystem act they perform, T3 for the kind their birth records carry. No part of either
verb is authorable against a format that has not settled.
**Two tracks, disjoint by direction.** They share only the manifest type. The split is real
enough that the plan **pre-split the pure planner into two TUs** (`export_plan` /
`import_plan`) rather than one `package_plan` — that separation exists specifically so these
two tracks do not fight over a file.
| Track | Owns |
|---|---|
| **T1** `bank-export` | `core/package/export_plan`, `shell/package/export_bank`, `shell/actions/package_export_action` |
| **T2** `bank-import` | `core/package/import_plan`, `shell/package/import_bank`, `shell/actions/package_import_action`, the panel's `.rsbank` drop route |
**Two shared files, named — and the disjointness here is CONDITIONAL, unlike W1's.**
`src/app/main.cpp` (one action-family registration line each) and the panel's bank menu (one
row each). Both are textual adjacency by construction, but Phase Ψ set the precedent of
granting `main.cpp` to a single track rather than sharing it (Ψ-W1-T3). **If the dispatcher
wants zero contention, serialize T2 behind T1** — T2 is the larger track and loses nothing by
starting second. The plan's default is to run them in parallel and rebase whichever lands
second.
#### Ε-W2-T1 — `bank-export`
**Landed** — see `docs/COMPLETED.md` for the full narrative. New `core/package/export_plan`
(pure: which entries, what names, what is missing, and therefore whether the export may
proceed — verdict `Ready`/`Incomplete`/`Refused`) and `shell/package/export_bank` (the
promptless verb, in three composable public steps — `surveyBankExport`, `digestSources`,
`writePackageFile` — arriving with a **const** `ReaSamplerSession&`, so "writes no ext
state, opens no undo point, never bumps the generation" holds by the type rather than by
memory), plus `shell/actions/package_export_action`, one `main.cpp` action-table row, and
one panel bank-menu row. Nothing is re-encoded; payloads are copied and hashed. The
exported unit is one bank — the pool included, since the pool is structurally one
`BankIndex` among many — and whole-book export stays out of scope for the phase. Both open
questions were answered at review: affordance ships as **both** the bindable action and
the panel row, and the default file name derives from the bank's display name through
`capture_paths::sanitizeStem`.
#### Ε-W2-T2 — `bank-import`
**Landed** — see `docs/COMPLETED.md` for the full narrative. New
`core/package/import_plan` (pure: the id remap table, the parent remap, the per-entry
land/skip-already-present/rename disposition, and the destination bank's display name
after `BankBook`'s own uniqueness fold — reached through a new additive
`BankBook::uniqueDisplayName` member, the only `core/model/` edit in the phase), and on
the shell side a REAPER-free `import_landing` (decode, verify every payload's
`hashBytes` digest against the manifest BEFORE the bank folder is created, then land
through the rollback journal) plus a REAPER-facing `import_bank` (the only piece
touching the extension's project state — the undo-batched persist and the generation
bump), `shell/actions/package_import_action`, the panel's `.rsbank` drop route, one
`main.cpp` row, one panel menu row, and a new `src/core/util/ascii_ws.h`. The
tracking-ledger guard runs before the file picker opens (Ε-F3, ruled: refuse outright on
`Unreadable`/`FutureVersion`, no confirm-and-proceed); the version gate runs before any
byte is written; all four collision classes — sample id, file name, content hash, bank
display name — are answered explicitly, with the display-name collision auto-suffixed
and never prompted (Ε-F2, ruled: always a new bank, never a merge); birth records land
via `recordCreated(sample, OriginKind::PackageImport)` in the same straight-line block
as the index add; the index mutation is one Ctrl-Z, and the landed files' survival as
orphans until the next prune is stated in the user-facing summary, not left implicit.
**Beyond spec:** `import_plan`'s `spelledLikeABankFile` mints a fresh name even absent a
collision, whenever the package's own entry name isn't spelled the way
`deriveBankPaths` would spell it — counted separately from a genuine folder-name
collision (`sanitizeRenameCount` vs `collisionRenameCount`) so a hostile or
foreign-spelled entry name (e.g. an unexpected extension) always lands sanitized rather
than verbatim.
---
### Ε-W3 — The compatibility fixtures
**Depends on Ε-W2 for:** both verbs existing. A round-trip claim cannot be tested against one
half of a round trip, and a "this build refuses a future package" claim cannot be tested
against a package this build is incapable of writing.
**One track.** The whole deliverable is one corpus and the harness over it; splitting it
would mean two tracks writing two halves of one fixture set.
**Ε-W3-T1 has landed** — `package-compat-fixtures` — see `docs/COMPLETED.md` for the full
narrative. 23 frozen `.rsbank` fixtures now live under `tests/fixtures/package_compat/`
(three version fixtures, nine truncations, eleven hostile-name packages, ~15 KB total),
decoded by a new `package_compat_tests` and driven through export → import → export by a
new `package_round_trip_tests`, plus the DAW script `docs/verify-package-transfer.md` for
the one claim no unit test can make. A repo-root `.gitattributes` (`*.rsbank binary`)
keeps the frozen bytes frozen under `core.autocrlf`. No production module was touched.
**Open question resolved:** the recommendation was followed — the corpus is one-sample
packages with a 300-byte payload each. **Deviation from spec:** RSBK stores no layout
section (it is derived from the manifest), so the planned "mid-layout" truncation lands
at the payload boundary instead, where the manifest parses but the exact-size proof fails.
---
## Phase Λ — ReaSampler on Linux: both artifacts, shipped ## Phase Λ — ReaSampler on Linux: both artifacts, shipped
**Ships:** `reaper_reasampler.so` and `reasampler_9000.vst3` built, installed and documented **Ships:** `reaper_reasampler.so` and `reasampler_9000.vst3` built, installed and documented
@@ -3472,7 +2637,9 @@ All textual adjacency, not semantic contention, unless marked otherwise.
## Traceability — all seventeen items ## Traceability — all seventeen items
The check that nothing was dropped. Every row points at a track that exists above. The check that nothing was dropped. Every row points at a track. All seventeen items are
Θ or Ξ work, and both phases have since landed in full — their track sections are retired
from this file; see `docs/COMPLETED.md` for each track's full narrative.
| # | Item (short) | Phase-Wave-Track | Worktree slug | | # | Item (short) | Phase-Wave-Track | Worktree slug |
|---|---|---|---| |---|---|---|---|
@@ -3521,10 +2688,11 @@ proof it exists to give.
- **All of Phase Ψ** (`ppsi-*`). **Seven tracks across three waves**, from a direct list - **All of Phase Ψ** (`ppsi-*`). **Seven tracks across three waves**, from a direct list
of seven defects and refinements (Daniel, 2026-08-01), not from `TODO-1.0.md`. Listed 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 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 recorded verbatim in the phase header as its provenance (Ψ.1–Ψ.7). Phase Ψ has since
content lives inline in its tracks. The seventh track, Ψ-W3-T1, is not one of the landed in full and its `PLAN.md` section is retired; the design content and the
seven defects/refinements itself — it came from a review finding mid-phase; see the Ψ.1–Ψ.7 list now live in `docs/COMPLETED.md`. The seventh track, Ψ-W3-T1, is not one of
Phase Ψ section for detail. the seven defects/refinements itself — it came from a review finding mid-phase; see
`docs/COMPLETED.md` for detail.
- **Γ-W3-T2 `bake-reset-amendment` is a CORRECTION, not a feature**, and belongs on this list - **Γ-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 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 plan's sequencing claim. If more corrections of this shape appear, they belong here rather
@@ -3596,6 +2764,7 @@ than discovering it later:
``` ```
Phase Θ — ReaSampler 9000: one parameter set, filter, shapeable envelopes, legible editor Phase Θ — ReaSampler 9000: one parameter set, filter, shapeable envelopes, legible editor
[LANDED — all seven waves (W1-W7); see docs/COMPLETED.md; full detail section removed from this file]
W1 Collapse and re-seam W1 Collapse and re-seam
T1 zone-retirement ......................... 16 T1 zone-retirement ......................... 16
T2 capture-handoff-bugs .................... 5, 6 T2 capture-handoff-bugs .................... 5, 6
@@ -3618,6 +2787,7 @@ Phase Θ — ReaSampler 9000: one parameter set, filter, shapeable envelopes, le
T1 arc-and-spline-aa ....................... (not one of the seventeen) T1 arc-and-spline-aa ....................... (not one of the seventeen)
Phase Ξ — The resample loop (W1 concurrency-safe with Θ from Θ-W2 onward) Phase Ξ — The resample loop (W1 concurrency-safe with Θ from Θ-W2 onward)
[LANDED — all three waves (W1-W3); see docs/COMPLETED.md; full detail section removed from this file]
W1 Consolidated tracking, and the programmed-note model W1 Consolidated tracking, and the programmed-note model
T1 tracking-consolidation .................. 17 T1 tracking-consolidation .................. 17
T2 note-program-model ...................... 15 (model) T2 note-program-model ...................... 15 (model)
@@ -3656,6 +2826,7 @@ Phase Γ — The instrument's control surface (none of the seventeen; ends
what Xi-W2-T1 shipped, and serialize behind T1 if it does not hold. 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) Phase Psi — The extension trust pass (none of the seventeen; a direct list of seven)
[LANDED — all three waves (W1-W3); see docs/COMPLETED.md; full detail section removed from this file]
W1 Exact bounds, disciplined switches, reachable actions, resolved drops [4 tracks] W1 Exact bounds, disciplined switches, reachable actions, resolved drops [4 tracks]
T1 capture-range-exactness ..... Psi.7 [opens with a DAW repro matrix] T1 capture-range-exactness ..... Psi.7 [opens with a DAW repro matrix]
T2 mode-switch-discipline ...... Psi.2 + Psi.3 [one chokepoint: applyMode] T2 mode-switch-discipline ...... Psi.2 + Psi.3 [one chokepoint: applyMode]
@@ -3674,6 +2845,7 @@ Phase Psi — The extension trust pass (none of the seventeen; a direct
exclusively. exclusively.
Phase Epsilon — The bank package (none of the seventeen; a direct request 2026-08-02) Phase Epsilon — The bank package (none of the seventeen; a direct request 2026-08-02)
[LANDED — all three waves (W1-W3); see docs/COMPLETED.md; full detail section removed from this file]
W1 The contract, the filesystem, and the ledger's new kind [3 tracks, disjoint by dir] W1 The contract, the filesystem, and the ledger's new kind [3 tracks, disjoint by dir]
T1 package-format .............. core/package: framing + TWO version ints T1 package-format .............. core/package: framing + TWO version ints
[E-F1 RULED: proprietary RSBK. No ZIP, no zlib] [E-F1 RULED: proprietary RSBK. No ZIP, no zlib]
+26 -26
View File
@@ -16,16 +16,16 @@ Checks for Θ, Ξ, Ψ, and Ε work that no unit test can close. Build **Release*
## The decisive observation ## The decisive observation
- [ ] **Run first.** `docs/verify-track-scope-multitrack.md` §3 by hand, and count the files REAPER writes (`docs/TODO.md:552`, `PLAN.md:2134`, `PLAN.md:2238`) - [ ] **Run first.** `docs/verify-track-scope-multitrack.md` §3 by hand, and count the files REAPER writes (`docs/TODO.md:552`, `docs/COMPLETED.md:894`, `docs/COMPLETED.md:906`)
- [ ] Two files confirms Ψ-W1-T1 and Ψ-W3-T1 at once; **one summed file invalidates both** — stop and report, the refusal is costing a capture 1.0.0 accepted (`PLAN.md:2239`, `docs/COMPLETED.md:884`) - [ ] Two files confirms Ψ-W1-T1 and Ψ-W3-T1 at once; **one summed file invalidates both** — stop and report, the refusal is costing a capture 1.0.0 accepted (`docs/COMPLETED.md:905`)
- [ ] Then walk the rest of `docs/verify-track-scope-multitrack.md` (§1–§2, §4–§7) for the multi-track refusal itself (`PLAN.md:2240`) - [ ] Then walk the rest of `docs/verify-track-scope-multitrack.md` (§1–§2, §4–§7) for the multi-track refusal itself (`docs/COMPLETED.md:898`)
## Capture range and bounds ## Capture range and bounds
- [ ] Over an item much longer than the selection: item scope × time selection, and item scope × razor — each lands exactly the window, not the whole item (`PLAN.md:2136`) - [ ] Over an item much longer than the selection: item scope × time selection, and item scope × razor — each lands exactly the window, not the whole item (`docs/COMPLETED.md:844`)
- [ ] Same source: track scope × time selection, and track scope × razor — same exact window (`PLAN.md:2136`) - [ ] Same source: track scope × time selection, and track scope × razor — same exact window (`docs/COMPLETED.md:844`)
- [ ] One razor-union case (two disjoint areas, one track) — lands the requested window, no `ReaSampler capture failed:` line (`PLAN.md:2137`) - [ ] One razor-union case (two disjoint areas, one track) — lands the requested window, no `ReaSampler capture failed:` line (`docs/COMPLETED.md:852`)
- [ ] Capture an item whose extent already equals the window — still lands, unchanged (the byte-identity regression floor) (`docs/COMPLETED.md:829`) - [ ] Capture an item whose extent already equals the window — still lands, unchanged (the byte-identity regression floor) (`docs/COMPLETED.md:849`)
- [ ] **The millisecond floor — SETTLED, nothing to re-run for `TailMode::None`.** The floor lives in the custom-time-bounds field (`RENDER_BOUNDSFLAG=0`), not in the render engine. Two live 48 kHz `TailMode::None` renders on `RENDER_BOUNDSFLAG=2` (time selection, handed over via `GetSet_LoopTimeRange`) came back exact — 97627 frames against 97627 — the second over a window whose START carried a sub-millisecond remainder, with no floored model of that window able to reproduce the count. Time selection is now the only bounds mode a capture can reach; the console verdict line and the `RENDER_STARTPOS`/`ENDPOS` read-back probe that answered this are gone. Full observation: `src/core/capture/render_settings.h`'s `kRenderBoundsTimeSelection` - [ ] **The millisecond floor — SETTLED, nothing to re-run for `TailMode::None`.** The floor lives in the custom-time-bounds field (`RENDER_BOUNDSFLAG=0`), not in the render engine. Two live 48 kHz `TailMode::None` renders on `RENDER_BOUNDSFLAG=2` (time selection, handed over via `GetSet_LoopTimeRange`) came back exact — 97627 frames against 97627 — the second over a window whose START carried a sub-millisecond remainder, with no floored model of that window able to reproduce the count. Time selection is now the only bounds mode a capture can reach; the console verdict line and the `RENDER_STARTPOS`/`ENDPOS` read-back probe that answered this are gone. Full observation: `src/core/capture/render_settings.h`'s `kRenderBoundsTimeSelection`
- [ ] **Still open — Auto and Manual tail.** `checkRenderedBounds` judges `TailMode::None` only (Auto/Manual add frames by design), so the settled result covers those two by INFERENCE, not observation, and the inference rests on an unverified PREMISE too: that the (retired) floor applied to the bounds identically across all three tail modes, and that all three now hand the window over the same way. Neither is measured — both live short renders that settled the bounds mode were `TailMode::None`; no Auto or Manual capture has been observed at all. **On Auto/Manual, the ONLY automatic check left is the 0-byte gate (`checkRenderedFileNotEmpty`)** — there is no automatic bounds signal for those two modes at all until this bullet is closed by hand. What would establish it: repeat an off-grid-start capture at **Manual** over a source that is loud right to the window's end, and check the landed file's frames against window + `tailMs` — a floored edge shows up in that count. **Auto** cannot be checked by count (it trims trailing silence), so it needs the null test by ear/inversion against the source instead - [ ] **Still open — Auto and Manual tail.** `checkRenderedBounds` judges `TailMode::None` only (Auto/Manual add frames by design), so the settled result covers those two by INFERENCE, not observation, and the inference rests on an unverified PREMISE too: that the (retired) floor applied to the bounds identically across all three tail modes, and that all three now hand the window over the same way. Neither is measured — both live short renders that settled the bounds mode were `TailMode::None`; no Auto or Manual capture has been observed at all. **On Auto/Manual, the ONLY automatic check left is the 0-byte gate (`checkRenderedFileNotEmpty`)** — there is no automatic bounds signal for those two modes at all until this bullet is closed by hand. What would establish it: repeat an off-grid-start capture at **Manual** over a source that is loud right to the window's end, and check the landed file's frames against window + `tailMs` — a floored edge shows up in that count. **Auto** cannot be checked by count (it trims trailing silence), so it needs the null test by ear/inversion against the source instead
- [ ] `[verify — DAW]` A tail is assumed to render PAST the window end — the SDK header (`:3048`) confirms only that `RENDER_TAILMS` is a length in ms, not that it extends past the end. If that assumption is wrong, a tail capture is silently SHORTER than its window with no detector at all. Report whether either tail capture comes up short against the source - [ ] `[verify — DAW]` A tail is assumed to render PAST the window end — the SDK header (`:3048`) confirms only that `RENDER_TAILMS` is a length in ms, not that it extends past the end. If that assumption is wrong, a tail capture is silently SHORTER than its window with no detector at all. Report whether either tail capture comes up short against the source
@@ -34,28 +34,28 @@ Checks for Θ, Ξ, Ψ, and Ε work that no unit test can close. Build **Release*
## Names and channels ## Names and channels
- [ ] Capture from a named track — the card reads `<Track> MM-DD HHMM`; capture again the same minute and the second carries an ordinal (`PLAN.md:2199`) - [ ] Capture from a named track — the card reads `<Track> MM-DD HHMM`; capture again the same minute and the second carries an ordinal (`docs/COMPLETED.md:882`)
- [ ] Capture from an unnamed track, and from a multi-item selection — both readable, `+N` present on the multi (`PLAN.md:2199`, `docs/COMPLETED.md:862`) - [ ] Capture from an unnamed track, and from a multi-item selection — both readable, `+N` present on the multi (`docs/COMPLETED.md:882`)
- [ ] Load a named capture into ReaSampler 9000 — the same name shows there (`PLAN.md:2200`) - [ ] Load a named capture into ReaSampler 9000 — the same name shows there (`docs/COMPLETED.md:881`)
- [ ] The card label stays legible over its scrim at every card size (`docs/COMPLETED.md:863`) - [ ] The card label stays legible over its scrim at every card size (`docs/COMPLETED.md:883`)
- [ ] Capture a dead-center mono source — the `.wav` is roughly half the size of the equivalent stereo capture (`PLAN.md:2216`) - [ ] Capture a dead-center mono source — the `.wav` is roughly half the size of the equivalent stereo capture (`docs/COMPLETED.md:887`)
- [ ] Insert that collapsed file on a stereo track and null it against the source — confirms REAPER sums a 1-channel item at unity (`PLAN.md:2218`) - [ ] Insert that collapsed file on a stereo track and null it against the source — confirms REAPER sums a 1-channel item at unity (`docs/COMPLETED.md:891`)
- [ ] Capture a true-stereo source — stays 2-channel, and both it and the collapsed file load into the instrument correctly (`PLAN.md:2217`) - [ ] Capture a true-stereo source — stays 2-channel, and both it and the collapsed file load into the instrument correctly (`docs/COMPLETED.md:889`)
## Mode switching ## Mode switching
- [ ] Solo tracks in Arrange, switch to Design, solo different tracks, switch back — each mode restores its own solo set verbatim (`PLAN.md:2151`) - [ ] Solo tracks in Arrange, switch to Design, solo different tracks, switch back — each mode restores its own solo set verbatim (`docs/COMPLETED.md:864`)
- [ ] Attempt a mode switch while the transport is playing, then while recording — both refuse, visibly (`PLAN.md:2151`) - [ ] Attempt a mode switch while the transport is playing, then while recording — both refuse, visibly (`docs/COMPLETED.md:859`)
- [ ] Click the footer mode segment, save, reopen the project — the mode persisted (`PLAN.md:2152`, `docs/COMPLETED.md:841`) - [ ] Click the footer mode segment, save, reopen the project — the mode persisted (`docs/COMPLETED.md:861`)
## Actions and drops ## Actions and drops
- [ ] Add the import action to a Media Explorer toolbar and fire it from there — it imports (`PLAN.md:2162`) - [ ] Add the import action to a Media Explorer toolbar and fire it from there — it imports (`docs/COMPLETED.md:867`)
- [ ] Fire the existing Main-section import binding — still works (`PLAN.md:2163`) - [ ] Fire the existing Main-section import binding — still works (`docs/COMPLETED.md:869`)
- [ ] Unload/reload (restart REAPER) — no duplicate Media Explorer entry in the action list (`PLAN.md:2164`) - [ ] Unload/reload (restart REAPER) — no duplicate Media Explorer entry in the action list (`docs/COMPLETED.md:870`)
- [ ] Drag one card across the arrange, over an FX window, over the TCP/MCP and back — cue changes per surface, every transition reverses (`PLAN.md:2176`, `PLAN.md:2177`) - [ ] Drag one card across the arrange, over an FX window, over the TCP/MCP and back — cue changes per surface, every transition reverses (`docs/COMPLETED.md:875`, `docs/COMPLETED.md:876`)
- [ ] Drag fast, and drag onto a narrow TCP — target class still resolves; no release anywhere in REAPER is a silent no-op (`PLAN.md:2178`, `docs/COMPLETED.md:858`) - [ ] Drag fast, and drag onto a narrow TCP — target class still resolves; no release anywhere in REAPER is a silent no-op (`docs/COMPLETED.md:875`, `docs/COMPLETED.md:878`)
- [ ] Drop a single card into the arrange — an item lands at the pointer's track and time (`PLAN.md:2174`) - [ ] Drop a single card into the arrange — an item lands at the pointer's track and time (`docs/COMPLETED.md:877`)
- [ ] Drag-out to an external app twenty-plus times in a row — audio arrives every time; this is a soak, a single pass is not a gate (`docs/COMPLETED.md:109`) - [ ] Drag-out to an external app twenty-plus times in a row — audio arrives every time; this is a soak, a single pass is not a gate (`docs/COMPLETED.md:109`)
- [ ] Drop a capture onto an FX container — the instrument loads with that capture (`docs/COMPLETED.md:110`) - [ ] Drop a capture onto an FX container — the instrument loads with that capture (`docs/COMPLETED.md:110`)
@@ -65,7 +65,7 @@ Checks for Θ, Ξ, Ψ, and Ε work that no unit test can close. Build **Release*
export/import round trip: writes-one-file, the transfer itself, re-importing the export/import round trip: writes-one-file, the transfer itself, re-importing the
same file never overwrites, the round trip back to the source, the too-new / same file never overwrites, the round trip back to the source, the too-new /
truncated / mid-payload-corruption refusals (each an exact string), the truncated / mid-payload-corruption refusals (each an exact string), the
unsaved-project refusals, and drag-and-drop (`docs/COMPLETED.md:1068`, `PLAN.md:2578`) unsaved-project refusals, and drag-and-drop (`docs/COMPLETED.md:1126`)
- [ ] Force a degraded tracking ledger and confirm the import refuses **before the - [ ] Force a degraded tracking ledger and confirm the import refuses **before the
file picker opens**: save a project with a bank, close REAPER, edit the saved file picker opens**: save a project with a bank, close REAPER, edit the saved
`.rpp`'s `owned_files` ext-state value inside its `<REASAMPLER ...>` block — corrupt `.rpp`'s `owned_files` ext-state value inside its `<REASAMPLER ...>` block — corrupt
@@ -115,8 +115,8 @@ Checks for Θ, Ξ, Ψ, and Ε work that no unit test can close. Build **Release*
## Look and feel ## Look and feel
- [ ] Sign off by eye in a live editor window: knob arcs, needles, envelope splines, waveform outline (`docs/COMPLETED.md:669`, `PLAN.md:375`) - [ ] Sign off by eye in a live editor window: knob arcs, needles, envelope splines, waveform outline (`docs/COMPLETED.md:669`, `docs/COMPLETED.md:583`)
- [ ] Same pass for legibility: text sizes, arc weight, and whether the waveform stroke thickens the docked panel (`docs/COMPLETED.md:613`, `PLAN.md:348`) - [ ] Same pass for legibility: text sizes, arc weight, and whether the waveform stroke thickens the docked panel (`docs/COMPLETED.md:613`, `docs/COMPLETED.md:562`)
- [ ] Piano strip at the 840 px default — keys tile uniformly, the 37 px end gutters read as acceptable (`docs/COMPLETED.md:219`) - [ ] Piano strip at the 840 px default — keys tile uniformly, the 37 px end gutters read as acceptable (`docs/COMPLETED.md:219`)
- [ ] Resize the editor across several widths — gutters stay symmetric, no key width jumps (`docs/COMPLETED.md:217`) - [ ] Resize the editor across several widths — gutters stay symmetric, no key width jumps (`docs/COMPLETED.md:217`)
- [ ] Set host/OS scaling to 150% then 200% — record how the strip and the AA strokes actually look (`docs/TODO.md:418`, `docs/COMPLETED.md:226`) - [ ] Set host/OS scaling to 150% then 200% — record how the strip and the AA strokes actually look (`docs/TODO.md:418`, `docs/COMPLETED.md:226`)