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