b584fa4da9
Ten more were already pointing at unrelated code. Vendor SDK and SWELL citations left alone — those files don't move.
830 lines
51 KiB
Markdown
830 lines
51 KiB
Markdown
# Render in place — product notes
|
||
|
||
Framing, rationale, and design-direction calls behind **Phase Ρ — render a track's
|
||
output to a new sibling track, in the timeline, without touching the bank.** The
|
||
tickable spec lives in `docs/PLAN.md` (§Phase Ρ); the architecture detail belongs in
|
||
`src/shell/capture/CLAUDE.md` and `src/core/capture/CLAUDE.md` once the track lands.
|
||
This doc holds the *why* — the third-verb argument that reconciles this feature with
|
||
the capture/placement separation, the prior art it borrows from, the reuse inventory
|
||
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
|
||
day; **all three [Daniel]-class forks RULED the same day** — Ρ-F1 **refuse a
|
||
multi-track selection**, Ρ-F2 **the result track always goes to Arrange**, Ρ-F3
|
||
**follow the panel tail setting**. See §"Rulings" for the index and the recorded
|
||
rationale; each is specified in place in the section that owns it. Nothing in this
|
||
doc is open.
|
||
|
||
**Ρ-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.
|
||
|
||
---
|
||
|
||
## What it is (and what it is not)
|
||
|
||
**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
|
||
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, and
|
||
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
|
||
track (stereo) and mute original*. Phase Ρ differs in exactly one respect, and that
|
||
respect is the whole feature: **instead of muting the original, it parks it.** The
|
||
source track goes to Design mode — hidden from the arrange, out of the mix, FX
|
||
offline, CPU reclaimed — and its rendered audio takes its place in the arrangement.
|
||
That is a strictly better disposition than mute, because mute leaves the design
|
||
scaffolding visible and its FX resident; Design mode removes both, reversibly, from
|
||
a snapshot.
|
||
|
||
**It is not a capture.** No `Sample` is minted into any `BankModel`, no index entry is
|
||
added, no file is recorded in the tracking ledger, the bank generation is not bumped,
|
||
and no live ReaSampler 9000 instance reloads. The bank does not change in any way an
|
||
observer could detect.
|
||
|
||
**It is not a freeze.** The source track's FX chain is untouched — not removed, not
|
||
bypassed permanently, not flattened. Design View's park is snapshot-based and fully
|
||
restored on toggle-back (`src/shell/view/CLAUDE.md` §Non-destructive restore), so
|
||
switching to Design brings the source back exactly as it was, FX and routing intact.
|
||
Ableton's *Freeze & Flatten* destroys the device chain; Phase Ρ never does.
|
||
|
||
**It is not a placement of a bank sample.** The insert action and the arrange drop
|
||
both take something already in the bank and put it on the timeline. Phase Ρ's file
|
||
was never in the bank and never will be. The two paths share `InsertMedia` and
|
||
nothing else.
|
||
|
||
---
|
||
|
||
## The third verb — and why the load-bearing principle survives it
|
||
|
||
Root `CLAUDE.md` carries the tool's sharpest rule:
|
||
|
||
> **Capture and placement are separate acts.** Capturing audio writes a file to the
|
||
> bank and adds an index entry. It **never** puts an item in the arrange view. […]
|
||
> Any code path that auto-inserts a capture into the timeline violates the purpose of
|
||
> the tool and **must be rejected in review**.
|
||
|
||
Phase Ρ renders audio, places an item in the arrange, and deliberately does not touch
|
||
the bank. The question is not rhetorical and the answer is not "it's fine because
|
||
Daniel asked for it."
|
||
|
||
**The answer is that the rule is about the bank, not about rendering.** Read the
|
||
sentence again: the object of "capturing" is *the bank* — a file in the bank folder
|
||
plus an index entry. The prohibition attaches to *that act* placing an item. What the
|
||
rule protects is a two-way boundary:
|
||
|
||
- the arrangement must never gain an item as a side effect of a bank gesture, and
|
||
- the bank must never gain a member as a side effect of an arrangement gesture.
|
||
|
||
Phase Ρ crosses neither direction, because **the bank is not a party to it.** The
|
||
system has two verbs today and gains a third:
|
||
|
||
| Verb | Source | Sink | Touches the bank |
|
||
|---|---|---|---|
|
||
| **Capture** (`RunCapture`, batch, realtime, bake, ingest) | arrange / instrument | bank | writes it |
|
||
| **Placement** (`RunInsertSelected`, `performArrangeDrop`) | bank | arrange | reads it |
|
||
| **Render in place** (Phase Ρ) | arrange | arrange | never |
|
||
|
||
Three verbs, three distinct (source, sink) pairs. The bank appears in exactly two of
|
||
them and never on both sides of one. The load-bearing rule is the statement that no
|
||
single verb may have the bank on one side and the arrange on the other *in the wrong
|
||
direction* — and Ρ has the bank on neither side.
|
||
|
||
What Ρ shares with capture is the **render**, not the capture: the same
|
||
`renderOffline` seam, the same `FxBypassGuard`, the same exact-bounds custom time
|
||
window, the same multi-track refusal, the same `RENDER_ADDTOPROJ = 0`. A render is a
|
||
mechanism; a capture is a render *plus* a bank landing. Ρ takes the mechanism and
|
||
declines the landing. That is reuse, not a breach.
|
||
|
||
### The boundary that keeps them from bleeding
|
||
|
||
Four things must stay true. Each is a review-rejectable condition, and three of the
|
||
four are structural rather than remembered:
|
||
|
||
1. **Ρ's shell never names the bank.** `render_in_place.cpp` must not call
|
||
`session.bank()`, `session.book()`, `session.recordCreated()`, or
|
||
`session.bumpBankGeneration()`. The `Sample` that `OfflineRenderBackend::capture`
|
||
returns is discarded, and on the project-media destination its `relativePath` is
|
||
left **empty** — so a Ρ `Sample` is inert by construction and could not be usefully
|
||
added to a bank even by accident.
|
||
2. **Ρ cannot express "write into the bank folder."** The destination reaches the
|
||
backend as a **two-valued enum** (`Bank` / `ProjectMedia`), never as a caller-supplied
|
||
path. There is no string a Ρ caller could pass that lands a file in
|
||
`reasampler_bank/`. This is the single most important structural choice in the
|
||
phase: it makes the boundary a type, not a convention.
|
||
3. **Ρ's file is never recorded as owned.** Prune deletes `(owned ∩ present) −
|
||
referenced` (`src/core/reclaim/CLAUDE.md`), where `owned` comes from the tracking
|
||
ledger. Ρ records nothing, so its file is not prune-eligible — and it lives outside
|
||
the bank folder, so prune's enumeration never sees it either. Two independent
|
||
layers. The symmetry is worth stating plainly: **the tool deletes only what it
|
||
owns, and a render-in-place file belongs to the project, not to the tool.**
|
||
4. **The traffic is one-way.** Ρ may borrow capture's render. **Capture may never
|
||
borrow Ρ's placement.** No capture action grows a "…and place it" option, ever. If
|
||
a future request wants capture-and-place, the answer is "fire the capture action,
|
||
then fire the insert action" — two acts, which is the whole point.
|
||
|
||
**What would count as drift**, stated so a reviewer can name it: a `renderDir` string
|
||
on `CaptureRequest` instead of the enum; a Ρ path that calls `session.bank().add()`;
|
||
a Ρ file recorded via `recordCreated`; a `place` flag added to `CaptureActionDef`; or
|
||
a "Ρ but also add it to the bank" convenience action. Any of those collapses the
|
||
three verbs back into two and the rule stops meaning anything.
|
||
|
||
---
|
||
|
||
## Prior art, and what each one contributes
|
||
|
||
The shape is not novel; the *disposition of the source* is. Named precedents, because
|
||
they anchor the argument better than reasoning does:
|
||
|
||
- **Logic Pro — Bounce in Place.** The idiom Ρ's name borrows. Renders a track's
|
||
output to audio at the same timeline position, on a new track, with the source
|
||
preserved. Confirms that "in place" in DAW usage means *at the same timeline
|
||
position*, not *onto the same track* — which is why the name is right despite Ρ
|
||
creating a new track.
|
||
- **Pro Tools — Commit.** The closest prior art, and the one that validates the mode
|
||
transition. Commit offers four dispositions for the source track: *Hide and Make
|
||
Inactive* (the default), *Make Inactive*, *Delete*, and *Do Nothing*. The default
|
||
is hide-and-deactivate — visually gone and processing gone. That is precisely what
|
||
Design View's park already does (`B_SHOWINTCP=0`, `B_SHOWINMIXER=0`,
|
||
`B_MAINSEND=0`, `I_FXEN=0`, per-FX offline), except that Ρ gets it *reversibly and
|
||
as a membership fact* rather than as a per-track inactive flag. Ρ is Commit with
|
||
a fifth disposition the DAWs do not have — *move to the design bench* — supplied by
|
||
the tool's own mode system.
|
||
([Sound on Sound](https://www.soundonsound.com/techniques/making-commitments),
|
||
[Production Expert](https://www.production-expert.com/production-expert-1/pro-tools-track-commit-vs-track-freeze))
|
||
- **REAPER — Render selected track time selection to new track and mute original.**
|
||
The action Daniel named. Contributes the range semantics (time selection) and the
|
||
new-track placement; Ρ replaces its source disposition and adds colour/name
|
||
cloning.
|
||
- **Ableton Live — Freeze & Flatten.** Contributes a negative: flatten destroys the
|
||
device chain. Ρ explicitly does not, and the Design-mode park is what makes
|
||
preserving it cost nothing at playback.
|
||
|
||
---
|
||
|
||
## What already exists — the reuse inventory
|
||
|
||
Daniel's framing was that the machinery is in place. It substantially is. This table
|
||
is the proof, and it is also the spec's shape: each row names the module that answers
|
||
the need, so the implementation is composition rather than construction.
|
||
|
||
| What Ρ needs | Already answered by |
|
||
|---|---|
|
||
| Resolve the source track + the range (razor-else-time) | `shell/capture/scope_resolve` — `ResolveScopeSource(CaptureScope::Track, …)` |
|
||
| Refuse a multi-track render | `core/capture/render_settings` — `isMultiTrackStemRender` / `multiTrackRefusalMessage`, fired inside `renderOffline` |
|
||
| Render exactly the requested window, wet, at track scope | `shell/capture/capture_orchestrator` — `renderOffline` + `FxBypassGuard` + `RenderTrackSelection` |
|
||
| Never add the render to the project as an item | `shell/capture/capture.cpp` — `RENDER_ADDTOPROJ = 0`, unconditional |
|
||
| Refuse a widened render | `core/capture/render_window::frameCountFor` + the bounds gate in `OfflineRenderBackend::capture` |
|
||
| Snapshot and restore every `RENDER_*` project setting | `ScopedRenderSettings` (RAII) in `capture.cpp` |
|
||
| Force the project to be saved first | the `EnumProjects` / `Main_SaveProject` gate in `OfflineRenderBackend::capture` |
|
||
| Name the render after its source track + a discriminator | `core/capture/capture_name` — `composeCaptureName`, `shell/capture/capture.cpp` — `captureNameFor` |
|
||
| Read the source track's display name (with the `Track N` fallback) | `shell/capture/scope_resolve::trackName` |
|
||
| Collapse a bit-identical stereo render to mono | `core/capture/wav_codec::collapseToMono`, driven by `collapseCapturedFileToMono` |
|
||
| Compute the `InsertMedia` bitmask with the stretch bit provably clear | `core/capture/insert_plan::computeInsertMode` |
|
||
| Place a file at a known track + time, undo-wrapped, selection restored | the recipe in `shell/capture/insert.cpp` / `shell/actions/arrange_drop_win.cpp` |
|
||
| Move a track into Design and reapply the active mode | `core/view` `MembershipIndex::tag` + `shell/view/view.h` `applyMode` / `mintManagedLanes` |
|
||
| Persist the view model | `ReaSamplerSession::saveToActiveProject()` (the `persistViewState` pattern in `design_view_actions.cpp`) |
|
||
| Register one more bindable action | `shell/actions/action_registry` — one `ActionTableRow` in `main.cpp`'s table |
|
||
| Pure folder arithmetic over the flat `I_FOLDERDEPTH` delta list | `core/capture/track_topology` (extended — see §"The new track") |
|
||
|
||
**What genuinely does not exist**, and why nothing already there stretches to cover
|
||
it — three small pure additions and one bounded seam:
|
||
|
||
1. **A render destination that is not the bank.** `OfflineRenderBackend::capture`
|
||
derives its output path from `deriveBankPaths(projectDir, …)` unconditionally
|
||
(`capture.cpp`'s `OfflineRenderBackend::capture`) and points `RENDER_FILE` at the bank folder. Nothing about
|
||
that is parameterized. The alternative — render into the bank and then move the
|
||
file out — was rejected: it puts a transient, unindexed, unowned file inside the
|
||
folder prune enumerates, which is exactly the file class the ownership rule exists
|
||
to reason about, and it would make the bank folder momentarily lie about its
|
||
contents. **Seam:** a `CaptureDestination { Bank, ProjectMedia }` field on
|
||
`CaptureRequest` (defaulting to `Bank`), resolved by the backend *after* its own
|
||
save gate, plus a `RenderPaths deriveRenderPaths(absoluteDir, baseName, uniqueTag)`
|
||
sibling in `capture_paths` that `deriveBankPaths` is then expressed in terms of, so
|
||
the file-stem spelling keeps one owner.
|
||
2. **The absolute path of the rendered file, returned.** `CaptureResult` carries only
|
||
`sample.relativePath`, which Ρ deliberately leaves empty. One new field,
|
||
`CaptureResult::absolutePath`, set on the Ok path.
|
||
3. **Where a sibling track goes, in folder terms.** Genuinely new, genuinely
|
||
necessary, and genuinely small — see §"The new track".
|
||
4. **The idempotent `Capture ` prefix.** Six lines in `core/capture/capture_name`.
|
||
|
||
Everything else is composition. No new directory, no new backend, no new interface,
|
||
no new persisted state.
|
||
|
||
---
|
||
|
||
## The render — scope, range, refusal
|
||
|
||
**Scope is Track**, always. `CaptureScope::Track` means the render hears the item/take
|
||
FX plus the selected track's own track FX, with every ancestor and the master
|
||
neutralized to unity — no FX, no fader, no pan/width/law colouring
|
||
(`fxBypassPlanFor`, `FxBypassGuard`). That is exactly right for a drop-in
|
||
replacement: what the render contains is *the track's own contribution to its
|
||
parent*, which is what the new sibling track must reproduce when it feeds the same
|
||
parent.
|
||
|
||
**The range is razor-else-time selection**, resolved by `ResolveScopeSource` — the
|
||
same rule every other capture action already obeys. Razor wins when present; the
|
||
razor union's bounds are the window. If neither a razor area nor a time selection is
|
||
present, the action refuses with the reason `resolveRange` already produces. **Item
|
||
extent is not a fallback**, and should not become one: item extent is item scope's
|
||
concern, and a track render bounded by whichever items happen to be selected is a
|
||
different and much less predictable verb.
|
||
|
||
**Multi-track selections are refused**, inherited rather than re-implemented.
|
||
`renderOffline` fires `isMultiTrackStemRender` before touching anything, keyed on the
|
||
render *source* (`SelectedTracks`, which track scope always uses), so any selection of
|
||
more than one track refuses with `multiTrackRefusalMessage(CaptureScope::Track)`
|
||
before a single project setting is written. Ρ inherits this for free and adds no
|
||
check of its own.
|
||
|
||
**Ρ-F1, RULED by Daniel, 2026-08-02: *refuse*.** One selected track per fire is a
|
||
**settled non-goal**, in the same sense as the other entries in §"What Phase Ρ
|
||
explicitly is NOT" — not a deferral. There is no per-track loop planned, no second
|
||
wave holding one, and no seam to be left half-open on the way past. If multi-track is
|
||
ever wanted it is a new phase with its own framing, and the counter-argument that made
|
||
this a fork (the stem-collapse hazard does not apply to a per-track loop, so the
|
||
refusal is inherited rather than required) is recorded in §"Rulings" so it does not
|
||
have to be rediscovered.
|
||
|
||
**Tail follows the panel setting — Ρ-F3, RULED by Daniel, 2026-08-02: *"follow panel
|
||
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
|
||
collapses losslessly to one channel and REAPER derives a mono item from the file
|
||
(`insert.cpp` passes only a path). Daniel's Ψ.6 ask named "mono arrange items"
|
||
explicitly, so this is the intended outcome, not a side effect. **But note what Ρ
|
||
changes about the risk:** root `CLAUDE.md` already flags, as `[verify — DAW]`,
|
||
whether REAPER sums a 1-channel item on a stereo track at the same unity gain as a
|
||
dual-mono 2-channel item. Until Ρ, that property was unverified but not load-bearing
|
||
— nothing in the tool placed a collapsed capture automatically. **Ρ is the first path
|
||
where a collapsed render is placed into the mix by the tool itself,** which promotes
|
||
that question from a footnote to a verification obligation on this phase.
|
||
|
||
---
|
||
|
||
## Where the file goes
|
||
|
||
**The project's recording path** — `GetProjectPathEx(proj, buf, sz)` (SDK header
|
||
2550), which the header's own `RECORD_PATH` entry names as the way to get the
|
||
*effective* path when `RECORD_PATH` is blank or relative (header 3102).
|
||
|
||
Why there rather than a dedicated `reasampler_renders/` folder: because the file is
|
||
**the project's media, not the tool's.** REAPER's own render-to-new-track, apply-FX,
|
||
and freeze glue actions all write into the recording path; *Clean current project
|
||
directory* and *Save project as… with copy of media* both understand it. A file in
|
||
the recording path is managed by REAPER's project-media machinery, which is exactly
|
||
the machinery that should own it. A `reasampler_renders/` folder would be marginally
|
||
more findable and would make ReaSampler the apparent owner of files it explicitly
|
||
does not own — the wrong trade.
|
||
|
||
The relative-paths-only invariant is untouched: it binds the persisted `BankIndex`,
|
||
and Ρ writes nothing to any index. REAPER stores the item's source path in the `.rpp`
|
||
by its own rules.
|
||
|
||
---
|
||
|
||
## Placement — exactly
|
||
|
||
The item lands at **`src.startSeconds`**, the render window's start, unrounded and
|
||
**unsnapped**.
|
||
|
||
Unsnapped is the load-bearing word. `performArrangeDrop` runs its drop time through
|
||
`SnapToGrid` because a hand drop wants snapping; Ρ must not, because a snapped
|
||
placement would move the audio off the sample-accurate position it was rendered from
|
||
and break the property the whole tool is built on. **Ρ's placement is the null test
|
||
performed automatically:** an offline render of a range, re-inserted at its source
|
||
position, nulls to silence against the source — root `CLAUDE.md` calls that the
|
||
tool's trust anchor. Ρ *is* that gesture, made a workflow. If Ρ's placement is not
|
||
sample-exact, Ρ is broken, and the way you find out is by soloing the two tracks with
|
||
one polarity-inverted.
|
||
|
||
The recipe is `insert.cpp`'s, verbatim, with the bank lookup removed:
|
||
snapshot the cursor → `SetOnlyTrackSelected(newTrack)` → `SetEditCurPos(startSeconds,
|
||
false, false)` → `InsertMedia(absolutePath, computeInsertMode(InsertOptions{}))` →
|
||
restore the cursor. `InsertOptions{}` defaults give native length, no tempo conform,
|
||
and `insert_plan` guarantees the &4 stretch-to-time-selection bit is never set — so
|
||
"do not silently time-stretch on insert" holds by construction. Ρ must never offer a
|
||
conform variant: a conform would defeat the exact placement it exists to produce.
|
||
|
||
**Selection afterwards is a deliberate divergence.** Every other placing path
|
||
restores the caller's track selection. Ρ leaves **the new track selected, alone.**
|
||
The reason is specific: in the headline case the source track is being parked out of
|
||
sight in the same gesture, so restoring the selection would leave the user selecting
|
||
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.
|
||
|
||
**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
|
||
|
||
### Index and folder — the one piece of genuinely new arithmetic
|
||
|
||
"Sibling" is easy to say and has three cases. Getting it wrong is audible, not
|
||
cosmetic, which is why this is the one place Ρ adds a real (small) pure function
|
||
rather than composing.
|
||
|
||
The naive answer — insert at `sourceIndex + 1` — is wrong twice:
|
||
|
||
- **Source is a folder parent** (`I_FOLDERDEPTH >= 1`). Inserting immediately after
|
||
it makes the new track the folder's **first child**, so the rendered audio is
|
||
summed back into the folder and runs through the parent's FX and fader a second
|
||
time. Track scope already put the parent's own FX and fader *into* the render, so
|
||
this double-processes audibly.
|
||
- **Source is the last track in its folder** (`I_FOLDERDEPTH <= -1`). The source
|
||
carries the folder's closing delta, so inserting after it lands the new track
|
||
**outside** the folder — the audio then bypasses the folder bus entirely and the
|
||
drop-in replacement is silently wrong in the other direction.
|
||
|
||
The correct rule is one computation in absolute nesting levels, over the same flat
|
||
`I_FOLDERDEPTH` delta list `track_topology::directChildIndices` already prefix-sums.
|
||
Given `depth[i]` for every track and `level[0] = 0`, `level[i+1] = level[i] +
|
||
depth[i]` (and `level[count] = 0` for a well-formed project):
|
||
|
||
1. `L = level[srcIdx]` — the source's own nesting level.
|
||
2. **Insert position** `p`: if `depth[srcIdx] >= 1` (folder parent), `p` = the first
|
||
`j > srcIdx` with `level[j] == L` — i.e. immediately after the whole folder, at
|
||
the source's own level; `count` if none. Otherwise `p = srcIdx + 1`.
|
||
3. **Two folder-depth writes**, and only two. With `b = p - 1` (the track that will
|
||
precede the new one) and `Lp = level[p]` (the level the track currently at `p`
|
||
sits at, `0` at end-of-project): set `depth[b] = L - level[b]`, and set the new
|
||
track's `depth = Lp - L`.
|
||
|
||
The total of all deltas is preserved, so nothing downstream of the insertion shifts.
|
||
Checked against every case:
|
||
|
||
| Case | `depth[b]` after | new track `depth` | Result |
|
||
|---|---|---|---|
|
||
| Normal track, mid-folder or top level | unchanged (`0`) | `0` | inserted directly below, same level |
|
||
| Last track in a folder (`-1`) | `0` | `-1` | new track becomes the folder's last member |
|
||
| Last in two folders (`-2`) | `0` | `-2` | closing delta moves to the new track intact |
|
||
| Folder parent | unchanged | `0` | new track lands after the whole folder, at the parent's level |
|
||
| Last track in the project | unchanged | `0` or the source's close | consistent, sums to zero |
|
||
|
||
That is roughly thirty lines, fully unit-testable with no DAW, and it belongs beside
|
||
`directChildIndices` in `core/capture/track_topology` — same input, same arithmetic,
|
||
same file. A malformed project whose deltas do not sum to zero should clamp rather
|
||
than assert; the failure mode is a track at the wrong nesting level, never a crash.
|
||
|
||
Creation is `InsertTrackInProject(proj, p, /*flags=*/0)` (SDK header 3954), then
|
||
`GetTrack(proj, p)` (3501) to obtain the handle. **`flags = 0`, not `1`:** the header
|
||
states `flags&1` adds default envelopes/FX, and a Ρ track must be bare — the FX are
|
||
already baked into the audio, and a default chain would process the render a second
|
||
time.
|
||
|
||
### Colour
|
||
|
||
`SetMediaTrackInfo_Value(newTrack, "I_CUSTOMCOLOR", (double)GetTrackColor(source))`.
|
||
`GetTrackColor` (3517) returns the custom colour already OR'd with `0x1000000`, or
|
||
`0` when the track has no colour set; `I_CUSTOMCOLOR` (2942) treats a value without
|
||
that bit as "not used." So the same single line clones a colour *and* clones the
|
||
absence of one, with no branch.
|
||
|
||
### Name
|
||
|
||
`"Capture " + sourceName`, where `sourceName` is `scope_resolve::trackName(source)` —
|
||
`GetTrackName` (3629), which already answers REAPER's `Track N` convention for an
|
||
unnamed track. An unnamed track 7 therefore yields `Capture Track 7`, which is a real,
|
||
deterministic, identifiable name; this is Ψ-W2-T1's precedent applied unchanged.
|
||
Written with `GetSetMediaTrackInfo_String(newTrack, "P_NAME", buf, true)` (2997).
|
||
|
||
**The prefix is idempotent — it never stacks.** If the source name already begins with
|
||
`"Capture "`, the new name is the source name **verbatim**. So rendering `MONEY`
|
||
gives `Capture MONEY`, and rendering `Capture MONEY` gives `Capture MONEY` again, not
|
||
`Capture Capture MONEY`.
|
||
|
||
The alternative — a counter suffix, `Capture MONEY 2` — is rejected. REAPER does not
|
||
uniquify track names either, duplicate track names are ordinary and harmless, and a
|
||
counter is a treadmill that has to be maintained forever. What actually distinguishes
|
||
two renders of the same source is their position in the track list and the item on
|
||
each; the name's job is to say *what this is*, and it says that correctly the first
|
||
time. Making the operation a fixed point is worth more than distinguishability here.
|
||
|
||
This is one pure function in `core/capture/capture_name` — `captureTrackName(sourceName)`
|
||
— tested for the plain case, the already-prefixed case, the empty-source case, and
|
||
the `Track N` case. The prefix string is a display convention, not a persisted key:
|
||
unlike `kManagedLanePrefix` or an action-id suffix, changing it later strands nothing.
|
||
|
||
---
|
||
|
||
## Mode transitions — the source parks, the result goes to Arrange
|
||
|
||
Daniel's original phrasing was *"the source track stays/goes to design mode, and the
|
||
resulting new sibling track […] stays in whatever mode was active when the action was
|
||
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.**
|
||
`membership().tag(sourceGuid, kDesignModeId)` covers both readings in one call —
|
||
`tag` replaces any prior single-mode membership, so a source already in Design
|
||
*stays* (no observable change) and a source in Arrange or untagged *goes*. This is
|
||
exactly what the shipped `VIEW_TAG_DESIGN` action does to a selection; Ρ performs it
|
||
on one track as part of a larger gesture.
|
||
|
||
Two inherited behaviours to state rather than fight:
|
||
|
||
- **Show-both on the source is not cleared.** Show-both is the user's explicit "pin
|
||
this visible across modes" flag. Ρ tagging a source into Design must not silently
|
||
unpin it; a show-both source stays visible in both stances, which is what the user
|
||
asked for.
|
||
- **A folder-parent source is not hidden by tagging it.** Parents are derived, never
|
||
tagged: a parent is visible in every mode any descendant leaf is visible in
|
||
(`core/view/CLAUDE.md`). Tagging a folder parent Design sets its *own* membership
|
||
but leaves it visible in Arrange as long as any child is an Arrange member. This is
|
||
a limitation of the shipped model that the existing tag action shares exactly; Ρ
|
||
inherits it. Do **not** invent a cascade that tags the children — that changes the
|
||
membership model to make one feature convenient.
|
||
|
||
**Result track: unconditionally an Arrange member**, whatever mode was active —
|
||
`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.
|
||
|
||
**Note what is novel here, because it is worth a reviewer's attention.** Ρ is the
|
||
**first** path in the tree to write an explicit `kArrangeModeId` record. The shipped
|
||
*tag selected tracks → Arrange* action does not — it dispatches to `doUntag()`, i.e.
|
||
Arrange-by-absence, which is why the constant appears in `view_mode_model` and
|
||
`mode_enable` but never in a `tag()` call. The record is nonetheless well-formed and
|
||
behaviourally identical to the untagged state everywhere that matters:
|
||
`ViewModeModel::isMember` answers `true` for `arrange` and `false` for `design` in
|
||
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:
|
||
|
||
| 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. |
|
||
| **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-fired case produces no visible change, and that is the ruling's accepted
|
||
cost.** Fire Ρ from the bench and the immediate feedback is that nothing appears. The
|
||
price buys the action meaning exactly one thing wherever it is fired — *commit this
|
||
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
|
||
the same path `doMoveItems` already uses — so a track that ends up carrying content
|
||
for two modes splits into managed lanes exactly as it would from any other membership
|
||
change. Ρ adds no lane rule of its own.
|
||
|
||
---
|
||
|
||
## Undo
|
||
|
||
**One undo block** (`Undo_BeginBlock2` / `Undo_EndBlock2` with `UNDO_STATE_ALL`, i.e.
|
||
`-1`), opened before the track is created and closed after the mode reapply — the
|
||
same shape `insert.cpp` and `performArrangeDrop` already use. The render itself sits
|
||
*outside* the block: `renderOffline` mutates only `RENDER_*` project settings, which
|
||
it snapshots and restores by RAII, and writes a file. Nothing there is undoable and
|
||
nothing there should be in the undo history.
|
||
|
||
What one Ctrl-Z therefore restores: the new track is gone, its item with it, the
|
||
source track's folder-depth write is reverted, and the track selection is back.
|
||
|
||
Three residuals, all inherited and all honest:
|
||
|
||
1. **The rendered file survives.** REAPER's undo does not delete files, prune is the
|
||
exclusive deletion authority in this system, and Ρ's file is not even prune-
|
||
eligible. An undone render leaves an orphan `.wav` in the project's recording
|
||
path — precisely what REAPER's own render and record actions do. Not a defect.
|
||
2. **The source stays tagged Design.** REAPER's undo restores live track state but
|
||
does not roll back the view model's membership index or active mode — documented
|
||
in `src/shell/view/CLAUDE.md` §Gotchas, where `snapshots_` already carries the same
|
||
split. The way out is the existing *tag selected tracks → Arrange* action. Do not
|
||
build a compensating mechanism for one feature; the model-vs-undo split is a
|
||
phase-D-scale question, not Ρ's.
|
||
3. **A membership entry for the deleted track's GUID lingers**, harmlessly:
|
||
`ViewModeModel::reconcile(liveGuids)` prunes unknown GUIDs on its next pass.
|
||
|
||
**Persist runs after the block closes, not inside it** — the ordering
|
||
`design_view_actions::doMoveItems` already documents, because `persistViewState` may
|
||
raise a Save-As dialog and a modal dialog must not sit inside an open undo block.
|
||
|
||
---
|
||
|
||
## The action
|
||
|
||
**Command-id suffix: `RENDER_TRACK_IN_PLACE`.** FOREVER-STABLE per channel
|
||
(`channelCommandId` composes `CEREBELLUM_REASAMPLER_` / `CEREBELLUM_REASAMPLER_BETA_`
|
||
in front of it), so this string can never change once shipped — user keybindings key
|
||
off the composed id.
|
||
|
||
Chosen deliberately as a **new verb family**, not a member of `CAPTURE_*`. The id is
|
||
permanent and it is the most durable statement the codebase makes about which pillar
|
||
a feature belongs to; filing this under `CAPTURE_` would encode the exact confusion
|
||
the third-verb argument exists to prevent. `RENDER_*` also leaves room for a future
|
||
`RENDER_ITEMS_IN_PLACE` without renaming anything.
|
||
|
||
**Actions-list phrase: `"render selected track to a new track (source moves to
|
||
Design)"`**, which REAPER shows as *ReaSampler: render selected track to a new track
|
||
(source moves to Design)*. Long, but the parenthetical is not decoration — a user
|
||
binding this to a key must know the source is about to disappear from the arrangement
|
||
before they press it, not after. The existing family already carries parentheticals of
|
||
this weight (*insert selected sample at edit cursor (conform to tempo)*).
|
||
|
||
Registration is **one `ActionTableRow`** in `main.cpp`'s `buildMainActionTable()` — the
|
||
Q-W6 data-driven table drives registration, `hookcommand` dispatch, and the unload
|
||
mirror-unregister from that one row. Main section only; no `custom_action` /
|
||
`hookcommand2` second registration is needed.
|
||
|
||
**Feedback:** silent on success (the new track is the feedback), `ShowConsoleMsg` on
|
||
every refusal, carrying the reason `resolveRange` / `renderOffline` already produced.
|
||
This matches `RunInsertSelected` exactly.
|
||
|
||
---
|
||
|
||
## What Phase Ρ explicitly is NOT
|
||
|
||
Stated as sharply as the goals, because a small phase stays small only if its edges
|
||
are named:
|
||
|
||
- **Not a bank capture, in any form.** No index entry, no ledger record, no
|
||
generation bump, no instance reload.
|
||
- **Not multi-track.** Ρ-F1, RULED: one selected track per fire; more than 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
|
||
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
|
||
destroy the exact placement the feature exists to produce.
|
||
- **Not a mute, not a delete, not a freeze.** The source keeps its items, its FX, its
|
||
routing and its automation. It moves stance; it loses nothing.
|
||
- **Not a source-track cascade.** Rendering a folder parent does not tag its children,
|
||
does not restructure the folder, and does not touch anything but the two
|
||
folder-depth values the insertion arithmetic requires.
|
||
- **Not a new persisted state.** Membership writes go into the existing `"reasampler"`
|
||
view section. Ρ adds no key, no version rung, no wire format.
|
||
- **Not a new directory.** Three small pure additions to existing `core/capture`
|
||
modules, one new shell TU in `shell/capture`, two lines in `panel_input.cpp`, one
|
||
row in `main.cpp`.
|
||
|
||
---
|
||
|
||
## Invariant amendments this phase owns
|
||
|
||
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
|
||
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.
|
||
|
||
1. **`src/shell/capture/CLAUDE.md` §Invariants** — *"`RunInsertSelected` is the one
|
||
deliberate exception to capture-never-places … every other capture entry point
|
||
writes only a file + index entry."* Ρ adds a second placing path in this
|
||
directory. The amended form must say that this directory now hosts two placing
|
||
paths and state the discriminator: `RunInsertSelected` places a *bank sample*;
|
||
`render_in_place` places a render that never entered the bank. Neither is a
|
||
capture placing itself.
|
||
2. **`src/shell/actions/CLAUDE.md` §Invariants** — *"`arrange_drop_win` is the only
|
||
timeline-placing shell in this directory."* Strictly this stays true if Ρ's shell
|
||
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
|
||
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
|
||
rewrite: that a render which never enters the bank and never leaves it is a third
|
||
verb outside the rule, with the two-way boundary spelled out. The rule's force must
|
||
not be diluted — it is what keeps the tool honest — so the amendment names the
|
||
exception precisely rather than softening the prohibition.
|
||
|
||
---
|
||
|
||
## Where it lives
|
||
|
||
**Pure** — three additions, all to existing modules with existing test targets, no new
|
||
directory:
|
||
|
||
- `core/capture/track_topology` — the sibling-placement arithmetic
|
||
(`siblingPlacement(depths, srcIdx) -> { insertIndex, precedingDepth, newDepth }`).
|
||
Same input list, same prefix-sum, same file as `directChildIndices`.
|
||
- `core/capture/capture_name` — `captureTrackName(sourceName)`, the idempotent prefix.
|
||
- `core/capture/capture_paths` — `RenderPaths` + `deriveRenderPaths(absoluteDir,
|
||
baseName, uniqueTag)`, with `deriveBankPaths` re-expressed over it so the stem
|
||
spelling keeps one owner (`bankRelativeForName` already depends on that being true).
|
||
|
||
**Shell** — one new TU plus one bounded edit:
|
||
|
||
- `shell/capture/render_in_place.{h,cpp}` — the action body. It lives in
|
||
`shell/capture/` rather than `shell/actions/` because it composes `renderOffline`
|
||
and `ResolveScopeSource` and is genuinely a render path, not a skin over one; the
|
||
directory's own CLAUDE.md says action *bodies* belong here and that `shell/actions`
|
||
only skins mutation logic owned elsewhere.
|
||
- `shell/capture/capture.cpp` — the destination branch (~6 lines at the path
|
||
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
|
||
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`.
|
||
|
||
**Performance posture:** every surface is cold — one gesture, once. None of the named
|
||
hot paths (peaks envelope compute, audition, the realtime-capture tick's
|
||
single-pointer-test idle fast path, the instrument's `process()`) is touched, and no
|
||
guardrail applies beyond the general one.
|
||
|
||
---
|
||
|
||
## DAW-verification obligations
|
||
|
||
Following the plan's convention, stated up front so they are an obligation rather
|
||
than a discovery. Nothing in Ρ is unit-testable past the pure functions.
|
||
|
||
- **The null test on Ρ's own output** — render a track over a range, then
|
||
polarity-invert the source against the new track and confirm silence. This is the
|
||
phase's trust anchor and the single most important check.
|
||
- **The three folder cases** — a normal mid-folder track, a last-in-folder track, and
|
||
a folder parent — each rendered, each confirming the new track's nesting level and
|
||
that the render feeds (or bypasses) the folder bus correctly. `[verify — DAW]`
|
||
whether `InsertTrackInProject` at index `p` combined with the two `I_FOLDERDEPTH`
|
||
writes settles without an intermediate `TrackList_AdjustWindows(false)` (header
|
||
7735; the note at 2721 says some attribute writes need a manual panel update, and
|
||
the `isMinor` semantics are undocumented).
|
||
- **The collapsed-mono placement** — render a dead-centre source, confirm the item is
|
||
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,
|
||
promoted to load-bearing by Ρ.
|
||
- **Both mode transitions** — 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 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
|
||
file survives; the source stays tagged Design.
|
||
- **The name and colour clone**, including a second run over an already-prefixed track
|
||
(must not stack) and an unnamed source (must read `Capture Track N`).
|
||
- **`GetProjectPathEx` on a project saved in a folder with a non-default recording
|
||
path**, confirming the render lands where the project's media lives.
|
||
|
||
---
|
||
|
||
## Rulings — Daniel's, 2026-08-02
|
||
|
||
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.
|
||
|
||
| Fork | Ruling | Specified in |
|
||
|---|---|---|
|
||
| **Ρ-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 |
|
||
|
||
**Ρ-F2 went against the framing, and against the request's own original wording.**
|
||
The framing recommended mode-following on the strength of Daniel's first phrasing
|
||
("stays in whatever mode was active") and of one use it enabled — an A/B of a chain
|
||
against its own render, both on the bench, neither touching the arrangement. The
|
||
ruling took the alternative the framing had itself named: an absolute rule, because Ρ
|
||
is *not a capture* and the mode-relative habit belongs to the capture pillar, not
|
||
here. **That A/B use no longer exists, and no text in this doc or in `PLAN.md` may
|
||
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".
|
||
|
||
**The ruling also has a cost the framing did not anticipate**, recorded here so it is
|
||
not read as scope creep at review: the panel's auto-tag detector tags every new track
|
||
to the active mode, so without an explicit-tag-wins filter the Design-fired case
|
||
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 Ρ.
|
||
|
||
**Ρ-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.
|
||
|
||
**One unexercised alternative, recorded and not built.** For Ρ-F3, a third option was
|
||
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.
|
||
It would recover the widening guard on the start edge without losing the tail, at the
|
||
cost of editing a gate shared by every capture path for one caller's benefit. It is
|
||
**not ruled in** and is not part of this phase. If the unguarded Auto/Manual render
|
||
ever produces a real bug, this is the shape of the fix.
|