Merge bake diagnosis soundness: every printed claim backed by an observation, write proof only where evidence crosses the plugin boundary

This commit is contained in:
2026-08-02 13:45:38 -04:00
19 changed files with 1416 additions and 367 deletions
+90 -11
View File
@@ -654,17 +654,16 @@ tracks-and-range-only shape.
**Context (surfaced by Ψ-W2-T2, mono-collapse).** The collapse (`collapseCapturedFileToMono`
/ `core/capture/wav_codec::collapseToMono`) ships for every extension capture path —
offline, realtime, batch, recapture — but not for `bake_land.cpp`'s `landOne`, the
resample bake's landing function. A dead-center instrument render (the common case
offline, realtime, batch, recapture — but not for `bake_land.cpp`'s landing, the
resample bake's `prepareLanding` / `commitLanding` pair. A dead-center instrument render (the common case
that motivated Ψ.6 in the first place) is exactly the dual-mono shape the predicate
collapses, so an un-collapsed bake keeps paying for the second channel it doesn't need.
**Not deferred for the reason once given.** `landOne` reads the staged file into `bytes`
once (`bake_land.cpp:101`), parses its layout (`:105`), hashes it (`:126`), derives the
channel count twice (`:131`, `:178`), and writes it (`:165`) — all from that same one
buffer, so collapsing `bytes` right after the layout parse would keep the hash, the
channel count, and the written file consistent by construction; there is no ordering
hazard here to defer around.
**Not deferred for the reason once given.** `prepareLanding` reads the staged file into
`prep.bytes` once, parses its layout, hashes it and derives the channel count from that
same one buffer, and `commitLanding` writes that buffer — so collapsing it right after the
layout parse would keep the hash, the channel count, and the written file consistent by
construction; there is no ordering hazard here to defer around.
**The real reason.** `bake_land.cpp` is Phase Ξ's freshly-landed surface
(Ξ-W2-T1, the resample bake chain) and another team is actively remediating it. Landing
@@ -677,9 +676,9 @@ however many channels the dialed sound has, and `bake_render.cpp:38` reads
`sample.channelCount()` off that render rather than hardcoding 2 — a mono-programmed
sound already bakes to a mono file today, with no change needed.
**Intended fix.** Once `bake_land.cpp` is quiet, call `collapseToMono` on the staged
`bytes` in `landOne` right after the layout parse (`:105`) and before the hash (`:126`),
matching the offline/realtime insertion point (post-parse, pre-identity-read).
**Intended fix.** Once `bake_land.cpp` is quiet, call `collapseToMono` on `prep.bytes` in
`prepareLanding` right after the layout parse and before the hash, matching the
offline/realtime insertion point (post-parse, pre-identity-read).
**Priority / risk.** Low — a size optimization on an already-correct path, not a
precision-invariant gap; the bake's dual-mono case still lands as a valid (if larger)
@@ -768,3 +767,83 @@ above.
function under a `<module>_tests` target with no REAPER, no VST3 SDK, and no
filesystem includes; `shell/capture/render_bounds_gate` shrinks to the file-move and
the two callers' plumbing.
## The capture path ignores `saveToActiveProject`'s return at four sites
**Context.** `saveToActiveProject()` returns false for exactly two reasons — no active
project, or an unsaved one — and in both cases NOTHING was written. Four capture sites
discard that return outright: `capture_orchestrator.cpp:343`, `capture_batch.cpp:266` and
`:333`, and `realtime_lifecycle.cpp:39`.
**The wart.** A capture on an unsaved project renders the file into the bank folder, adds
the `Sample` to the in-memory book, records a birth record in memory — and loses all three
on reload. The bytes stay on disk with no index entry and no persisted ledger record, so
they are a foreign file prune will never reclaim (an unrecorded file is untouchable by
design — `core/tracking/CLAUDE.md`). Nothing is printed. The bank-op family already reads
this return and discards its undo point on a false; the capture family does not read it at
all.
**Why filed, not fixed.** Pre-existing, and the right answer is a product decision this
dispatch had no mandate for: refuse the capture up front, keep it and warn, or prompt for
a Save-As (the bank ops chose "quiet persist by design, deliberately no Save-As prompt" —
whether capture should follow is a separate call).
**Done looks like.** A capture attempted with no saved project either does not write bytes
at all, or writes them and says so in a sentence naming what will not survive a reload —
and the choice between those two is recorded rather than implicit.
## `panel_input`'s wheel handler persists the whole book per wheel message
**Context.** `panel_input.cpp:450``handleWheel` calls `markTailDirty()` on every wheel
message that actually moves `manualMs`, while the pointer is over the footer in Manual
mode. (It coalesces sub-notch deltas within ONE message and no-ops at a bound, so the
count is wheel messages that changed the value, not raw notches.)
**The wart.** `markTailDirty` is `saveToActiveProject()` — a full `BankBook` serialize plus
six ext-state value writes on the UI thread — for a setting that is one number. A flick
over the footer is a dozen of them in a few hundred milliseconds. Disproportionate rather
than incorrect: no guardrail is violated (this is nowhere near the two named hot paths),
and the writes are idempotent.
**Intended fix.** Coalesce: mark dirty and let one timer tick flush, the same shape the
panel already uses elsewhere for repaint batching.
**Done looks like.** A continuous wheel gesture over the footer produces one persist, and
the value that lands is the gesture's final one.
## `RunCaptureItemAssign`'s undo point does not follow the pattern its comment claims
**Context.** `capture_orchestrator.cpp:364-365` states that the action follows the bank-op
family's discard-on-unsaved pattern.
**The wart.** It does not: `:382-383` records the undo point unconditionally whenever
`sampleId` is non-empty, and never consults the persist's return at all. So on an unsaved
project it records an undo point for ext-state that was never written — the empty
no-effect entry `persistBankOp`'s guardrail exists to avoid. The comment describes the
intended behavior, not the code.
**Why filed, not fixed.** It is one instance of the capture-family gap filed above, and
fixing it alone would leave the other four sites divergent. Fix them together, or explain
in one place why capture differs from bank ops.
**Done looks like.** The comment and the code agree, and the whole capture family answers
the unsaved-project case one way.
## `core/tracking/CLAUDE.md`'s untracked-file enumeration says "reaches the `.rpp`" too loosely
**Context.** `src/core/tracking/CLAUDE.md:24-31` enumerates how a created file can stay
untracked, and describes the ledger as reaching the `.rpp` at the following
`saveToActiveProject()`.
**The wart.** `saveToActiveProject()` writes REAPER's IN-MEMORY project state and marks the
project dirty; REAPER writes the `.rpp` on the project's own save, which may be much later
or never. The sentence was already loose before this branch and is not made wrong by it —
but it is the same over-claim ("a write reached the file on disk") the bake's reporting
pass spent several rounds removing from its own sentences, so it should read the same way.
**Why filed, not fixed.** Editing another layer's own CLAUDE.md from a persist-and-report
dispatch is exactly the boundary crossing the per-directory docs exist to prevent. It is a
doc-keeper edit.
**Done looks like.** The enumeration distinguishes "in the project's state" from "on disk
in the `.rpp`", and does not gain a second home for the distinction.