docs: rule Phase Rho's three forks

Refuse multi-track; result track always Arrange; follow panel tail. Rho-F2 overrides the mode-following framing: explicit Arrange tags on track and item, plus an explicit-tag-wins filter in detectNewContent.
This commit is contained in:
2026-08-02 07:40:01 -04:00
parent 734672ba0e
commit c037aa1cb6
2 changed files with 377 additions and 160 deletions
+154 -62
View File
@@ -77,16 +77,19 @@ claim above therefore holds unqualified — no unanswered [Daniel]-class questio
anywhere in this plan, Phase Ε included — and no track in this plan is gated on a
decision.**
**Phase Ρ (added 2026-08-02) reopens the class with three forks, and NONE of them gates a
dispatch.** **Ρ-F1** (multi-track: inherit the existing refusal, or loop per selected
track), **Ρ-F2** (when Design is the active mode, does the new track go to Design as
Daniel's rule states, or always to Arrange), **Ρ-F3** (tail: follow the panel setting, or
force `None`). Each is stated with a recommendation and its counter-argument in the phase
header below and in `docs/product/render-in-place.md` §"Open forks". All three are
user-visible policy rather than implementation detail, and all three are answerable at
implementation review; the phase's single track is specced against the recommended answer
in each case, so a ruling that agrees changes nothing and a ruling that disagrees changes
one named paragraph.
**Phase Ρ (added 2026-08-02) opened three more [Daniel]-class forks and ALL THREE ARE
RULED**, same day (Daniel, 2026-08-02): **Ρ-F1** multi-track*"refuse"*, one track per
fire, a settled non-goal rather than a deferral; **Ρ-F2** the result track's mode —
*"for this action which is not a capture, the result track should always go to
arrange"*; **Ρ-F3** tail — *"follow panel tail settings."* The rulings are folded into
the track below and indexed at `docs/product/render-in-place.md` §"Rulings". **Ρ-F2
overrode the request's own original wording** ("stays in whatever mode was active") and
is the only one of the three that changed the spec: the result track is now an Arrange
member unconditionally, the A/B-on-the-bench behaviour mode-following would have enabled
is gone, and the ruling pulls in a two-line fix to the panel's auto-tag detector that
would otherwise reverse it on the next timer tick. **The plan-wide claim above therefore
still holds unqualified — no unanswered [Daniel]-class question remains anywhere in this
plan, Phase Ρ included — and no track in this plan is gated on a decision.**
**Ruling 3 (Daniel, 2026-08-01) — real units at the host boundary.** *"The parameter values
exposed to the VST host should be in real units, such that the host automation lanes report
@@ -2825,9 +2828,10 @@ properties under test are structural and a large payload proves nothing extra.
**Ships:** one bindable action that renders the selected track's output over the current
range to a file outside the bank, drops that file as an item on a brand-new sibling track
at the exact position it was rendered from, clones the source's colour and its name with an
idempotent `Capture ` prefix, and moves the source track into Design mode — where Design
View's existing park hides it, takes it out of the mix, and puts its FX offline. The bank is
never read, never written, and never notified.
idempotent `Capture ` prefix, moves the source track into Design mode — where Design
View's existing park hides it, takes it out of the mix, and puts its FX offline — and puts
the result track into Arrange, unconditionally. The bank is never read, never written, and
never notified.
**Consolidates: none of the seventeen.** Phase Ρ came from a direct request (Daniel,
2026-08-02) and is scoped in `docs/product/render-in-place.md`. Daniel's framing, verbatim
@@ -2836,7 +2840,10 @@ in substance: *similar to REAPER's "Render selected track time selection to new
stays/goes to design mode, and the resulting new sibling track — which gets the rendered
audio item placed correctly in the timeline — stays in whatever mode was active when the
action was run; the new track clones the source track colour and name with a Capture
prefix; this must NOT put the rendered audio into the ReaSampler banks/pool.* It
prefix; this must NOT put the rendered audio into the ReaSampler banks/pool.* **The
mode-following clause in that framing was superseded by Daniel's own Ρ-F2 ruling the same
day** — *"for this action which is not a capture, the result track should always go to
arrange"* — and the quote is kept verbatim only as the record of the request. It
**supersedes nothing** — a sweep of `docs/TODO.md` and `docs/TODO-1.0.md` for
`render.in.place|render to new track|preserve.source` returns nothing.
@@ -2897,10 +2904,15 @@ These bind the track in this phase, in addition to the plan-wide set above.
unit-tested before any DAW work; a render that lands the new track at the wrong nesting
level is audibly wrong in both directions (double-processed through a folder it re-enters,
or bypassing the folder bus entirely).
- **The new track is tagged BEFORE the mode reapply.** An untagged track is an Arrange
member by default, so a reapply that runs first parks the brand-new capture track when
Design is active. The auto-tag poller's later observation must be idempotent, not
load-bearing.
- **The Ρ-F2 ruling survives the auto-tag detector.** The result track and its placed
item are tagged `kArrangeModeId` **explicitly** (a membership record, not an `untag()`
to the Arrange default), and `panel_input::detectNewContent` drops added GUIDs that
already carry a record. Without that filter the detector tags every new track to the
active mode on its next tick and a Design-fired render silently becomes a Design
member — the ruling reversed inside a second. This criterion **replaces** the
tag-before-reapply ordering criterion the phase carried under mode-following: with the
result track an Arrange member unconditionally, a Design reapply parking it is the
correct outcome and the ordering is state hygiene, not behaviour.
- **One undo block, `UNDO_STATE_ALL`,** opened before the track is created and closed after
the mode reapply; the render sits outside it. `persistViewState` runs **after** the block
closes — it may raise a Save-As dialog, which must not sit inside an open undo block
@@ -2914,29 +2926,42 @@ idle fast path, the instrument's `process()`) is touched.
**Concurrency.** Phase Ρ is extension-side. Γ lives in `core/instrument/` +
`shell/instrument/`; Ε lands in the new `core/package/` + `shell/package/`. The pre-existing
files Ρ edits are named in its track's surface boundary and intersect neither.
files Ρ edits are named in its track's surface boundary and intersect neither — including
`shell/panel/panel_input.cpp`, which the Ρ-F2 ruling adds: no in-flight track in this plan
touches that file (Phase Ψ's two named regions in it, the footer block and the drag-arm
block, are both landed, and both are functions other than `detectNewContent`).
### Open forks — Daniel's. None gates a dispatch.
### Rulings — Daniel's, 2026-08-02. Nothing open.
The track below is specced against the **recommended** answer in each case, so a ruling that
agrees changes nothing and a ruling that disagrees changes one named paragraph. Full
statements with counter-arguments: `docs/product/render-in-place.md` §"Open forks".
All three forks this phase opened were ruled the day it was framed. Full statements, the
counter-arguments that made each a fork, and the one unexercised alternative:
`docs/product/render-in-place.md` §"Rulings".
- **Ρ-F1 — multi-track.** *Recommendation: inherit the existing refusal* (one track per
fire). The stem-collapse hazard would not actually apply to a per-track loop, so the
counter is real; what it costs is bookkeeping (indices shift per insertion, partial
failure, undo label). If Daniel wants it, the honest shape is a **second wave**, not a
bigger first one.
- **Ρ-F2 — the new track's mode when Design is active.** *Recommendation: to Design, as
Daniel's rule states.* The alternative is an absolute rule (source always → Design,
capture always → Arrange). The relative rule wins on Daniel's explicit words and on the
A/B-on-the-bench use the absolute rule cannot express; it does mean firing Ρ from Design
puts nothing into the arrangement.
- **Ρ-F3 — tail.** *Recommendation: follow the panel setting.* Under Auto/Manual the placed
item is longer than the window it replaces (correct for a decaying chain) and the
exact-bounds gate is inactive (it runs only under `TailMode::None`) — inherited, not
introduced. A third option is named in the product doc: keep the panel setting but run the
gate's start-alignment check regardless of tail mode.
- **Ρ-F1 — multi-track. RULED: refuse.** One selected track per fire, inherited from
`isMultiTrackStemRender`. A **settled non-goal**, in the same register as the other
entries under §"What Phase Ρ explicitly is NOT" — not deferred-with-a-plan. There is no
per-track loop planned, no second wave holding one, and no seam to leave half-open for
it. Matches the framing recommendation; nothing in the track changed.
- **Ρ-F2 — the result track's mode. RULED: always Arrange.** *"For this action which is
not a capture, the result track should always go to arrange."* **This overrode the
framing and the request's own original wording** — the result track no longer follows
the active mode. The source still goes to Design. Three consequences, all specced
below: the result track and its item are tagged `kArrangeModeId` explicitly; firing
from Design produces **no visible change** (the A/B-on-the-bench behaviour
mode-following would have enabled **does not exist** and must not be cited as a
benefit anywhere); and the panel's auto-tag detector needs a two-line
explicit-tag-wins filter, or it re-tags the result track to Design on its next timer
tick and silently reverses the ruling.
- **Ρ-F3 — tail. RULED: follow the panel tail settings.** Matches the framing
recommendation; nothing in the track changed. **Two consequences, accepted rather than
caveated:** under Auto/Manual the placed item is **longer than the window it replaces**
(correct for a decaying chain, wrong for a butt-joined section — the user's lever is
the panel's own tail setting), and the exact-bounds gate is **inactive** in those two
modes because it runs only under `TailMode::None`. Both are inherited from every other
capture path, not introduced here. The item's **start** is exact in all three modes, so
the null test holds in all three. The third option floated at framing (run the gate's
start-alignment check regardless of tail mode) is **not ruled in** and is recorded in
the product doc as an unexercised alternative.
---
@@ -2946,7 +2971,8 @@ statements with counter-arguments: `docs/product/render-in-place.md` §"Open for
**One track, deliberately.** The whole phase is roughly 350 lines: three small pure
additions to existing `core/capture` modules, one bounded edit to the offline backend, one
new shell TU, one `ActionTableRow`, two invariant amendments. Splitting it would create a
new shell TU, a two-line filter in `panel_input.cpp`, one `ActionTableRow`, four invariant
amendments. Splitting it would create a
merge dance across `core/capture` for no gain, and the pure half cannot be reviewed
meaningfully apart from the caller that gives it meaning. **Named contingency:** if the
sibling-placement arithmetic balloons in implementation, that function is the natural split
@@ -2956,7 +2982,8 @@ point — it is the only piece with zero dependency on anything else in the trac
**Goal.** One bindable action: render the selected track's output over the current range to
the project's recording path, place it on a new sibling track at the exact render position,
clone colour and name, move the source to Design and the new track to the active mode.
clone colour and name, move the source to Design and the result track to Arrange —
**always Arrange, whatever mode was active** (Ρ-F2).
**Spec:** `docs/product/render-in-place.md` — §"The render", §"Where the file goes",
§"Placement", §"The new track", §"Mode transitions", §"Undo", §"The action".
@@ -2974,7 +3001,11 @@ clone colour and name, move the source to Design and the new track to the active
`bankRelativeForName` already depends on that).
- **Edits, bounded:** `src/shell/capture/capture.{h,cpp}` — the `CaptureDestination` enum on
`CaptureRequest`, the ~6-line destination branch at path derivation, and
`CaptureResult::absolutePath`. `src/app/main.cpp` — one `ActionTableRow`.
`CaptureResult::absolutePath`. `src/shell/panel/panel_input.cpp` — two lines inside
`detectNewContent` only, dropping added GUIDs that already carry a membership record
(`MembershipIndex::query(guid) != nullptr`) before the `autoTagNewContent` call. That
edit exists **only because of the Ρ-F2 ruling**. `src/app/main.cpp` — one
`ActionTableRow`.
- **Does not own:** anything under `core/instrument/`, `shell/instrument/`, `core/package/`,
`shell/package/`, `core/model/`, `core/tracking/`, `core/reclaim/`, or `shell/persist/`.
No new directory. No new persisted state, no new ext-state key, no new wire version rung.
@@ -3022,17 +3053,41 @@ clone colour and name, move the source to Design and the new track to the active
placing path follows, because in the headline case the source is being parked out of sight
in the same gesture and restoring the selection would leave the user selecting an
invisible track.
- **Modes:** `membership().tag(sourceGuid, kDesignModeId)` (covers stays *and* goes — `tag`
replaces prior single-mode membership); `membership().tag(newTrackGuid,
view.activeModeId())`; then `mintManagedLanes(view, nullptr)`; then `applyMode(view,
- **Modes (Ρ-F2, RULED — absolute, not mode-following):**
`membership().tag(sourceGuid, kDesignModeId)` (covers stays *and* goes — `tag` replaces
prior single-mode membership); `membership().tag(newTrackGuid, kArrangeModeId)`
**`kArrangeModeId` unconditionally, never `view.activeModeId()`**; `membership().tag(…,
kArrangeModeId)` for **each item on the new track** (enumerate after `InsertMedia`; the
track is brand new so those are exactly the items just placed — `item_read::itemGuid` is
the existing GUID seam); then `mintManagedLanes(view, nullptr)`; then `applyMode(view,
view.activeModeId(), nullptr)` — a reapply, never a switch, so it is not transport-gated
and does not touch solo. **Tag order is load-bearing** (see acceptance criteria).
and does not touch solo.
- **Why explicit tags and not `untag()`:** an untagged GUID is an Arrange member by
behaviour but carries no membership record, and the record is what the auto-tag
detector's new filter keys on. Tag, don't untag. **This is the first explicit
`kArrangeModeId` record in the tree** — the shipped *tag selected tracks → Arrange*
action dispatches to `doUntag()`, i.e. Arrange-by-absence. The record is well-formed
and behaviourally identical (`isMember` answers the same for both states, `untag()`
still clears it); it costs one persisted entry. Unit-test the JSON round-trip;
`[verify — DAW]` that such a project shows no view-behaviour difference.
- **Fired from Design, the result track is parked and nothing is visible.** That is the
ruled behaviour, not a bug: the result track is an Arrange member, so a Design reapply
parks it, and it appears in the source's place on the next switch to Arrange. The
tag-before-reapply ordering that mode-following made load-bearing is now state hygiene
only.
- **Selection stays absolute too.** The result track is left selected, alone, in both
cases — even fired from Design, where that selects a parked track. Making selection
conditional on the active mode would reintroduce exactly the mode-relative behaviour
Ρ-F2 removed.
- **Inherited and deliberately not fought:** show-both on the source is not cleared (it is
the user's pin); a folder-parent source is not hidden by tagging, because parents are
derived (`core/view/CLAUDE.md`) — the existing tag action behaves identically. **Do not
invent a cascade that tags the children.**
- **Feedback:** silent on success (the new track is the feedback), `ShowConsoleMsg` on every
refusal — matching `RunInsertSelected`.
refusal — matching `RunInsertSelected`. **[propose at review]** whether the Design-fired
path breaks that silence with a one-line `ShowConsoleMsg` naming the track it created:
under Ρ-F2 that path produces no visible change, so a silent success is
indistinguishable from a no-op. Recommendation: yes, one line and one string.
- **Action:** suffix **`RENDER_TRACK_IN_PLACE`** — FOREVER-STABLE per channel, minted as a
new `RENDER_*` verb family rather than a `CAPTURE_*` member, deliberately: the id is
permanent and is the most durable statement the codebase makes about which pillar a
@@ -3055,6 +3110,13 @@ them). A track that lands Ρ without these reads as an invariant breach in revie
render that never enters the bank and never leaves it is a third verb outside the rule,
with the two-way boundary named. The prohibition must not be softened; the exception must
be named precisely.
4. `src/core/view/CLAUDE.md` §Invariants — *"New tracks are tagged to the active mode at
creation."* **Added by the Ρ-F2 ruling.** That rule now applies only to a GUID carrying
no membership record: an explicit tag wins over the detector. Amend the sentence and
state the reason in one clause — the detector classifies content the *user* made, not
content the tool made and already classified. `src/shell/panel/CLAUDE.md` describes
`panel_input` only as "the new-content auto-tag timer" and does not restate the rule, so
it needs no amendment.
**Acceptance criteria.**
- `siblingPlacement` is unit-tested over all five cases in the product doc's table (normal
@@ -3071,7 +3133,17 @@ them). A track that lands Ρ without these reads as an invariant breach in revie
boundary condition 1.
- The action registers, dispatches, and mirror-unregisters through the single
`ActionTableRow` — no separate registration mechanism.
- All three invariant amendments are in the diff.
- `render_in_place.cpp` contains no reference to `activeModeId` in the tagging path —
checkable by grep, and the review gate for the Ρ-F2 ruling. The result track and its
item are tagged `kArrangeModeId`; `activeModeId()` appears only in the `applyMode`
reapply.
- `detectNewContent` no longer auto-tags a GUID that already carries a membership record,
and the existing `view_mode_model` / `guid_diff` tests pass unchanged (the filter is
shell-side; `autoTagNewContent`'s pure contract does not move).
- A membership index carrying an explicit `kArrangeModeId` record JSON-round-trips
unchanged, and `isMember` answers identically for that record and for an absent one —
one added `view_mode_model` test, no DAW.
- All four invariant amendments are in the diff.
**DAW-verification obligation** (stated up front; nothing past the pure functions is
unit-testable):
@@ -3087,8 +3159,14 @@ unit-testable):
existing `[verify — DAW]` on mono-item-on-stereo-track summing, **promoted to
load-bearing by this phase**: Ρ is the first path where a collapsed render is placed into
the mix by the tool itself.
- **Both mode transitions** (fired from Arrange; fired from Design), plus the ordering check
that the new track is never momentarily parked.
- **Both mode transitions under the Ρ-F2 ruling** fired from Arrange (source parks;
result track visible and in the mix) and fired from Design (source stays on the bench;
result track parked, then present in the source's place after a switch to Arrange).
**In each case wait out at least one panel timer tick and re-check the membership**
that is the auto-tag-detector regression, and it is what catches a missing
explicit-tag-wins filter or an untagged item, either of which silently reverses the
ruling. The old ordering check (that the new track is never momentarily parked) no
longer applies.
- **Undo** — one Ctrl-Z removes track and item and reverts the folder-depth write; the file
survives (REAPER's undo deletes no files, and prune cannot reach this one); the source
stays tagged Design, whose way out is the existing *tag selected tracks → Arrange* action.
@@ -3099,7 +3177,10 @@ unit-testable):
- **`GetProjectPathEx` against a project with a non-default recording path**, confirming the
render lands where the project's media lives.
**Open questions.** The three [Daniel] forks above (Ρ-F1/F2/F3), none gating. **[propose at
**Open questions.** **No [Daniel] questions — Ρ-F1, Ρ-F2 and Ρ-F3 are all RULED**
(2026-08-02), so nothing here is gated. Two **[propose at review]** items, both
recommendation-carrying. First, whether the Design-fired path emits a one-line
`ShowConsoleMsg` (see the Feedback bullet) — recommendation: yes. Second, **[propose at
review]** whether `render_in_place` should refuse when the source track is the master —
`ResolveScopeSource` collects via `CountSelectedTracks`/`GetSelectedTrack`, which skip the
master (SDK header), so a master-only selection already resolves to zero tracks and refuses
@@ -3181,8 +3262,11 @@ proof it exists to give.
and `docs/TODO-1.0.md` for `render.in.place|render to new track|preserve.source` returns
nothing. The smallest phase in this plan, deliberately: it is a thin third verb composed
out of machinery that already exists, and the burden was on any new machinery to prove it
unavoidable. Its three [Daniel]-class forks (Ρ-F1/F2/F3) are **open**, and none gates the
dispatch — the track is specced against the recommended answer in each case.
unavoidable. Its three [Daniel]-class forks (Ρ-F1/F2/F3) were opened and ruled the same
day it was framed, so the track here is not gated on a decision; see "Decision state"
above. **Ρ-F2 was the one that moved the spec** — the result track goes to Arrange
unconditionally rather than following the active mode, which is also the only reason the
phase touches `panel_input.cpp` at all.
### Deliberate compressions
@@ -3328,10 +3412,11 @@ Phase Rho — Render in place (none of the seventeen; a direct reque
W1 The verb [ONE track, deliberately]
T1 render-in-place ............. render selected track -> new sibling track,
item placed at the exact render position,
colour + "Capture " name cloned, source -> Design.
NEVER touches the bank.
[OPEN, none gating: R-F1 multi-track, R-F2 the
new track's mode from Design, R-F3 tail]
colour + "Capture " name cloned, source -> Design,
result track -> ARRANGE always. Never the bank.
[R-F1 RULED: refuse multi-track, settled non-goal.
R-F2 RULED: result track ALWAYS Arrange, never
mode-following. R-F3 RULED: follow panel tail]
The THIRD VERB. Capture = arrange -> bank (writes it). Placement = bank -> arrange
(reads it). Render in place = arrange -> arrange (bank on neither side). That is why the
@@ -3343,10 +3428,17 @@ Phase Rho — Render in place (none of the seventeen; a direct reque
Genuinely new: ONE pure sibling-placement function (folder-parent and last-in-folder are
both audibly wrong if inserted at srcIdx+1), an idempotent "Capture " prefix, a
deriveRenderPaths sibling, and CaptureResult::absolutePath. Everything else composes.
Three invariant amendments are track deliverables (the Psi precedent): shell/capture and
shell/actions CLAUDE.md placing-path claims, plus ONE sentence in root CLAUDE.md naming
the third verb without softening the prohibition.
Shared files, named: capture.{h,cpp} (destination enum + result field) and main.cpp (one
ActionTableRow) — the only pre-existing shell files touched. Disjoint from Gamma
(core+shell/instrument) and Epsilon (core+shell/package).
R-F2 fallout, the ruling's only cost: the result track AND its item are tagged
kArrangeModeId explicitly, and detectNewContent must drop added GUIDs that already carry
a membership record — the auto-tag detector otherwise re-tags a Design-fired result to
Design on its next tick and reverses the ruling. Firing from Design is therefore
deliberately invisible: the render waits in Arrange. No A/B-on-the-bench behaviour exists.
Four invariant amendments are track deliverables (the Psi precedent): shell/capture and
shell/actions CLAUDE.md placing-path claims, ONE sentence in root CLAUDE.md naming
the third verb without softening the prohibition, and core/view CLAUDE.md's "new tracks
are tagged to the active mode" made conditional on carrying no membership record.
Shared files, named: capture.{h,cpp} (destination enum + result field), main.cpp (one
ActionTableRow), and panel_input.cpp (detectNewContent only; Psi's two named regions in
that file are landed and are other functions) — the only pre-existing shell files
touched. Disjoint from Gamma (core+shell/instrument) and Epsilon (core+shell/package).
```