docs(phase-s): spec three-view editor redesign (S-VIEW) — Sample/Browse/Zone

This commit is contained in:
2026-07-27 12:46:43 -04:00
parent c22a78bcba
commit 13b23801a0
3 changed files with 530 additions and 147 deletions
+290 -27
View File
@@ -2139,6 +2139,274 @@ not a raw literal). What S18 adds is only the missing *plugin identity* layer.
headers before use — several §1a claims are experienced estimates until the spike
confirms them.
## Editor view-model redesign — three views: Sample / Browse / Zone (S-VIEW; Daniel, 2026-07-27, r9)
> **Additive sub-phase of Phase S — an *editor* redesign, not an engine change.** Re-partitions
> the ReaSampler 9000 editor from today's two-view toggle (Browser + Zones) into a **three-view
> model where the loaded sample is the home**, adds two new performance parameters (key-tracking,
> preview velocity) and two visual components (envelope overlay, real piano-key pattern), and
> frames two engineering prerequisites (drop-to-FX bug, default window size). The S3 voice
> engine, keymap resolution, and read-only-over-bank contract are **unchanged**; the
> component-state format extends additively for key-tracking. Product framing: `docs/product/
> midi-playback.md` §Addendum r9. Same standing discipline: **LICE/SWELL drawing only,
> all layout/hit-test in pure geometry modules, RT-safe, VST3 class UID unchanged, verify every
> API name/signature against the vendored headers before use.**
### The view model — Sample is home; Browse is modal; Zone is a dedicated surface
The three views are **not a flat three-way toggle** (today's Browser|Zones segmented switch is
retired). The model is **document-with-modal-picker** (mirror: Ableton Simpler — the device face
*is* the loaded sample; loading a new one is a distinct act):
- **Sample (the home / default face).** What the editor shows on open with a capture loaded. The
hero waveform, the envelope overlay, all per-sample tuning controls, the preview-trigger. This
is where the user lives.
- **Browse (a modal page layered over Sample).** Summoned by a **Browse button** (and, when
nothing is loaded, the empty-state's primary affordance — Browse must be *very* easy to open
when there is no capture selected). It renders as a **full-window overlay** over the Sample
face — filters + captures grid + **select-then-confirm** to change the loaded sample, then
dismisses back to Sample. It is a picker sheet, not a peer tab.
- **Zone (a dedicated editing surface).** Opened by its own **Zone button** when the user wants
to map the capture(s) across the keyboard. Not shown by default ("most of the time zones won't
be used" — the S10 reframe). Returns to Sample on close.
**Navigation contract.** From Sample: a Browse button opens the Browse overlay; a Zone button
opens the Zone surface. From Browse: select a card + confirm (or cancel) returns to Sample with
(or without) a new loaded capture. From Zone: a close/back affordance returns to Sample. The
empty state (no capture loaded) surfaces Browse as its dominant call-to-action. **The fresh
instance stays silent with a "pick a capture" empty state (S10 reversal, unchanged).**
### View 1 — Sample (the new main view)
The home face. Composition follows the reference devices (Simpler / Phase Plant): a **hero
waveform up top with the envelope drawn over it at accurate time**, a **dense labelled
value-strip beneath**, **root fenced as its own affordance**, and a **preview cluster**. Bands,
top to bottom:
- **Title band.** Plugin name (channel-derived `vstPluginName()`) + live readout (loaded capture
name / `[pick a capture]` / `[bank empty]` / `[host: no bridge]`) + the **Browse** and **Zone**
view buttons. (Inherits today's title-band text logic.)
- **Hero waveform band (enlarged) with the ENVELOPE OVERLAY (new).** The picked capture's
full-resolution envelope (from `peaks` over the cached mono PCM — no new decode), given real
vertical space (the Simpler/Phase-Plant hero, materially taller than today's 72px strip). Drawn
over it, at **accurate wall-clock time**: (a) the **S11 markers** — start (teal/secondary),
loop start/end (purple/tertiary), the faint loop-region fill — moved here from Browse
unchanged; and (b) the **NEW amplitude-envelope overlay** — the AHDSR shape (Gate) or the
fade-in/%-length/fade-out shape (Trigger) traced as a curve across the sample at the same time
base the voice engine uses, in an accent hue. This is the "reads like a real sampler, not a
spreadsheet" move: the envelope becomes a *shape over the sound*, not four abstract sliders.
- **Root + preview cluster (fenced, new preview-trigger).** Root note shown as a first-class,
always-visible control (fenced like Phase Plant's "Root" box), draggable/typeable — this is the
keyboard-strip root-drag from today's Browse setup, promoted to a fenced control. Adjacent: the
**NEW preview-trigger button** (fires the sampler at the loaded capture's root note through the
live voice engine, off the audio-thread commit path — no MIDI controller needed) with an
**adjacent velocity knob** setting the preview velocity level. The **Mono/Stereo toggle** moves
here from Browse (it is a per-capture output-mode tuning concern, not a choosing concern).
- **The "Modes-and-down" control strip (moved from Zone).** Every per-sample control that lives
on today's Zones param panel for the single-capture case — **Mode** (Gate/Trigger), **Pitch
engine** (Varispeed/Preserve), the **AHDSR** sliders (Gate) or **Length%/Fade-in/Fade-out**
(Trigger), and the **AD pitch envelope** (Off/On + P.Attack/P.Decay/P.Depth) — renders here as
the Sample view's control strip, laid out as a dense labelled value-row (bold micro-caps over a
value/slider, the reference grammar). For a single loaded capture this is the same one-zone
storage site the S15-F2 lean already established (the empty-map single-capture face reads/writes
the same `PerformanceZone` defaults) — no new storage.
**Browse elements that MOVE to Sample (inventory — nothing silently dropped):** the large
waveform preview, the S11 start/loop markers + loop fill, the keyboard-strip root affordance (now
the fenced root control), and the Mono/Stereo toggle.
### View 2 — Browse (reduced to *choosing*, laid over Sample as a modal)
Browse's only job is **pick a capture**. Today's Browser view is close but overloaded; it is cut
to the choosing essentials and rendered as a modal overlay:
- **Kept (the excellent core, unchanged):** the **type-to-filter search box**, the **bank filter
tabs** (All + one per named bank), the **captures grid** (cards: peak thumbnail + name +
root/key badge), **scroll** (wheel + thumb), and card **selection**.
- **Added:** a **confirm/cancel** affordance (select a card, confirm to load it into Sample and
dismiss; cancel to dismiss unchanged) — the modal-picker close semantics. Double-click-to-load
is the natural accelerator.
- **Removed from Browse (moved to Sample or cut):** the **large waveform preview** (redundant —
the grid thumbnails already show every capture's waveform; moved to Sample as the hero); the
**Mono/Stereo toggle** (moved to Sample); the **root-note keyboard-strip** in the setup band
(moved to Sample as the fenced root control); the **loop-point labels** and the **track-root
message** (cut — they waste space and add nothing to *choosing*).
**Rationale (why cut, not keep-and-hide):** Browse is a picker. Every tuning affordance on it is
a mode error — you tune what you *have*, you browse for what you *want*. Concentrating tuning on
Sample and choosing on Browse makes each view do one job, and makes the modal overlay light
enough to summon and dismiss without ceremony.
### View 3 — Zone (dedicated keyboard-mapping surface + key-tracking + real piano pattern)
Zone is the multi-zone keymap editor — RS5K structurally lacks a multi-zone-in-one-instance view,
so this is a genuine capability, kept as the deliberate secondary surface. Retains today's Zones
view wholesale, with two changes:
- **Kept (inventory — nothing silently dropped):** **+ Add Zone** / **Delete** buttons, the
**keyboard strip with one bar per zone** (selected zone lit accent-primary + static glow, others
categorical), the **selected-zone legend** with the three **Low/High/Root numeric-entry fields**
(click-to-type, `parseNoteEntry`), and the **per-zone param control panel** (Mode, Pitch engine,
AHDSR/Trigger, AD pitch envelope) — the same panel Sample now also hosts for the single-capture
case (one storage site, two surfaces).
- **NEW — key-tracking parameter (0%200%, default 100%).** A per-`PerformanceZone` scalar on how
sample pitch tracks the keyboard around the root note. **100% = standard 12-tone-ET tracking**
(today's behavior, bit-identical); **0% = no tracking** (the sample plays at root pitch on every
key — a fixed one-shot); **200% = double-rate tracking**. Additive on `PerformanceZone`,
version-bumped, defaulting to 100% so pre-existing zones are unchanged. Applies inside the
repitch math in **both** engines: under **Varispeed** it scales the semitone offset feeding
`pitchRatio(note, root)` (`effectiveSemis = (note root) · keyTrack`); under **Preserve** it
scales the semitone offset feeding `set_shift(2^(effectiveSemis/12))`. Surfaces as a control in
the Zone param panel (and, for the single capture, on the Sample control strip). **The pure
sampler core owns the key-track math** (unit-tested: a known note/root/keyTrack triple asserts
the expected ratio); the shell only maps the 0200% control to the scalar.
- **NEW — real piano-key pattern on the keyboard strip.** Today's spectral strip is pretty but
does not read as a keyboard (Daniel's note). Keep the pastel spectral hue as the backdrop, but
**overlay the actual alternating white/black key pattern** — bright cells for naturals, dark
cells for accidentals (C#/D#/F#/G#/A#), per the palette (bright ≈ a light neutral, dark ≈
`bg/base`/hairline) — so the strip is instantly identifiable as a keyboard. It need not be
*shaped* like a keyboard (no protruding black keys); it carries the **pattern** as an overlay,
so a glance reads pitch position without counting. The pure `keyboard_strip` geometry gains a
`isBlackKey(note)`/per-key-natural query (12-tone pattern, pure + unit-tested); the shell draws
the two-tone overlay over the spectral fill. This same strip serves the Sample view's fenced
root affordance (one keyboard grammar everywhere).
### New parameters — ownership and persistence (D-B; instrument-owned, never bank facts)
- **Key-tracking** — per-`PerformanceZone`, additive/version-bumped component state, default 100%.
A **performance choice**, never written to `Sample` or the bank. Back-compat: an absent field on
an older blob → 100% (bit-identical playback).
- **Preview velocity** — a **utility** setting for the Sample view's preview-trigger button, not a
musical parameter of the capture. **Lean: transient editor state** (like a metronome level) —
it does not need to persist in component state, so it adds no version bump and no bank-fact risk.
*(Fork S-VIEW-F1, Daniel's call: transient vs. a small persisted editor convenience. Lean
transient — simplest, no compat surface, and a preview level is not something users expect to
survive a reload.)*
### The envelope overlay — visual component (new)
The amp envelope drawn as a curve over the Sample view's hero waveform at accurate wall-clock
time (the Simpler/Phase-Plant grammar). Gate → the AHDSR shape (attack ramp, hold, decay to
sustain, release tail); Trigger → the fade-in/unity/%-length/fade-out shape anchored to `playEnd`.
The time base is the same the voice engine resolves (seconds → frames at the live rate), so the
drawn shape lines up with the waveform under it. **Pure geometry:** an `envelope_overlay` module
(mirror of `waveform_view` / `param_slider`) maps the AHDSR/Trigger params + the sample's
frame-length to a polyline in the waveform rect (`param↔pixel` at the shared time base),
unit-tested against known param sets; the shell traces it via kit line draws in an accent hue.
Decorative + informative — it never captures input (the sliders remain the edit surface); dragging
the envelope *nodes* directly is a deferred stretch (fork S-VIEW-F2, lean defer — sliders first,
node-drag is polish once the overlay reads).
### The real piano-key pattern — visual component (new)
Covered under View 3 above. Pure: `keyboard_strip` gains the natural/accidental predicate; shell
draws the bright/dark overlay over the existing pastel spectral fill. Shared by the Zone strip and
the Sample root affordance.
### Engineering prerequisite 1 — drop-to-FX bug (routed to implementation, NOT a design call)
**Symptom (Daniel):** dropping a capture onto a track's FX chain does not instantiate + init
ReaSampler 9000 — the audio-to-arrange drop works, but the "instrument init never fires."
**SDK sweep (done this pass — no false ceiling asserted):** the S17 drop-to-FX machinery is
**present and SDK-correct**. `src/instrument_drop_win.cpp` calls `TrackFX_AddByName(track,
"VST3:" + vstPluginName(), /*recFX*/false, /*instantiate*/-1)` then injects component state via
`TrackFX_SetNamedConfigParm(track, fx, "vst_chunk", <base64>)`; both signatures + semantics are
**verified present** in `reaper_plugin_functions.h` (the `VST3:` prefix, negative-instantiate =
always-new, and the `vst_chunk` base64 write-parm are all documented). The gesture is wired
(`src/bank_panel.cpp`: `decideGesture``InstrumentDrop` → on release `buildInstrumentDropChunk`
`performInstrumentDrop`), FX-hotspot detection uses `GetThingFromPoint` (`fx_*` prefix), and the
factory registers `kInstrumentSynthSampler` correctly. **So the platform CAN do this — the
capability is not in question; the wiring is present.** This is therefore a **runtime diagnosis
task, not a design decision or a platform-ceiling problem.** The likely fault domains for
staff-engineer to investigate (in order):
1. **FX-name resolution:** whether REAPER's scanned name for the plugin exactly matches `"VST3:" +
vstPluginName()` (a display-name/scan-name mismatch would make `TrackFX_AddByName` return 1).
2. **The gesture reaching `performInstrumentDrop`:** whether `decideGesture` actually resolves
`InstrumentDrop` over an FX button (vs. falling through to `OsDrag`, which produces exactly the
observed "drops as audio into arrange" symptom) — check the `overFxHotspot`/`GetThingFromPoint`
path and the single-capture arming guard.
3. **The `vst_chunk` write / blob format:** whether `TrackFX_SetNamedConfigParm` returns true and
the injected blob matches what `setState` accepts (a format drift would create-then-play-nothing
rather than fail to instantiate).
**Routing:** flagged for **staff-engineer** as a bug diagnosis. No product fork; no redesign
gates on it, but it is a **functional prerequisite** for the drop-to-FX gesture to feel finished.
**Verify in DAW:** drag a capture onto a track's FX button → a ReaSampler 9000 instance appears on
that track already playing that capture (one Ctrl-Z removes it).
### Engineering prerequisite 2 — default window size for 1080p (routed to implementation)
**Symptom (Daniel):** the editor window is too small by default; assume a 1080p minimum screen.
**SDK sweep (done this pass — mechanism verified):** the VST3 editor size is set by the
`IPlugView`/`CPluginView` contract. `getSize()` returns the view's `rect` (set via `setRect` — the
default the host opens at); `checkSizeConstraint()` is where a minimum is enforced; `onSize()`
handles host resizes; `canResize()` already returns `kResultTrue`. **Verified in
`vendor/vst3sdk/public.sdk/source/common/pluginview.h`.** Today `ReaSamplerEditor`'s constructor
sets `ViewRect(0, 0, 560, 400)` — the undersized default. **The fix is a one-line default change**
(a larger initial `ViewRect` sized for the new three-band Sample face on a 1080p display) **plus
an optional `checkSizeConstraint` minimum** so the host cannot shrink the window below a usable
floor. **This is not a platform limitation — the mechanism exists and is trivial.** The exact
default dimensions are a build-time value to set against the Sample face's band heights (hero
waveform + control strip want materially more than 400px tall; a ~840×560 or larger default is the
starting point, tuned at build). **Routing:** flagged for **staff-engineer**; no product fork.
**Verify in DAW:** the editor opens at the new default on a 1080p screen showing the full Sample
face without scrolling, and cannot be resized below the constraint floor.
### Module architecture (preserve the pure/shell split)
- **Pure (new/extended):** `envelope_overlay` (AHDSR/Trigger params + frame-length → polyline in a
rect; unit-tested); `keyboard_strip` extended with the natural/accidental predicate; the sampler
core extended with the **key-track scalar** in the repitch math (unit-tested against known
note/root/keyTrack → ratio); `sample_map` (`PerformanceZone`) extended with the additive
`keyTrack` field + component-state version bump + back-compat default.
- **Shell (`reasampler_editor.cpp`):** re-partition the paint/hit-test into the three views
(Sample face, Browse modal overlay, Zone surface) replacing the two-view toggle; add the
preview-trigger button + velocity knob wired to an off-audio-thread preview note through the
voice engine; draw the envelope overlay + piano-key overlay via the kit; set the larger default
`ViewRect` + `checkSizeConstraint`. All layout/hit-test math stays in the pure geometry modules.
### Precision / invariant implications
- **Read-only bank consumer (unchanged).** Key-tracking, preview velocity, and every marker/mode
control are the instrument's **performance map** (D-B) — never written to `Sample` or the bank.
- **Additive, back-compat component state.** `keyTrack` is a new optional field defaulting to 100%;
an older blob (absent field) restores to 100% → **playback bit-identical** under the same engine.
No existing field changes.
- **RT discipline (unchanged).** The preview-trigger fires a note through the existing voice engine
via the off-audio-thread commit path (`commitMapAndReload` idiom); no new `process`-thread work,
no allocation on the audio thread.
- **VST3 class UID unchanged.** A view reorganization + additive param is **not** a compat event;
saved instances rebind and restore. The UID stays the S-NAME-1/S18 forever-locked identity.
- **Capture ≠ placement ≠ playback (unchanged).** The preview-trigger plays; it never captures,
never inserts a timeline item. The three acts stay distinct.
### Open questions / forks (Daniel / Phase S team)
- **S-VIEW-F1 — preview velocity persistence.** Transient editor state (lean) vs. a small persisted
editor convenience. Lean transient — no compat surface, no version bump.
- **S-VIEW-F2 — envelope-overlay interactivity.** Read-only informative overlay + sliders as the
edit surface (lean, ship first) vs. draggable envelope nodes on the overlay (deferred stretch —
polish once the overlay reads).
- **S-VIEW-F3 — Browse modal presentation.** Full-window overlay (lean — the modal-picker feel) vs.
a large centered sheet with a dimmed Sample behind. Presentation detail; either satisfies "easy
to summon and dismiss."
### Must-verify before build (S-VIEW)
- **Editor size mechanism** — `getSize`/`setRect`/`checkSizeConstraint`/`onSize`/`canResize`
**verified present** in `vendor/vst3sdk/public.sdk/source/common/pluginview.h`; confirm the exact
min-size enforcement point (`checkSizeConstraint`) behaves under REAPER's host at build.
- **Drop-to-FX** — `TrackFX_AddByName` / `TrackFX_SetNamedConfigParm` / `GetThingFromPoint` all
**verified present** in `reaper_plugin_functions.h`; the remaining work is **runtime diagnosis**
(name match, gesture routing, blob format), not an SDK question.
- **Preview note through the voice engine off-thread** — confirm the existing `commitMapAndReload`
/ off-thread reload idiom is the right seam to fire a one-shot preview note without touching
`process` on the UI thread; no torn state on the atomic voice-engine pointer.
- **Envelope-overlay time base** — confirm the seconds→frames resolution the overlay draws against
matches the voice engine's live-rate resolution so the drawn shape lines up with the waveform.
---
# Look-and-feel — visual design language (Phase L)
@@ -2520,12 +2788,10 @@ slots that persist independent of their occupant, no slot actions, no MIDI-binda
no capture-to-slot-N. A slot is just a display position a sample occupies; dragging cards
rearranges which sample sits where.
**M9 overlap (awareness note — unchanged intent).** M9 "slots" (capture-to-slot-N /
insert-slot-N, MIDI-bindable, MPC-style) remains **explicitly deferred (Daniel, 2026-07-26).**
The interchangeable substrate L7 builds still *eases* a future M9 revival — it lays the
addressable-position groundwork M9 would sit on — but L7 adds **no** slot-numbered capture/insert
actions and **no** MIDI bindings. The "plain vs. M9-shaped" sub-fork is closed: plain
gap-preserving substrate, per F2 above.
**M9 note.** M9 "slots" (capture-to-slot-N / insert-slot-N, MIDI-bindable, MPC-style) is
**abandoned (Daniel, 2026-07-27)** — will not be built. L7 adds **no** slot-numbered
capture/insert actions and **no** MIDI bindings. The "plain vs. M9-shaped" sub-fork is closed:
plain gap-preserving substrate, per F2 above.
### 2. Decorative metadata overlay (bars.beats · s.ms)
@@ -2656,20 +2922,18 @@ overlay/selection-border draw. The L1 kit draws; no palette/font decision re-ope
## The L3 gate + Phase S coordination contract
**L3 (VST editor + embed-strip restyle) is GATED on Phase S landing on dev.** The VST
editor (`IPlugView` LICE surface), the S6 embed strip, and the keyboard strip live in
Phase S, which is **not on dev yet** (it exists on the phase-s worktree). L3 cannot be
built on dev until Phase S's editor/embed surfaces (≈ S1 / S6 / S10) merge to dev — **the
Phase L team must not chase these files on dev; they are not there.** Until then L3 is a
planned, blocked point; L1, L2, L4, L5, L6, and **L7 have all landed** — see `COMPLETED.md`.
L4, L5, L6, and L7 are independent of the L3 gate.
**L3 (VST editor + embed-strip restyle) has landed (merged `c53683e`, 2026-07-27).** The
Phase S surfaces (`reasampler_editor.cpp` + `reasampler_embed.cpp`) arrived on dev drawing
flat `LICE_FillRect` blocks + raw GDI `DrawTextA`, off a local pre-L1 forest-green palette
— the coordination contract's "born in the kit" branch did not occur — so L3 performed a
full restyle of both draw shells through the L1 kit. **L1, L2, L3, L4, L5, L6, and L7
have all landed — see `COMPLETED.md`. Phase L is complete.** The Phase Q gate condition
"Phase S + L3 merged to dev" is now satisfied.
**Coordination contract (load-bearing):** Phase S's S10S13 build their interaction UX with
the **current** drawing and **adopt the L1 kit when it is available — they are NOT gated on
Phase L.** Whichever lands first (the L1 kit or the S10S13 UX), the kit is the **one source
of drawing**: if S10S13 reach dev before L1, they draw in the current language and L3
restyles them; if L1 lands first, they are born in the kit. Either way there is one kit and
one look; L3 completes the VST/embed adoption and applies the settled-and-revised **B +
**Coordination contract (load-bearing — resolved):** Phase S's S10S13 built their
interaction UX with the current drawing and adopted the L1 kit via L3 (the coordination
contract's "L3 restyles them" branch). There is now one kit and one look across both
artifacts; L3 completed the VST/embed adoption and applied the settled-and-revised **B +
three-accent pastel** treatment (with C's **pastel** spectral keyboard strip as the
signature surface), routing text through the kit's cached-font `text()` (§3.1 — the kit's
current face; no font change). The VST3 class UID is unchanged — a visual refresh is not a
@@ -2925,8 +3189,9 @@ and **(2) Phase L L3** merged to dev (the VST restyle, itself gated on Phase S).
have already landed** (see `COMPLETED.md`) — the earlier "L2 + L3" wording was stale and is
corrected to **L3 only**. **D2** is functionally complete (D2-W1..W3-B landed; its lone open item,
a per-track lane-split panel indicator, is *explicitly deferred*, not a blocking residual). **M9**
(slots) is *explicitly deferred* (Daniel, 2026-07-26), not scheduled work. D2 and M9 are named here
only so that *reactivating* either re-arms the quiescence condition; neither blocks the gate today.
(slots) is **abandoned** (Daniel, 2026-07-27) — will not be built. D2 is named here only so that
*reactivating* its deferred panel indicator re-arms the quiescence condition; neither D2 nor M9
blocks the gate today.
*Why:* Phase Q touches **nearly every file in `src/`** (relocate, re-namespace, split the four
largest TUs, plus the §2b renames); every large in-flight branch (Phase S on its worktree, L3 once
it lands) is diffed against the *current flat layout*, so landing a rename-and-relocate-everything
@@ -2935,10 +3200,8 @@ re-resolution, not a linear one. Phase Q is *last* precisely because it reshapes
other pillar stands on: landing it early taxes every subsequent phase; landing it last taxes
nothing. Re-confirm quiescence against dev before Q-W1.
> **M9 disposition (Daniel-decision note).** "Deferred indefinitely" ≠ "abandoned." Immaterial to
> the gate (both clear it); matters only if M9 is reactivated — *before* Phase Q it lands cheaply on
> the flat layout, *after* it is authored against the reorganized tree. Surfaced, not silently
> resolved (full note: `docs/product/code-organization.md` §4).
> **M9 disposition — resolved (Daniel, 2026-07-27): abandoned.** M9 is out; it will not be
> reactivated. The gate remains satisfied; no re-arm condition applies.
## Wave sequencing (each independently landable, CTest-green at every step)
@@ -2991,7 +3254,7 @@ every commit), a property only an *incremental* reorg uses. Risk-ordered:
the thumbnail path is noted but **not touched** — reworking it risks the hot path.
- **No big-bang commit.** Every wave is independently landable and CTest-green; reject a change set
that cannot be verified at each step.
- **Do not begin before the GATE.** Re-confirm the tree is quiescent (Phase S + L + D2 + M9
merged/closed) before any Q point.
- **Do not begin before the GATE.** Re-confirm the tree is quiescent (Phase S + L + D2 merged/closed;
M9 abandoned) before any Q point.
- **Verify** the CMake `src/` path updates and the SWELL/LICE surfaces still resolve after
relocation, as the existing build already requires.