From 61e90af547986a727ae00f51b1c3895664fce633 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 2 Aug 2026 07:06:23 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20record=20Phase=20=CE=95=20fork=20ruling?= =?UTF-8?q?s=20in=20the=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ε-F1 proprietary RSBK, Ε-F2 new bank with auto-suffix, Ε-F3 refuse on a degraded ledger. No Ε track is gated now. Also carries Phase Ρ's plan sections, authored concurrently in this shared checkout. --- docs/PLAN.md | 498 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 447 insertions(+), 51 deletions(-) diff --git a/docs/PLAN.md b/docs/PLAN.md index 4f26733..88df744 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -9,7 +9,8 @@ come from the seventeen: it came from a direct list of seven defects and refinem (Daniel, 2026-08-01) and is specified inline in its own section below — there is no backing product doc for it, **and Phase Ε**, which likewise did not come from the seventeen: it came from a direct request (Daniel, 2026-08-02) and is scoped in -`docs/product/bank-package.md`. +`docs/product/bank-package.md`, **and Phase Ρ**, likewise a direct request (Daniel, +2026-08-02), scoped in `docs/product/render-in-place.md`. ## What this doc is, and how it relates to the others @@ -30,9 +31,9 @@ seventeen: it came from a direct request (Daniel, 2026-08-02) and is scoped in cited section rather than reading a file whole. **Worktree slug convention:** `p-w-t-`. Greek phase letters -transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**, **Ψ → `psi`**, **Ε → `e`**. So Θ-W1-T1 -dispatches into `pth-w1-t1-zone-retirement`, Γ-W1-T1 into `pg-w1-t1-knob-interaction-law`, -and Ψ-W1-T1 into `ppsi-w1-t1-capture-range-exactness`. +transliterate: **Θ → `th`**, **Ξ → `xi`**, **Γ → `g`**, **Ψ → `psi`**, **Ε → `e`**, **Ρ → `r`**. So +Θ-W1-T1 dispatches into `pth-w1-t1-zone-retirement`, Γ-W1-T1 into +`pg-w1-t1-knob-interaction-law`, and Ψ-W1-T1 into `ppsi-w1-t1-capture-range-exactness`. ## Decision state @@ -62,13 +63,30 @@ the stage lengths to 10s"* — the ceiling moves in Γ-W1-T1. **Γ-F7** (the VST *order*) is **RULED: signal flow** — *"signal flow order."* **There is now NO unanswered [Daniel]-class question anywhere in this plan.** -**That claim is scoped to Phases Θ / Ξ / Γ / Ψ, and Phase Ε reopens the class.** Phase Ε -(added 2026-08-02) carries **three** [Daniel]-class forks — **Ε-F1** (container format), -**Ε-F2** (import target), **Ε-F3** (import under a degraded ledger) — each stated with a -recommendation and its counter-argument in the phase header below and in -`docs/product/bank-package.md` §"Open forks". **Only Ε-F1 blocks a dispatch** (Ε-W1-T1); -the other two are answerable at Ε-W2 and could be ruled at implementation review if Daniel -prefers, but both are user-visible policy rather than implementation detail. +**Phase Ε (added 2026-08-02) opened three more [Daniel]-class forks and ALL THREE ARE +RULED**, same day (Daniel, 2026-08-02): **Ε-F1** container format — *"proprietary +container"*, the hand-rolled `RSBK`; **Ε-F2** import target — *"always lands as a new bank, +with an auto suffix if name collision"*; **Ε-F3** import under a degraded tracking ledger — +*"refuse mismatched import."* The rulings are folded into the tracks below and indexed at +`docs/product/bank-package.md` §"Rulings". **Two of the three landed somewhere other than +the framing recommendation:** Ε-F2 dropped the proposed rename prompt in favour of a +deterministic suffix, and Ε-F3 reversed allow-with-confirm to refuse — carried by the +framing's own counter-argument, that the accepted tracking residual contemplates *one* +untracked capture while a bulk import strands hundreds in a single gesture. **The plan-wide +claim above therefore 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.** + +**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. **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 @@ -2267,19 +2285,19 @@ actionable message, and neither direction is ever a partial landing. `export|package|portable` returns only unrelated matches (prune's portable move-to-trash, MIDI-playback prose) — so this phase **supersedes nothing and absorbs nothing.** -**Three forks are OPEN and are Daniel's.** They are the first unanswered [Daniel]-class -questions in this plan since Γ closed its seven; each is stated with a recommendation and -its counter-argument in `docs/product/bank-package.md` §"Open forks". +**Three forks were opened at framing and all three are RULED** (Daniel, 2026-08-02). The +rationale for each — including why two went against the framing recommendation — is +recorded at `docs/product/bank-package.md` §"Rulings"; the binding specification for each +is in the section of that doc named below. -| Fork | Question | Recommendation | Blocks | +| Fork | Ruling | Specified in | Bound into | |---|---|---|---| -| **Ε-F1** | Container format: hand-rolled `RSBK` vs. ZIP via the vendored minizip (`vendor/WDL/WDL/zlib/`) | **hand-rolled `RSBK`** — the only option where the whole codec lands pure | **Ε-W1-T1 dispatch.** One-way door: packages are in users' hands the day it ships | -| **Ε-F2** | Import target: always a new bank, or also offer merge-into-existing | **new bank by default**, merge as a *separate* bindable verb if wanted at all | Ε-W2-T2's action count and dialog; not W1 | -| **Ε-F3** | Import while the tracking ledger is degraded: allow-with-confirm, or refuse | **allow with an up-front confirm naming the consequence** — matches the accepted residual rather than inventing a new block | Ε-W2-T2's guard; not W1 | +| **Ε-F1** | **Proprietary container** — the hand-rolled `RSBK`. ZIP via the vendored MiniZip64 (`vendor/WDL/WDL/zlib/`) and a hand-written stored-only ZIP shape are both rejected and are not to be revisited in this phase | §"The container" | Ε-W1-T1 | +| **Ε-F2** | **Import always lands as a new bank**, with an automatic numeric suffix on a display-name collision — no prompt, no overwrite. Merge-into-existing is **out of scope for Phase Ε**, not deferred | §"Identity and collision on import", incl. the auto-suffix rule | Ε-W2-T2 | +| **Ε-F3** | **Refuse** the import when the tracking ledger is degraded. No confirm-and-proceed path, no opt-out | §"Import under a degraded tracking ledger" | Ε-W2-T2 | -**Ε-F1 is the only one that blocks a dispatch.** Ε-F2 and Ε-F3 can be carried into Ε-W2-T2 -as ruled-at-review if Daniel prefers, but both are user-visible policy rather than -implementation detail, so the plan's default is to ask. +**No Ε track is gated on a decision.** Every track in this phase is dispatchable as +written. ### Phase-Ε acceptance criteria @@ -2334,7 +2352,9 @@ new directories** (`src/core/package/`, `src/shell/package/`) that no other phas not read here. The only pre-existing files any Ε track edits are named per track below — `core/tracking/origin_ledger` (W1-T3, exclusively), the root `CMakeLists.txt` `add_subdirectory` list (W1-T1 and W1-T2, one line each), `src/app/main.cpp` and the panel's -bank menu (W2-T1 and W2-T2, one registration line and one menu row each). **No Ε track +bank menu (W2-T1 and W2-T2, one registration line and one menu row each), and +`core/model/bank_book.{h,cpp}` (W2-T2 only — **one additive public `const` member**, required +by the Ε-F2 auto-suffix rule so the name fold keeps its single home). **No Ε track touches `core/instrument/`, `shell/instrument/`, or any capture backend.** --- @@ -2356,10 +2376,10 @@ bytes and structs, T2 knows only paths and bytes, T3 knows only the ledger. `src/shell/package`. Two append-only lines in one list: **textual merge adjacency, not semantic contention.** Whichever lands second rebases. -**T1 is the wave's only gated dispatch** — it cannot start before Ε-F1 is ruled, because the -fork *is* T1's deliverable. T2 and T3 are unaffected by Ε-F1 and can start immediately: T2's -API is bytes-in/bytes-out regardless of what those bytes mean, and T3 touches no package -code at all. +**All three tracks are dispatchable now.** T1 carries the Ε-F1 ruling — the container is the +hand-rolled `RSBK`, decided, not a candidate T1 chooses among. T2 and T3 never depended on +that ruling anyway: T2's API is bytes-in/bytes-out regardless of what those bytes mean, and +T3 touches no package code at all. #### Ε-W1-T1 — `package-format` @@ -2379,6 +2399,12 @@ root `CMakeLists.txt`. **Does not own:** `export_plan` / `import_plan` (Ε-W2), `shell/`, `core/model`, or `core/tracking`. **Behavior.** +- **The container is the hand-rolled `RSBK` (Ε-F1, ruled).** Magic `RSBK`, a fixed + little-endian header carrying the two version fields, a length-prefixed JSON manifest, then + each entry's payload concatenated in manifest order. Framing is built on `core/wire/bytes.h` + (`putLE` / `ByteReader`) and the manifest on `core/json` — both already owned and tested + here. **No ZIP, no compressor, no new third-party source in the build**; a link edge to + `vendor/WDL/WDL/zlib/` means the ruling was misread. - **Two version integers, not one.** `formatVersion` = what this writer emitted; `minReaderVersion` = the oldest reader that can read it safely. The reader's whole rule is `minReaderVersion <= kPackageFormatVersion`. An **additive** change (a new optional @@ -2425,8 +2451,8 @@ root `CMakeLists.txt`. **Does not own:** `export_plan` / `import_plan` (Ε-W2), - `package_format_tests`, `package_manifest_tests`, `bank_package_tests` all run without REAPER or a DAW. -**Open questions.** **[Daniel] Ε-F1** — the container format itself; this track cannot be -dispatched until it is ruled. **[propose at review]** whether `package_format` and +**Open questions.** **No [Daniel] questions — Ε-F1 is RULED** (proprietary `RSBK`), so this +track is dispatchable as written. **[propose at review]** whether `package_format` and `bank_package` are genuinely two modules or one — the split is proposed on responsibility grounds (constants and classification vs. offset arithmetic) and may collapse if the arithmetic turns out to be twenty lines. @@ -2613,24 +2639,39 @@ from the bank's display name (recommended, sanitized through #### Ε-W2-T2 — `bank-import` -**Goal.** A package becomes a bank in this project — completely, or not at all — with every -one of the four collision classes answered explicitly rather than by whatever the model -happens to do. +**Goal.** A package becomes a **new** bank in this project — completely, or not at all — +with every one of the four collision classes answered explicitly rather than by whatever the +model happens to do. -**Spec:** `docs/product/bank-package.md` §"Identity and collision on import", §"Failure -modes" (import rows), §"Version tagging: both directions". +**Spec:** `docs/product/bank-package.md` §"Identity and collision on import" (including the +auto-suffix rule), §"Failure modes" (import rows), §"Import under a degraded tracking +ledger", §"Version tagging: both directions". **Surface boundary — owns:** new `core/package/import_plan` (pure: the id remap table, the parent remap, the per-entry write / skip-already-present / rename disposition, the destination bank name after uniqueness folding), new `shell/package/import_bank` (the promptless verb), new `shell/actions/package_import_action`, the panel's `WM_DROPFILES` route for a `.rsbank` (routing only — the existing ingest route for audio files is untouched), one -registration line in `src/app/main.cpp`, one panel menu row. **Does not own:** anything on the -export side, `bank_book`'s rules (consumed, never re-implemented), `origin_ledger` -(Ε-W1-T3's). +registration line in `src/app/main.cpp`, one panel menu row, and — the **only** `core/model/` +edit in the phase — **one additive public `const` member on `BankBook`** (recommended +`std::string uniqueDisplayName(const std::string& seed) const`), so the auto-suffix probe runs +behind the model's own name fold. **Does not own:** anything on the export side, +`bank_book`'s *rules* (consumed, never re-implemented — the new member exposes the existing +fold, it does not add a second one), `origin_ledger` (Ε-W1-T3's). **Behavior.** -- **Version gate first, before any byte is written.** `minReaderVersion` above this build +- **The ledger guard runs FIRST — before the file picker opens (Ε-F3, ruled: refuse).** If + `tracking::ledgerDegraded(status)` holds for the project's loaded ledger status + (`Unreadable` or `FutureVersion`; `core/tracking/origin_ledger.h:94`, `:100-101`), the + import **refuses outright** — no picker, no bytes read, no confirm-and-proceed path, no + opt-out. `Fresh` and `Loaded` both proceed. **Do not key this on + `PruneReport::blockedByTracking`**: that flag also fires on undecodable `rsusage_*` keys, + which govern deletion-time protection and have nothing to do with writing birth records. + The refusal is a `ShowConsoleMsg` block mirroring `prune_action.cpp:30-69` in structure and + tone, with two variants (malformed / newer-build) and every recovery line naming **this + build's** namespace through `version::extStateNamespace()`. Exact wording in the spec doc. + **Export is deliberately not gated this way** — that is Ε-W2-T1's, and it stays ungated. +- **Version gate second, before any byte is written.** `minReaderVersion` above this build refuses the whole package and reports through `ShowMessageBox` (**verified**, `reaper_plugin_functions.h:6546`) naming three things: the package's requirement, this build's ceiling, and the writer's semver. Two of the three is not enough @@ -2643,7 +2684,22 @@ export side, `bank_book`'s rules (consumed, never re-implemented), `origin_ledge `capture_paths::deriveBankPaths`, silently, counted in the summary. (3) **Content hash** — consult `BankModel::findByHash` **before writing the payload**; on a hit, skip the write entirely and let the entry collapse, so a dedup never manufactures an orphan. (4) **Bank - display name** — Ε-F2. + display name** — **auto-suffix, no prompt** (Ε-F2, ruled). Seed = the package's recorded + source bank name **verbatim** (or the literal `Imported bank` if absent/blank); take the + first of `seed`, `seed + " 2"`, `seed + " 3"`, … whose fold is free in the destination + book, ascending from 2. Four points that decide the behaviour and must not be re-invented: + the seed is **never re-parsed** (`"Drums 2"` colliding lands as `"Drums 2 2"`, not + `"Drums 3"` — a bare trailing integer is indistinguishable from `"Kit 808"`); the probe + **fills gaps** (first-free, not highest-plus-one, so it is a pure function of the current + name set); the probe **terminates** by pigeonhole within `B + 1` candidates for `B` banks, + so **no arbitrary cap**; and the fold is `BankBook`'s own (`bank_book.h:252-258`), reached + through the new public member, never re-implemented in `import_plan`. Sample display names + are **not** suffixed, and `slot_map` positions are untouched. +- **Always a new bank; never a merge (Ε-F2, ruled).** The import creates a bank — it never + merges into an existing one, never lands into the pool, and offers no target picker. A + pool export therefore lands as a **named** bank `"Pool 2"`, which is correct, not a glitch. + This track ships **one** action, not two; merge-into-existing is out of scope for the + phase, and move/copy already cover the after-the-fact case. - **Birth records at landing.** Every landed file goes through `ReaSamplerSession::recordCreated(sample, OriginKind::PackageImport)` at the same point the `Sample` is added, in the same straight-line block, per `core/tracking/CLAUDE.md`'s @@ -2667,8 +2723,19 @@ export side, `bank_book`'s rules (consumed, never re-implemented), `origin_ledge - `planImport` is pure and total, and every one of the four collision classes has a test that exercises it without a filesystem: colliding ids, colliding file names, a hash already present, and a colliding bank name. -- Importing a package built from bank B into a project that already contains B produces a - correct result under the Ε-F2 ruling, with every id reminted and no entry lost. +- Importing a package built from bank B back into the project that already contains B lands + a **new** bank named `"B 2"`, with every id reminted, no entry lost, and B itself + unmutated. Importing it a third time lands `"B 3"`. +- The suffix probe is pinned by pure tests over a name set, covering at minimum: a free seed + (no suffix applied), a case/whitespace-folded collision (`"drums"` blocks `"Drums"`), a gap + (`"Drums"` + `"Drums 3"` present ⇒ `"Drums 2"`), a seed that already ends in a number + (`"Drums 2"` colliding ⇒ `"Drums 2 2"`), an absent/blank recorded name (⇒ `Imported bank`), + and a package whose source bank was the pool (⇒ `"Pool 2"`, a named bank). +- A degraded ledger (`Unreadable` and `FutureVersion`, both asserted) refuses the import with + **no picker shown, zero files written, zero index mutation**, and the message names the + channel-correct ext-state namespace. An undecodable `rsusage_*` key with an otherwise + `Loaded` ledger **does not** block — asserted, because the tempting reuse of + `blockedByTracking` would silently make it. - An entry whose payload fails its `hashBytes` check aborts the import with **zero** files landed and **zero** index mutation — asserted on both, since either alone would pass a weaker test. @@ -2685,11 +2752,13 @@ export side, `bank_book`'s rules (consumed, never re-implemented), `origin_ledge metadata, confirm a live ReaSampler 9000 instance picks up the new bank content on the generation bump, and confirm one Ctrl-Z removes the index entries. -**Open questions.** **[Daniel] Ε-F2** — new bank always, or merge offered; decides whether -this track ships one action or two. **[Daniel] Ε-F3** — behaviour when the ledger is degraded -at import time. **[propose at review]** whether the import summary is a console block, a -message box, or both; the recommendation is a console block plus a one-line message box, so -the detail is copyable and the outcome is unmissable. +**Open questions.** **No [Daniel] questions — Ε-F2 and Ε-F3 are both RULED** (new bank +always with an auto suffix; refuse on a degraded ledger), so this track is dispatchable as +written and ships one action. **[propose at review]** whether the import summary is a console +block, a message box, or both; the recommendation is a console block plus a one-line message +box, so the detail is copyable and the outcome is unmissable. Note the refusal path is +already fixed at a console block by the Ε-F3 spec, so this call is about the *success* +summary only. --- @@ -2751,6 +2820,294 @@ properties under test are structural and a large payload proves nothing extra. --- +## Phase Ρ — Render in place: a track's output to a new sibling, source to the bench + +**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. + +**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 +in substance: *similar to REAPER's "Render selected track time selection to new track +(stereo) and mute original", except instead of muting the original, the source track +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 +**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. + +### The framing answer — why this does not breach the load-bearing principle + +Root `CLAUDE.md`'s rule — *capture and placement are separate acts; any code path that +auto-inserts a capture into the timeline must be rejected in review* — is a rule **about +the bank**, and Phase Ρ does not put the bank on either side of its verb. The full argument +is `docs/product/render-in-place.md` §"The third verb"; the operative summary, which every +review of this phase must apply: + +| 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** (this phase) | arrange | arrange | never | + +What Ρ shares with capture is the **render** — `renderOffline`, `FxBypassGuard`, exact +custom time bounds, `RENDER_ADDTOPROJ = 0`, the multi-track refusal — not the capture. A +capture is a render *plus* a bank landing; Ρ takes the mechanism and declines the landing. + +**Four boundary conditions, three of them structural, all review-rejectable:** + +1. Ρ's shell never calls `session.bank()`, `session.book()`, `session.recordCreated()`, or + `session.bumpBankGeneration()`. The `Sample` the backend returns is discarded, and on the + `ProjectMedia` destination its `relativePath` is left **empty** — a Ρ `Sample` is inert + by construction. +2. **Ρ cannot express "write into the bank folder."** The destination reaches the backend as + a two-valued enum, never a caller-supplied path string. A `renderDir` string on + `CaptureRequest` instead of the enum **is** the drift, and is rejected on sight. +3. Ρ's file is never recorded as owned, so prune (`(owned ∩ present) − referenced`) cannot + reach it — and it lives outside the bank folder, so prune's enumeration never sees it + either. Two independent layers. The tool deletes only what it owns; a render-in-place + file belongs to the project. +4. **Traffic is one-way.** Ρ may borrow capture's render; **capture may never borrow Ρ's + placement.** No `place` flag on `CaptureActionDef`, no "capture and also place" action, + ever. + +### Phase-Ρ acceptance criteria + +These bind the track in this phase, in addition to the plan-wide set above. + +- **Placement is sample-exact and unsnapped.** The item lands at the render window's + `startSeconds`, unrounded, with `SnapToGrid` deliberately **not** applied (unlike + `performArrangeDrop`). Ρ's placement is the null test performed automatically — a render + of a range re-inserted at its source position nulls to silence against the source — so a + snapped or rounded placement is a phase failure, not a rough edge. +- **No tempo conform, ever.** `computeInsertMode(InsertOptions{})` only; `insert_plan` + already guarantees the &4 stretch-to-time-selection bit is never set. No conform variant + of this action is offered. +- **The bank path is byte-identical to today.** The `CaptureDestination` enum defaults to + `Bank`; every existing capture entry point must produce exactly the file, path, hash, and + index entry it produces now. If any capture test changes expectation, the seam is wrong. +- **The new track is bare.** `InsertTrackInProject(proj, p, /*flags=*/0)` — flags&1 adds + default envelopes/FX (SDK header 3954) and a default chain would process the render a + second time. +- **Both `I_FOLDERDEPTH` writes, or none.** The sibling-placement arithmetic is pure and + 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. +- **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 + (`design_view_actions::doMoveItems`' documented ordering). +- **Every pure module gets a `_tests` target.** All three pure additions land in + existing `core/capture` modules that already have one. + +**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. + +**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. + +### Open forks — Daniel's. None gates a dispatch. + +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". + +- **Ρ-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. + +--- + +### Ρ-W1 — The verb + +**Depends on:** nothing. + +**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 +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 +point — it is the only piece with zero dependency on anything else in the track. + +#### Ρ-W1-T1 — `render-in-place` + +**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. + +**Spec:** `docs/product/render-in-place.md` — §"The render", §"Where the file goes", +§"Placement", §"The new track", §"Mode transitions", §"Undo", §"The action". + +**Surface boundary — owns:** +- **New:** `src/shell/capture/render_in_place.{h,cpp}` (the action body) and its + `CMakeLists.txt` entry. It lives in `shell/capture/` rather than `shell/actions/` because + it composes `renderOffline` and `ResolveScopeSource` — that directory's `CLAUDE.md` places + action *bodies* here and reserves `shell/actions` for skins over mutation logic owned + elsewhere. +- **Extends (existing modules, existing test targets):** `core/capture/track_topology` + (`siblingPlacement`), `core/capture/capture_name` (`captureTrackName`), + `core/capture/capture_paths` (`RenderPaths` + `deriveRenderPaths`, with `deriveBankPaths` + re-expressed over it so the file-stem spelling keeps one owner — + `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`. +- **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. + +**Behavior.** +- **Resolve** via `ResolveScopeSource(CaptureScope::Track, …)` — razor-else-time range, + selected tracks, canonical GUIDs, source track name. No range → refuse with the reason + `resolveRange` produced. **Item extent is not a fallback and must not become one.** +- **Refuse multi-track by inheritance.** `renderOffline` fires `isMultiTrackStemRender` + before touching anything, keyed on the render *source* (`SelectedTracks`), so Ρ adds no + check of its own and gets `multiTrackRefusalMessage(CaptureScope::Track)` for free. +- **Render** through `renderOffline(CaptureScope::Track, src.sourceTracks, req)` with + `req.destination = ProjectMedia`, tail from `bankPanelTailSetting()`, `channelCount = 2`, + `Float32`, `sampleRate = 0`, and the name from `captureNameFor(src.trackNames, 0, + "capture")`. Inherits the FX-bypass guard, the render-selection guard, the `RENDER_*` + snapshot/restore, `RENDER_ADDTOPROJ = 0`, the exact-bounds gate, the unsaved-project + Save-As gate, and the lossless mono collapse — **all unchanged**. +- **Destination resolves in the backend, after its own save gate**, so an unsaved project is + still prompted before any path arithmetic runs. `ProjectMedia` → `GetProjectPathEx(proj, + …)` (SDK header 2550; header 3102 names it as the way to get the *effective* recording + path when `RECORD_PATH` is blank or relative). The relative-paths-only invariant is + untouched — it binds the `BankIndex`, and Ρ writes to no index. +- **Sibling placement** (pure, in `track_topology`): prefix-sum `I_FOLDERDEPTH` to absolute + levels; `L = level[srcIdx]`; if `depth[srcIdx] >= 1` the insert position `p` is the first + `j > srcIdx` with `level[j] == L` (else `count`), otherwise `p = srcIdx + 1`; then exactly + two writes — `depth[p-1] = L - level[p-1]` and the new track's `depth = level[p] - L` + (with `level[count] = 0`). Total delta sum preserved, so nothing downstream shifts. The + five cases and their expected outcomes are tabulated in the product doc; a malformed + project whose deltas do not sum to zero clamps rather than asserts. +- **Create + dress:** `InsertTrackInProject(proj, p, 0)`, `GetTrack(proj, p)`; + `SetMediaTrackInfo_Value(new, "I_CUSTOMCOLOR", (double)GetTrackColor(source))` — one line + clones a colour and the absence of one, since `GetTrackColor` returns the value already + OR'd with `0x1000000` and `0` means unset; `GetSetMediaTrackInfo_String(new, "P_NAME", + buf, true)` with `captureTrackName(trackName(source))`. +- **Name rule (pure, tested):** `"Capture " + sourceName`, **idempotent** — if the source + name already begins with the prefix, the new name is the source name verbatim, so a second + run yields `Capture MONEY`, never `Capture Capture MONEY`. An unnamed source yields + `Capture Track N` (`trackName` uses `GetTrackName`, which already answers REAPER's own + convention — the Ψ-W2-T1 precedent). A counter suffix is rejected: REAPER does not + uniquify track names either, and it is a treadmill. +- **Place:** snapshot the edit cursor → `SetOnlyTrackSelected(new)` → + `SetEditCurPos(src.startSeconds, false, false)` → `InsertMedia(absolutePath, + computeInsertMode(InsertOptions{}))` → restore the cursor. **Leave the new track selected, + alone** — a deliberate divergence from the restore-the-selection convention every other + 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, + 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). +- **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`. +- **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 + feature belongs to. Phrase: **`"render selected track to a new track (source moves to + Design)"`**. Main section only; one `ActionTableRow`; no `custom_action`/`hookcommand2`. + +**Invariant amendments — deliverables of this track, not follow-ups** (the Phase Ψ +precedent, where three such amendments were 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."* Amend to state that this directory now hosts two + placing paths and give 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."* Scope the sentence explicitly to that + directory and cross-reference the third verb. +3. Root `CLAUDE.md` §"The load-bearing principle" — **one sentence, not a rewrite**: a + 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. + +**Acceptance criteria.** +- `siblingPlacement` is unit-tested over all five cases in the product doc's table (normal + mid-folder, last-in-folder `-1`, last-in-two-folders `-2`, folder parent, last track in + the project) plus a malformed non-zero-sum delta list, with no DAW. +- `captureTrackName` is unit-tested for plain, already-prefixed (idempotence), empty, and + `Track N` sources. +- `deriveRenderPaths` round-trips the same stem spelling `deriveBankPaths` produces for the + same inputs, and the existing `capture_paths` tests pass unchanged. +- Every existing capture test passes with **no expectation change** — the proof that the + destination seam is inert on the `Bank` path. +- `render_in_place.cpp` contains no reference to `session.bank()`, `session.book()`, + `recordCreated`, or `bumpBankGeneration` — checkable by grep, and the review gate for + 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. + +**DAW-verification obligation** (stated up front; nothing past the pure functions is +unit-testable): +- **The null test on Ρ's own output** — render a track over a range, polarity-invert the + source against the new track, confirm silence. The phase's trust anchor. +- **The three folder cases**, each confirming the new track's nesting level and that the + render feeds (or correctly bypasses) the folder bus. `[verify — DAW]` whether + `InsertTrackInProject` plus the two `I_FOLDERDEPTH` writes settle without an intermediate + `TrackList_AdjustWindows(false)` (SDK header 7735; header 2721 notes some attribute writes + need a manual panel update, and the `isMinor` semantics are undocumented). +- **The collapsed-mono placement** — render a dead-centre source, confirm a mono item, and + confirm it sums at the same level 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 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. +- **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. + All three residuals are inherited from the documented model-vs-undo split + (`src/shell/view/CLAUDE.md` §Gotchas), not introduced here. +- **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` 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 +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 +with "nothing selected"; the recommendation is to leave that inherited behaviour alone +rather than add a message for a case the existing path already handles correctly. + +--- + ## Traceability — all seventeen items The check that nothing was dropped. Every row points at a track that exists above. @@ -2815,9 +3172,17 @@ proof it exists to give. (Daniel, 2026-08-02), not from `TODO-1.0.md`. Listed here as a block, like Γ and Ψ; the product reasoning lives in `docs/product/bank-package.md`. It **supersedes nothing** — neither `docs/TODO.md` nor `docs/TODO-1.0.md` records export, import, or a package format, - so there is no deferred entry to absorb or contradict. It is also the only phase in this - plan that opens with unanswered [Daniel]-class forks rather than closing them; see - "Decision state" above. + so there is no deferred entry to absorb or contradict. Its three [Daniel]-class forks + (Ε-F1/F2/F3) were opened and ruled the same day it was framed, so no track here is gated on + a decision; see "Decision state" above. +- **All of Phase Ρ** (`pr-*`). **One track in one wave**, from a direct request (Daniel, + 2026-08-02), not from `TODO-1.0.md`; the product reasoning lives in + `docs/product/render-in-place.md`. 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. 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. ### Deliberate compressions @@ -2934,14 +3299,15 @@ Phase Psi — The extension trust pass (none of the seventeen; a direct Phase Epsilon — The bank package (none of the seventeen; a direct request 2026-08-02) W1 The contract, the filesystem, and the ledger's new kind [3 tracks, disjoint by dir] T1 package-format .............. core/package: framing + TWO version ints - [BLOCKED until E-F1 is ruled — Daniel] + [E-F1 RULED: proprietary RSBK. No ZIP, no zlib] T2 package-fs-shell ............ shell/package: atomic write, streaming, pickers [no REAPER save-picker exists; SWELL/Win32 split] T3 import-origin-kind .......... OriginKind::PackageImport = 5, append-only W2 The two verbs [2 tracks; disjointness CONDITIONAL — see below] T1 bank-export ................. export_plan + verb + action; project untouched T2 bank-import ................. import_plan + verb + action + .rsbank drop - [OPEN: E-F2 import target, E-F3 degraded ledger] + [E-F2 RULED: always a NEW bank, auto-suffix, no + merge. E-F3 RULED: REFUSE on degraded ledger] W3 The compatibility fixtures [1 track] T1 package-compat-fixtures ..... frozen bytes prove BOTH version directions @@ -2949,8 +3315,38 @@ Phase Epsilon — The bank package (none of the seventeen; a direct reque fields default, unknown keys skipped); newer package in older build REFUSES WHOLE with a three-part message (package needs / this build reads / writer semver). The gate is minReaderVersion <= kPackageFormatVersion — formatVersion is for the message, not the gate. + All three E-forks were ruled 2026-08-02, the day the phase was framed: NO track here is + gated on a decision. Import name collision = first free of seed, "seed 2", "seed 3", ... + seed never re-parsed, fold is BankBook's own. Shared files, named: root CMakeLists.txt add_subdirectory list (W1-T1 | W1-T2, one append-only line each); main.cpp + the panel bank menu (W2-T1 | W2-T2, one registration - line and one menu row each). W1's three tracks are unconditionally disjoint; W2's two are + line and one menu row each); bank_book.{h,cpp} is W2-T2's alone (one additive public const + member). W1's three tracks are unconditionally disjoint; W2's two are textually adjacent only — serialize T2 behind T1 if zero contention is wanted. + +Phase Rho — Render in place (none of the seventeen; a direct request 2026-08-02) + 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] + + 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 + load-bearing capture/placement rule survives it; the full argument is in the phase header + and in docs/product/render-in-place.md. + Structural boundary, not a convention: the render destination reaches the backend as a + TWO-VALUED ENUM (Bank | ProjectMedia), never a caller-supplied path — so no Rho caller can + name the bank folder. A renderDir string on CaptureRequest IS the drift. + 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). ```