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 anywhere in this plan, Phase Ε included — and no track in this plan is gated on a
decision.** decision.**
**Phase Ρ (added 2026-08-02) reopens the class with three forks, and NONE of them gates a **Phase Ρ (added 2026-08-02) opened three more [Daniel]-class forks and ALL THREE ARE
dispatch.** **Ρ-F1** (multi-track: inherit the existing refusal, or loop per selected RULED**, same day (Daniel, 2026-08-02): **Ρ-F1** multi-track*"refuse"*, one track per
track), **Ρ-F2** (when Design is the active mode, does the new track go to Design as fire, a settled non-goal rather than a deferral; **Ρ-F2** the result track's mode —
Daniel's rule states, or always to Arrange), **Ρ-F3** (tail: follow the panel setting, or *"for this action which is not a capture, the result track should always go to
force `None`). Each is stated with a recommendation and its counter-argument in the phase arrange"*; **Ρ-F3** tail — *"follow panel tail settings."* The rulings are folded into
header below and in `docs/product/render-in-place.md` §"Open forks". All three are the track below and indexed at `docs/product/render-in-place.md` §"Rulings". **Ρ-F2
user-visible policy rather than implementation detail, and all three are answerable at overrode the request's own original wording** ("stays in whatever mode was active") and
implementation review; the phase's single track is specced against the recommended answer is the only one of the three that changed the spec: the result track is now an Arrange
in each case, so a ruling that agrees changes nothing and a ruling that disagrees changes member unconditionally, the A/B-on-the-bench behaviour mode-following would have enabled
one named paragraph. 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 **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 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 **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 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 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 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. The bank is View's existing park hides it, takes it out of the mix, and puts its FX offline — and puts
never read, never written, and never notified. 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, **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 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 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 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 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 **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. `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 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, level is audibly wrong in both directions (double-processed through a folder it re-enters,
or bypassing the folder bus entirely). or bypassing the folder bus entirely).
- **The new track is tagged BEFORE the mode reapply.** An untagged track is an Arrange - **The Ρ-F2 ruling survives the auto-tag detector.** The result track and its placed
member by default, so a reapply that runs first parks the brand-new capture track when item are tagged `kArrangeModeId` **explicitly** (a membership record, not an `untag()`
Design is active. The auto-tag poller's later observation must be idempotent, not to the Arrange default), and `panel_input::detectNewContent` drops added GUIDs that
load-bearing. 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 - **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 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 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/` + **Concurrency.** Phase Ρ is extension-side. Γ lives in `core/instrument/` +
`shell/instrument/`; Ε lands in the new `core/package/` + `shell/package/`. The pre-existing `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 All three forks this phase opened were ruled the day it was framed. Full statements, the
agrees changes nothing and a ruling that disagrees changes one named paragraph. Full counter-arguments that made each a fork, and the one unexercised alternative:
statements with counter-arguments: `docs/product/render-in-place.md` §"Open forks". `docs/product/render-in-place.md` §"Rulings".
- **Ρ-F1 — multi-track.** *Recommendation: inherit the existing refusal* (one track per - **Ρ-F1 — multi-track. RULED: refuse.** One selected track per fire, inherited from
fire). The stem-collapse hazard would not actually apply to a per-track loop, so the `isMultiTrackStemRender`. A **settled non-goal**, in the same register as the other
counter is real; what it costs is bookkeeping (indices shift per insertion, partial entries under §"What Phase Ρ explicitly is NOT" — not deferred-with-a-plan. There is no
failure, undo label). If Daniel wants it, the honest shape is a **second wave**, not a per-track loop planned, no second wave holding one, and no seam to leave half-open for
bigger first one. it. Matches the framing recommendation; nothing in the track changed.
- **Ρ-F2 — the new track's mode when Design is active.** *Recommendation: to Design, as - **Ρ-F2 — the result track's mode. RULED: always Arrange.** *"For this action which is
Daniel's rule states.* The alternative is an absolute rule (source always → Design, not a capture, the result track should always go to arrange."* **This overrode the
capture always → Arrange). The relative rule wins on Daniel's explicit words and on the framing and the request's own original wording** — the result track no longer follows
A/B-on-the-bench use the absolute rule cannot express; it does mean firing Ρ from Design the active mode. The source still goes to Design. Three consequences, all specced
puts nothing into the arrangement. below: the result track and its item are tagged `kArrangeModeId` explicitly; firing
- **Ρ-F3 — tail.** *Recommendation: follow the panel setting.* Under Auto/Manual the placed from Design produces **no visible change** (the A/B-on-the-bench behaviour
item is longer than the window it replaces (correct for a decaying chain) and the mode-following would have enabled **does not exist** and must not be cited as a
exact-bounds gate is inactive (it runs only under `TailMode::None`) — inherited, not benefit anywhere); and the panel's auto-tag detector needs a two-line
introduced. A third option is named in the product doc: keep the panel setting but run the explicit-tag-wins filter, or it re-tags the result track to Design on its next timer
gate's start-alignment check regardless of tail mode. 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 **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 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 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 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 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 **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, 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", **Spec:** `docs/product/render-in-place.md` — §"The render", §"Where the file goes",
§"Placement", §"The new track", §"Mode transitions", §"Undo", §"The action". §"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). `bankRelativeForName` already depends on that).
- **Edits, bounded:** `src/shell/capture/capture.{h,cpp}` — the `CaptureDestination` enum on - **Edits, bounded:** `src/shell/capture/capture.{h,cpp}` — the `CaptureDestination` enum on
`CaptureRequest`, the ~6-line destination branch at path derivation, and `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/`, - **Does not own:** anything under `core/instrument/`, `shell/instrument/`, `core/package/`,
`shell/package/`, `core/model/`, `core/tracking/`, `core/reclaim/`, or `shell/persist/`. `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. 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 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 in the same gesture and restoring the selection would leave the user selecting an
invisible track. invisible track.
- **Modes:** `membership().tag(sourceGuid, kDesignModeId)` (covers stays *and* goes — `tag` - **Modes (Ρ-F2, RULED — absolute, not mode-following):**
replaces prior single-mode membership); `membership().tag(newTrackGuid, `membership().tag(sourceGuid, kDesignModeId)` (covers stays *and* goes — `tag` replaces
view.activeModeId())`; then `mintManagedLanes(view, nullptr)`; then `applyMode(view, 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 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 - **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 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 derived (`core/view/CLAUDE.md`) — the existing tag action behaves identically. **Do not
invent a cascade that tags the children.** invent a cascade that tags the children.**
- **Feedback:** silent on success (the new track is the feedback), `ShowConsoleMsg` on every - **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 - **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 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 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, 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 with the two-way boundary named. The prohibition must not be softened; the exception must
be named precisely. 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.** **Acceptance criteria.**
- `siblingPlacement` is unit-tested over all five cases in the product doc's table (normal - `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. boundary condition 1.
- The action registers, dispatches, and mirror-unregisters through the single - The action registers, dispatches, and mirror-unregisters through the single
`ActionTableRow` — no separate registration mechanism. `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 **DAW-verification obligation** (stated up front; nothing past the pure functions is
unit-testable): unit-testable):
@@ -3087,8 +3159,14 @@ unit-testable):
existing `[verify — DAW]` on mono-item-on-stereo-track summing, **promoted to 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 load-bearing by this phase**: Ρ is the first path where a collapsed render is placed into
the mix by the tool itself. the mix by the tool itself.
- **Both mode transitions** (fired from Arrange; fired from Design), plus the ordering check - **Both mode transitions under the Ρ-F2 ruling** fired from Arrange (source parks;
that the new track is never momentarily parked. 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 - **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 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. 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 - **`GetProjectPathEx` against a project with a non-default recording path**, confirming the
render lands where the project's media lives. 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 — review]** whether `render_in_place` should refuse when the source track is the master —
`ResolveScopeSource` collects via `CountSelectedTracks`/`GetSelectedTrack`, which skip the `ResolveScopeSource` collects via `CountSelectedTracks`/`GetSelectedTrack`, which skip the
master (SDK header), so a master-only selection already resolves to zero tracks and refuses 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 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 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 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 unavoidable. Its three [Daniel]-class forks (Ρ-F1/F2/F3) were opened and ruled the same
dispatch — the track is specced against the recommended answer in each case. 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 ### Deliberate compressions
@@ -3328,10 +3412,11 @@ Phase Rho — Render in place (none of the seventeen; a direct reque
W1 The verb [ONE track, deliberately] W1 The verb [ONE track, deliberately]
T1 render-in-place ............. render selected track -> new sibling track, T1 render-in-place ............. render selected track -> new sibling track,
item placed at the exact render position, item placed at the exact render position,
colour + "Capture " name cloned, source -> Design. colour + "Capture " name cloned, source -> Design,
NEVER touches the bank. result track -> ARRANGE always. Never the bank.
[OPEN, none gating: R-F1 multi-track, R-F2 the [R-F1 RULED: refuse multi-track, settled non-goal.
new track's mode from Design, R-F3 tail] 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 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 (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 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 both audibly wrong if inserted at srcIdx+1), an idempotent "Capture " prefix, a
deriveRenderPaths sibling, and CaptureResult::absolutePath. Everything else composes. deriveRenderPaths sibling, and CaptureResult::absolutePath. Everything else composes.
Three invariant amendments are track deliverables (the Psi precedent): shell/capture and R-F2 fallout, the ruling's only cost: the result track AND its item are tagged
shell/actions CLAUDE.md placing-path claims, plus ONE sentence in root CLAUDE.md naming kArrangeModeId explicitly, and detectNewContent must drop added GUIDs that already carry
the third verb without softening the prohibition. a membership record — the auto-tag detector otherwise re-tags a Design-fired result to
Shared files, named: capture.{h,cpp} (destination enum + result field) and main.cpp (one Design on its next tick and reverses the ruling. Firing from Design is therefore
ActionTableRow) — the only pre-existing shell files touched. Disjoint from Gamma deliberately invisible: the render waits in Arrange. No A/B-on-the-bench behaviour exists.
(core+shell/instrument) and Epsilon (core+shell/package). 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).
``` ```
+223 -98
View File
@@ -9,12 +9,20 @@ the capture/placement separation, the prior art it borrows from, the reuse inven
that makes it small, and the handful of decisions the shape actually turns on. that makes it small, and the handful of decisions the shape actually turns on.
Status: framed by product-designer (2026-08-02) from Daniel's direct request the same Status: framed by product-designer (2026-08-02) from Daniel's direct request the same
day. **Three [Daniel]-class forks are open**Ρ-F1 (multi-track), Ρ-F2 (the new day; **all three [Daniel]-class forks RULED the same day**Ρ-F1 **refuse a
track's mode when Design is active), Ρ-F3 (tail) — each stated with a recommendation multi-track selection**, Ρ-F2 **the result track always goes to Arrange**, Ρ-F3
in §"Open forks". None of them blocks a dispatch; all three are answerable at **follow the panel tail setting**. See §"Rulings" for the index and the recorded
implementation review if Daniel prefers, but all three are user-visible policy rather rationale; each is specified in place in the section that owns it. Nothing in this
than implementation detail. Everything else below is a product-designer call with its doc is open.
reasoning stated; contradict it in review with an argument, not a preference.
**Ρ-F2 overrode the framing, including Daniel's own original phrasing.** The request
as first stated had the new track take whatever mode was active when the action fired;
the ruling replaced that with an absolute rule. §"Mode transitions" is written against
the ruling, and the A/B-on-the-bench behaviour that mode-following would have produced
**no longer exists anywhere in this design** — do not reintroduce it as a benefit.
Everything else below is a product-designer call with its reasoning stated; contradict
it in review with an argument, not a preference.
--- ---
@@ -23,8 +31,9 @@ reasoning stated; contradict it in review with an argument, not a preference.
**Render in place takes one selected track, renders its output over the current **Render in place takes one selected track, renders its output over the current
range to a file, and drops that file as an item on a brand-new sibling track at the range to a file, and drops that file as an item on a brand-new sibling track at the
exact position it was rendered from — then moves the source track into Design mode.** exact position it was rendered from — then moves the source track into Design mode.**
The new track inherits the source's colour and its name with a `Capture ` prefix. The The new track inherits the source's colour and its name with a `Capture ` prefix, and
bank is never opened, never read, never written. belongs to Arrange unconditionally (Ρ-F2). The bank is never opened, never read, never
written.
The model Daniel named is REAPER's own *Render selected track time selection to new The model Daniel named is REAPER's own *Render selected track time selection to new
track (stereo) and mute original*. Phase Ρ differs in exactly one respect, and that track (stereo) and mute original*. Phase Ρ differs in exactly one respect, and that
@@ -235,19 +244,46 @@ different and much less predictable verb.
render *source* (`SelectedTracks`, which track scope always uses), so any selection of render *source* (`SelectedTracks`, which track scope always uses), so any selection of
more than one track refuses with `multiTrackRefusalMessage(CaptureScope::Track)` more than one track refuses with `multiTrackRefusalMessage(CaptureScope::Track)`
before a single project setting is written. Ρ inherits this for free and adds no before a single project setting is written. Ρ inherits this for free and adds no
check of its own. See fork Ρ-F1 for the alternative. check of its own.
**Tail follows the panel setting** — None / Auto / Manual, read from **Ρ-F1, RULED by Daniel, 2026-08-02: *refuse*.** One selected track per fire is a
`bankPanelTailSetting()` like every other capture path. Under Auto or Manual the **settled non-goal**, in the same sense as the other entries in §"What Phase Ρ
rendered file is longer than the requested window by design (the chain's decay rings explicitly is NOT" — not a deferral. There is no per-track loop planned, no second
past the range end), so the placed item is correspondingly longer than the source wave holding one, and no seam to be left half-open on the way past. If multi-track is
window. That is the musically correct answer for a design chain with reverb on it, ever wanted it is a new phase with its own framing, and the counter-argument that made
and forcing None would be Ρ inventing a policy the rest of the tool does not have. this a fork (the stem-collapse hazard does not apply to a per-track loop, so the
Two consequences to state rather than discover: the exact-bounds gate in refusal is inherited rather than required) is recorded in §"Rulings" so it does not
`OfflineRenderBackend::capture` runs **only** under `TailMode::None`, so Auto/Manual have to be rediscovered.
renders are unguarded against widening (inherited, not introduced); and the placed
item's *start* is exact in every mode, because a tail is added at the end only. See **Tail follows the panel setting — Ρ-F3, RULED by Daniel, 2026-08-02: *"follow panel
fork Ρ-F3. tail settings."*** None / Auto / Manual, read from `bankPanelTailSetting()` like every
other capture path. Forcing `None` would be Ρ inventing a policy the rest of the tool
does not have, and a decaying design chain wants its tail when its source is about to
be silenced.
**Two consequences of that ruling, both accepted, both stated here because an
implementer meets them directly:**
1. **Under Auto or Manual the placed item is longer than the window it replaces.** The
chain's decay rings past the range end and the render carries it, so the item on
the result track extends past the source range. This is correct for reverb and
wrong for a section meant to butt against the next one — and the ruling accepts
that trade rather than splitting the behaviour. The user's lever is the panel's own
tail setting: set it to None before firing if a hard edge is wanted.
2. **The exact-bounds gate is inactive under Auto and Manual.** The gate in
`OfflineRenderBackend::capture` runs **only** under `TailMode::None`, so Ρ renders
in the other two modes are unguarded against a widened render. This is inherited
from every other capture path, not introduced by Ρ, and it is not a caveat to bury:
the phase's exactness guarantee under Auto/Manual is the *start* only.
The placed item's **start is exact in every tail mode**, because a tail is only ever
added at the end. The null test therefore holds in all three modes — the shared region
nulls; the tail simply has nothing to null against.
A third option was floated at framing and is **not ruled in**: keep the panel setting
but run the bounds gate's start-alignment check regardless of tail mode, since a tail
only ever extends the end. It is recorded as an unexercised alternative in §"Rulings"
and is not to be built into this phase.
**Mono collapse applies**, unchanged. A render whose channels are bit-identical **Mono collapse applies**, unchanged. A render whose channels are bit-identical
collapses losslessly to one channel and REAPER derives a mono item from the file collapses losslessly to one channel and REAPER derives a mono item from the file
@@ -313,6 +349,14 @@ sight in the same gesture, so restoring the selection would leave the user selec
an invisible track. The new track is the workflow's next subject; select it. The edit an invisible track. The new track is the workflow's next subject; select it. The edit
cursor *is* restored, since nothing about Ρ argues for moving it. cursor *is* restored, since nothing about Ρ argues for moving it.
**The Ρ-F2 ruling inverts that reasoning in the Design-fired case, and the rule stays
absolute anyway.** Fired from Design, the result track is the parked one and the source
is the visible one, so leaving the result selected selects an invisible track — the
exact thing the rule exists to avoid in the other direction. Making the selection
conditional on the active mode would reintroduce mode-relative behaviour, which is
precisely what the ruling removed from this action. One rule, both cases: the result
track ends up selected, alone.
--- ---
## The new track — index, folder, colour, name ## The new track — index, folder, colour, name
@@ -406,11 +450,14 @@ unlike `kManagedLanePrefix` or an action-id suffix, changing it later strands no
--- ---
## Mode transitions — resolving "stays/goes" ## Mode transitions — the source parks, the result goes to Arrange
Daniel's phrasing was *"the source track stays/goes to design mode, and the resulting Daniel's original phrasing was *"the source track stays/goes to design mode, and the
new sibling track […] stays in whatever mode was active when the action was run."* resulting new sibling track […] stays in whatever mode was active when the action was
Both halves resolve into the shipped membership model without inventing anything. run."* **The second half was overridden by his own later ruling — Ρ-F2, 2026-08-02:
*"for this action which is not a capture, the result track should always go to
arrange."*** The source half stands exactly as first stated; the result half is now
absolute and mode-independent.
**Source track: unconditionally a Design member afterwards.** **Source track: unconditionally a Design member afterwards.**
`membership().tag(sourceGuid, kDesignModeId)` covers both readings in one call — `membership().tag(sourceGuid, kDesignModeId)` covers both readings in one call —
@@ -433,29 +480,91 @@ Two inherited behaviours to state rather than fight:
inherits it. Do **not** invent a cascade that tags the children — that changes the inherits it. Do **not** invent a cascade that tags the children — that changes the
membership model to make one feature convenient. membership model to make one feature convenient.
**New track: tagged to the mode that was active when the action fired**, **Result track: unconditionally an Arrange member**, whatever mode was active
synchronously and explicitly — `membership().tag(newTrackGuid, view.activeModeId())`. `membership().tag(newTrackGuid, kArrangeModeId)`, synchronously and explicitly, **not**
an `untag()` to the Arrange default. The distinction is load-bearing: an explicit
membership record is what protects the ruling from the auto-tag detector (below). An
untagged track is an Arrange member by observable behaviour but carries no record, and
a record is what the detector must be made to respect.
**This must happen before `applyMode` reapplies, and that ordering is load-bearing.** **Note what is novel here, because it is worth a reviewer's attention.** Ρ is the
The auto-tag poller (`panel_input::detectNewContent`, over `guid_diff::GuidBaseline`) **first** path in the tree to write an explicit `kArrangeModeId` record. The shipped
would tag the new track on its next tick, and would reach the same answer — but the *tag selected tracks → Arrange* action does not — it dispatches to `doUntag()`, i.e.
reapply inside Ρ's own gesture runs first. An untagged track is an Arrange member by Arrange-by-absence, which is why the constant appears in `view_mode_model` and
default, so if the active mode is Design and Ρ reapplies before tagging, the reapply `mode_enable` but never in a `tag()` call. The record is nonetheless well-formed and
**parks the brand-new capture track** — hidden, out of the mix — and it stays parked behaviourally identical to the untagged state everywhere that matters:
until the next mode switch. Tagging explicitly, first, closes that window; the `ViewModeModel::isMember` answers `true` for `arrange` and `false` for `design` in
poller's later observation is then idempotent (it re-derives the same mode). both cases, the derived-parent and lane rules read the same, and `untag()` still
returns it to absence. The only differences are one more entry in the persisted
membership index and — the point — its visibility to the detector's filter. **[verify
— DAW]** that a project saved with an explicit Arrange record round-trips and shows no
behavioural difference from an untagged track; the JSON round-trip is unit-testable and
should be tested, but the live view behaviour is not.
**The placed item is tagged Arrange too**, explicitly, for the same reason. Item
membership is a separate index from track membership and it is what drives lane
minting; leaving the item untagged hands it to the detector, which tags a new item to
the active mode whenever its track has no pre-existing single-mode content — and a
brand-new track never does. After `InsertMedia`, enumerate the new track's items (it
is brand new, so they are exactly the ones just placed) and tag each;
`shell/capture/item_read::itemGuid` is the existing GUID seam.
### The one hazard the ruling creates — the auto-tag detector
The ruling **dissolves** the ordering hazard the mode-following rule carried and
**creates a different one** in its place. Both are stated, because the first was
written into the plan's acceptance criteria and is now wrong.
**Dissolved.** Under mode-following, the tag had to precede the `applyMode` reapply:
an untagged track is an Arrange member by default, so a Design reapply running first
would have parked a track destined for Design, and it would have stayed parked until
the next switch. Under the ruling the result track *is* an Arrange member, so a Design
reapply parking it is the correct outcome and the ordering no longer changes what the
user sees. Tag first anyway, for state hygiene — it is simply no longer load-bearing.
**Created, and it is a defect rather than a nuance.** The panel's new-content detector
(`panel_input::detectNewContent` over `guid_diff::GuidBaseline`, feeding
`view_mode_model::autoTagNewContent`) tags **every new track to the active mode**,
unconditionally: it diffs live GUIDs against the previous tick and never consults the
membership index. Fire Ρ while Design is active and, on the next timer tick, the
detector re-tags the brand-new result track — and its item — from Arrange to Design,
silently reversing the ruling inside a second. Nothing in Ρ's own gesture can outrun
it, because it runs after.
**The fix is one rule, and it is worth stating generally: an explicit tag wins over
the detector.** `detectNewContent` drops any added GUID that already carries a
membership record (`MembershipIndex::query(guid) != nullptr`) before building its
auto-tag input. Two lines, at the point in that function where the model is already in
hand. The rule is right beyond Ρ — the detector exists to classify content the *user*
made, not content the tool made and has already classified — and it is a strict
improvement on an existing rough edge: a track brought back by undo keeps its original
mode instead of being re-tagged to whatever mode happens to be active at the time.
Two alternatives were considered and rejected. Re-arming the baseline after Ρ's
gesture (the `bankPanelNotifyProjectLoaded` mechanism) works, but it absorbs an entire
tick of genuinely new content silently — a blunt instrument aimed at two GUIDs.
Changing `autoTagNewContent`'s own track rule changes Design View's behaviour for
every caller in order to serve one.
The resulting behaviour, stated completely: The resulting behaviour, stated completely:
| Active mode when fired | Source afterwards | New track afterwards | What the user sees | | Active mode when fired | Source afterwards | Result track afterwards | What the user sees |
|---|---|---|---| |---|---|---|---|
| **Arrange** | Design — parked, hidden, FX offline | Arrange — visible, in the mix | The headline case. The design chain vanishes from the arrangement and its audio takes its place, at the same position, same colour, named after it. | | **Arrange** | Design — parked, hidden, FX offline | Arrange — visible, in the mix | The headline case. The design chain vanishes from the arrangement and its audio takes its place, at the same position, same colour, named after it. |
| **Design** | Design — visible | Design — visible | Both on the bench, side by side, for A/B. Neither reaches Arrange. | | **Design** | Design — visible on the bench | Arrange — parked while Design is active | The bench keeps only the source. The render is not on screen; it is waiting in the arrangement, and switching to Arrange shows it in the source's place. |
The Design-active case is coherent — you are iterating on the bench and want the **The Design-fired case produces no visible change, and that is the ruling's accepted
render beside its source — but it means firing Ρ from Design never puts anything into cost.** Fire Ρ from the bench and the immediate feedback is that nothing appears. The
the arrangement. That follows directly from Daniel's stated rule and is a genuinely price buys the action meaning exactly one thing wherever it is fired — *commit this
useful second behaviour, not a defect. It is also the subject of fork Ρ-F2. design work into the arrangement* — which is the reasoning behind the ruling: Ρ is not
a capture, and a capture's mode-relative habits do not apply to it.
**Feedback deserves reconsideration for this case, and only this case.** Ρ is
otherwise silent on success because the new track is the feedback; fired from Design
there is no visible new track, so a silent success is indistinguishable from a no-op.
**[propose at review]** whether the Design-fired path should emit a one-line
`ShowConsoleMsg` naming the track it created. Recommendation: yes — it costs one line
and one string, and it is the only place in the phase where success is invisible.
**Lane minting runs**, via `mintManagedLanes(view, nullptr)` before the reapply, on **Lane minting runs**, via `mintManagedLanes(view, nullptr)` before the reapply, on
the same path `doMoveItems` already uses — so a track that ends up carrying content the same path `doMoveItems` already uses — so a track that ends up carrying content
@@ -535,8 +644,12 @@ are named:
- **Not a bank capture, in any form.** No index entry, no ledger record, no - **Not a bank capture, in any form.** No index entry, no ledger record, no
generation bump, no instance reload. generation bump, no instance reload.
- **Not multi-track** (this phase — see Ρ-F1). One selected track per fire; more than - **Not multi-track.** Ρ-F1, RULED: one selected track per fire; more than one refuses
one refuses. with the message that already exists. Settled, not deferred — no per-track loop is
planned, half-built toward, or left a seam for.
- **Not mode-following.** Ρ-F2, RULED: the result track is an Arrange member
unconditionally. There is no variant that follows the active mode, no per-fire
choice, and no bench-visible result — firing from Design puts nothing on the bench.
- **Not item-scoped.** No `RENDER_ITEMS_IN_PLACE`, no item-extent range fallback. The - **Not item-scoped.** No `RENDER_ITEMS_IN_PLACE`, no item-extent range fallback. The
seam is left open by the id family; the feature is not built. seam is left open by the id family; the feature is not built.
- **Not a tempo-conforming insert.** No conform variant, ever — a conform would - **Not a tempo-conforming insert.** No conform variant, ever — a conform would
@@ -549,13 +662,14 @@ are named:
- **Not a new persisted state.** Membership writes go into the existing `"reasampler"` - **Not a new persisted state.** Membership writes go into the existing `"reasampler"`
view section. Ρ adds no key, no version rung, no wire format. view section. Ρ adds no key, no version rung, no wire format.
- **Not a new directory.** Three small pure additions to existing `core/capture` - **Not a new directory.** Three small pure additions to existing `core/capture`
modules, one new shell TU in `shell/capture`, one row in `main.cpp`. modules, one new shell TU in `shell/capture`, two lines in `panel_input.cpp`, one
row in `main.cpp`.
--- ---
## Invariant amendments this phase owns ## Invariant amendments this phase owns
Two statements in the tree become false the moment Ρ lands, and amending them is a Three statements in the tree become false the moment Ρ lands, and amending them is a
**deliverable of the track**, not a follow-up — the precedent is Phase Ψ, where three **deliverable of the track**, not a follow-up — the precedent is Phase Ψ, where three
such amendments were carried as acceptance criteria of the tracks that broke them. A such amendments were carried as acceptance criteria of the tracks that broke them. A
track that lands Ρ without these reads as an invariant breach in review. track that lands Ρ without these reads as an invariant breach in review.
@@ -572,6 +686,15 @@ track that lands Ρ without these reads as an invariant breach in review.
lives in `shell/capture/`, but the sentence reads as a claim about the system. lives in `shell/capture/`, but the sentence reads as a claim about the system.
Amend it to be explicit that it scopes to *this directory*, and cross-reference the Amend it to be explicit that it scopes to *this directory*, and cross-reference the
third verb. third verb.
3. **`src/core/view/CLAUDE.md` §Invariants** — *"New tracks are tagged to the active
mode at creation."* Ρ-F2 makes this conditional for the first time: the detector's
active-mode rule now applies only to a GUID that carries **no** membership record,
because an explicit tag wins over it (§"Mode transitions"). Amend the sentence to
say so, 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/`'s
own CLAUDE.md describes `panel_input` as "the new-content auto-tag timer" without
restating the rule, so it needs no amendment; if that changes, the rule has one
home and this is it.
Root `CLAUDE.md` §"The load-bearing principle" should gain **one sentence**, not a Root `CLAUDE.md` §"The load-bearing principle" should gain **one sentence**, not a
rewrite: that a render which never enters the bank and never leaves it is a third rewrite: that a render which never enters the bank and never leaves it is a third
@@ -605,6 +728,11 @@ directory:
derivation) and `CaptureResult::absolutePath`. **No behavioural change on the bank derivation) and `CaptureResult::absolutePath`. **No behavioural change on the bank
path**: the enum defaults to `Bank`, and the bank branch must be byte-identical to path**: the enum defaults to `Bank`, and the bank branch must be byte-identical to
today. today.
- `shell/panel/panel_input.cpp` — two lines inside `detectNewContent`: drop added
GUIDs that already carry a membership record, so an explicit tag wins over the
auto-tag detector (§"Mode transitions"). This edit exists **only because of the Ρ-F2
ruling**; without it the ruling reverses itself on the next timer tick. No other
function in the file is touched.
- `src/app/main.cpp` — one `ActionTableRow`. - `src/app/main.cpp` — one `ActionTableRow`.
**Performance posture:** every surface is cold — one gesture, once. None of the named **Performance posture:** every surface is cold — one gesture, once. None of the named
@@ -633,9 +761,15 @@ than a discovery. Nothing in Ρ is unit-testable past the pure functions.
mono, and confirm it sums at the same level as the stereo source did. This is root mono, and confirm it sums at the same level as the stereo source did. This is root
`CLAUDE.md`'s existing `[verify — DAW]` on mono-item-on-stereo-track summing, `CLAUDE.md`'s existing `[verify — DAW]` on mono-item-on-stereo-track summing,
promoted to load-bearing by Ρ. promoted to load-bearing by Ρ.
- **Both mode transitions** — fired from Arrange (source parks, new track visible) and - **Both mode transitions** — fired from Arrange (source parks; result track visible
fired from Design (both visible, neither in Arrange) — plus the ordering check that and in the mix) and fired from Design (source stays on the bench; result track
the new track is never momentarily parked. parked, then present in the source's place after switching 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 the check that catches a missing
explicit-tag-wins filter or an untagged item — either of which silently reverses the
Ρ-F2 ruling. The old ordering check (that the new track is never momentarily parked)
no longer applies: under the ruling the result track is an Arrange member and a
Design reapply parking it is correct.
- **Undo** — one Ctrl-Z removes the track and item and restores the folder depth; the - **Undo** — one Ctrl-Z removes the track and item and restores the folder depth; the
file survives; the source stays tagged Design. file survives; the source stays tagged Design.
- **The name and colour clone**, including a second run over an already-prefixed track - **The name and colour clone**, including a second run over an already-prefixed track
@@ -645,60 +779,51 @@ than a discovery. Nothing in Ρ is unit-testable past the pure functions.
--- ---
## Open forks — Daniel's ## Rulings — Daniel's, 2026-08-02
### Ρ-F1 — Multi-track: inherit the refusal, or render each selected track? All three [Daniel]-class forks this doc opened were ruled the same day it was framed.
Nothing here is open. This section is an index; each ruling is **specified** in the
section that owns it, and that section is the implementation-binding text.
**Recommendation: inherit the refusal.** One selected track per fire; more than one | Fork | Ruling | Specified in |
refuses with the message that already exists. Daniel's phrasing was singular ("the |---|---|---|
source track"), and it costs zero code. | **Ρ-F1** | **Refuse** a multi-track selection. One track per fire, inherited from `isMultiTrackStemRender`. A per-track loop is a settled non-goal for this phase, not a deferral | §"The render" — the multi-track paragraph; §"What Phase Ρ explicitly is NOT" |
| **Ρ-F2** | **The result track always goes to Arrange**, whatever mode was active. *"For this action which is not a capture, the result track should always go to arrange."* | §"Mode transitions" — the absolute rule, the item tag, and the auto-tag-detector fix it requires |
| **Ρ-F3** | **Follow the panel tail setting** (None / Auto / Manual), with both consequences accepted | §"The render" — the tail paragraph and its two numbered consequences |
**The counter-argument is real**, which is why this is a fork rather than a call. **Ρ-F2 went against the framing, and against the request's own original wording.**
REAPER's model action operates on the selection, and the multi-track *stem-collapse* The framing recommended mode-following on the strength of Daniel's first phrasing
hazard does not actually apply here: Ρ could loop, running `renderOffline` once per ("stays in whatever mode was active") and of one use it enabled — an A/B of a chain
selected track with a one-track source each time, and every individual render would against its own render, both on the bench, neither touching the arrangement. The
be single-track and safe. What it would cost is not the render but the bookkeeping — ruling took the alternative the framing had itself named: an absolute rule, because Ρ
each insertion shifts the indices of every later track, so the folder arithmetic must is *not a capture* and the mode-relative habit belongs to the capture pillar, not
be re-derived per iteration; and the undo label, the partial-failure story ("three of here. **That A/B use no longer exists, and no text in this doc or in `PLAN.md` may
five rendered"), and the selection-afterwards rule all have to be answered. still claim it as a benefit.** The cost the ruling accepts is that firing Ρ from
Design produces no visible change; the compensating console message is a
[propose at review] item in §"Mode transitions".
If Daniel wants multi-track, the honest shape is a **second wave**, not a bigger **The ruling also has a cost the framing did not anticipate**, recorded here so it is
first one — the single-track verb is a strict prerequisite either way, and the design not read as scope creep at review: the panel's auto-tag detector tags every new track
already leaves room (the placement function takes an index and returns a plan, so a to the active mode, so without an explicit-tag-wins filter the Design-fired case
loop just re-reads the depth list each pass). reverses the ruling on the next timer tick. The two-line fix in
`panel_input::detectNewContent` is a **deliverable of this phase**, caused by this
ruling. Under mode-following it would not have been needed — the detector would have
agreed with Ρ.
### Ρ-F2 — When Design is the active mode, where does the new track go? **Ρ-F1 and Ρ-F3 both matched the recommendation**, so nothing in the spec moved. The
counter-arguments are kept because they are the reason each was a fork rather than a
call, and they will resurface: for Ρ-F1, that the stem-collapse hazard does **not**
apply to a per-track loop (each iteration renders one track), so the refusal is
inherited rather than forced — what a loop would actually cost is bookkeeping (indices
shift per insertion, so the folder arithmetic re-derives each pass; plus an undo label,
a partial-failure story, and a selection-afterwards rule). For Ρ-F3, that a drop-in
replacement arguably wants exact length, and that forcing `None` would have kept the
exact-bounds gate live on every Ρ render.
**Recommendation: to Design, as Daniel stated** — the new track takes whatever mode **One unexercised alternative, recorded and not built.** For Ρ-F3, a third option was
was active. Implemented as written. floated at framing: follow the panel setting *and* run the bounds gate's
start-alignment check regardless of tail mode, since a tail only ever extends the end.
**The alternative is defensible**: make Ρ *absolute* rather than mode-relative — the It would recover the widening guard on the start edge without losing the tail, at the
source always goes to Design, the capture always goes to Arrange, regardless of which cost of editing a gate shared by every capture path for one caller's benefit. It is
stance you fired from. That reading makes Ρ mean "promote this design work into the **not ruled in** and is not part of this phase. If the unguarded Auto/Manual render
arrangement" in every context, which is arguably the verb's actual purpose, and it ever produces a real bug, this is the shape of the fix.
means the action does the same thing wherever you are.
The relative rule wins on Daniel's explicit words and on one real use: firing Ρ from
Design to get an A/B of a chain against its own render, on the bench, without either
touching the arrangement. That is a genuinely useful thing the absolute rule cannot
express. But it does mean a user in Design mode who expects to have "committed
something to the arrangement" has not. **One confirm.**
### Ρ-F3 — Tail: follow the panel setting, or force None?
**Recommendation: follow the panel setting** (None / Auto / Manual), for consistency
with every other render path and because a decaying design chain wants its tail when
its source is about to be silenced.
**The counter:** Ρ's pitch is a drop-in replacement for the source, and under Auto or
Manual the placed item is longer than the window it replaces — which is correct for
reverb and wrong for a section you intend to butt against the next one. Forcing None
would also keep the exact-bounds gate active on every Ρ render, since that gate runs
only under `TailMode::None`.
A third option exists and is worth naming: follow the panel setting but **run the
bounds gate's start-alignment check regardless of tail mode**, since a tail only ever
extends the end. That gets both properties at the cost of a small change to the gate's
condition, and it is the option I would take if Daniel wants the guard without losing
the tail. Not recommended by default only because it edits a shared gate for one
caller's benefit.