# Provenance — product notes Framing, rationale, and the dual-canvas reconciliation behind the reshaped **Milestone 10 (provenance)**. The tickable spec lives in `PLAN.md` (M10); the authoritative technical detail is `CONTEXT.md` (§Data model, §capture) plus this note for the reconciliation calls. This doc holds the *why* and the open forks so they don't clutter the build docs. Status: **PROPOSAL, pending Daniel's sign-off (2026-07-23).** Reshaped from the old "provenance + null-test verify" M10. Two decisions are fixed by Daniel and are **not** reopened here (see *What was cut* below). The remaining content is the provenance-only shape plus the dual-canvas interaction forks Daniel needs to settle. --- ## What was cut (fixed by Daniel — do not reopen) - **The null-test verification ACTION is cut.** Daniel verifies bit-accuracy himself when he cares (he already null-tested the first capture spike, M3). The tool ships no null-test button. - **The "true pre-FX dry capture" mechanism is dropped.** The old M10 note said the null test would require a true pre-FX dry render (bypass-around-render or the M8 realtime pre-FX tap). With the null-test action gone, that mechanism has no consumer and is dropped too. `CaptureRequest.wetDry` stays in the struct as an inert seam (M7 already retained it), but M10 does **not** build a dry path. What survives from the old M10: **provenance + "re-capture from source,"** which Daniel confirmed is genuinely useful — with the added constraint that it must be coherent with the dual-canvas (Design View / two-canvas) architecture built in parallel. --- ## What provenance is (and what already exists) **Provenance records where a sample came from, so a sample can be regenerated from its source.** The concrete case: you capture something into the bank, place it, process it further, and re-capture the processed result — provenance is the thread back from the child sample to the parent it was resampled from, plus enough about the capture to reproduce it. **Most of the data model already exists.** `Sample` (M1, landed) already carries: ``` std::optional provenance; // set only when resampled struct Provenance { std::string parentSampleId; std::string fxChainSnapshot; }; ``` and it already JSON-round-trips (M1's lossless-round-trip test covers it). So M10 is **not** "add provenance fields" — the seam is cut. M10 is: 1. **Populate** `provenance` on captures that resample from an existing bank sample. 2. **Consume** it via a "re-capture from source" action that regenerates the sample. 3. **Reconcile** both with the dual-canvas model (the new work — see below). ### What `fxChainSnapshot` should mean now (given the M7 rework) The old note assumed provenance would snapshot a chain for a *pre-FX dry* render. That's gone. Under the **shipped M7 capture model**, capture is always wet and the control is the **FX scope** (item = item/take FX only; track = item FX + that track's own track FX), with the out-of-scope chain neutralized to unity by `FxBypassGuard` for the render. There is no wet/dry dial. So `fxChainSnapshot` should record **the capture recipe, not a dry-render chain**: the scope, the source range (already on `Sample.sourceRange`), the source track GUID(s) (already on `Sample.trackGuids`), the tail setting, and — the genuinely new bit — enough of the **source FX-chain identity at capture time** that "re-capture from source" can tell whether the source still matches what was captured. This is a *fingerprint for reproducibility*, not a mechanism for a different render mode. > **Fork P1 — how much chain state does `fxChainSnapshot` carry?** Two shapes: > **(a) thin fingerprint** — a hash/summary of the source scope + FX-chain identity > at capture time, used only to detect drift ("source has changed since capture") > and to re-run the *same* capture request; or **(b) fat snapshot** — a full > serialized FX-chain state string (`TrackFX` chunk) that re-capture could restore > before rendering, so the regenerated sample matches even if the user has since > tweaked the chain. (a) is simpler, non-destructive, and matches "re-capture > reflects the source as it is *now*"; (b) is heavier, mutates the live chain during > re-capture (a new destructive-ish surface), and re-opens some of the pre-FX-dry > complexity we just cut. **Lean: (a) thin fingerprint.** Re-capture-from-source > most naturally means "run the capture again against the source's *current* state" > — that's the useful workflow (I changed the source, give me the updated sample). > The fingerprint's job is to *tell* the user the source drifted, not to freeze it. > Confirm this is the intent before scoping. --- ## The dual-canvas reconciliation (the real new work) Daniel's constraint, verbatim: *"with the sound design canvas parallel, I think provenance is genuinely useful, but we should make sure it's compliant with the dual canvas stuff."* The dual-canvas ("two-canvas," Phase D2) architecture that landed in parallel: Design View is a **mode projection** over one timeline reaching both **tracks** (D1 parking) and **items** (D2 fixed lanes). New content is **auto-tagged to the active mode** at creation. Membership + lane-ownership are GUID-keyed and persist in the `"reasampler"` `view_state` section — a **separate** pure module (`view_mode_model`) from the bank (`bank_model`). The settled placement rule (CONTEXT §Capture placement — mode-aware): *an explicit placement while in Design mode lands the item in the Design lane.* There are exactly **four** genuine interaction points between provenance and this model. Each is a fork for Daniel. ### Where re-capture lands (the load-bearing one) "Re-capture from source" regenerates a sample into the bank. Per the load-bearing capture principle, **regenerating the bank sample never inserts into the timeline** — so at the bank level there is *no* canvas interaction: the regenerated file + index entry land in the bank exactly as any capture does, and the bank is mode-agnostic. The interaction only appears **if re-capture also re-places** the regenerated sample onto the timeline (replacing the old placed item). That is a *placement*, and placement is mode-aware under D2. > **Fork P2 — does "re-capture from source" re-place, or only refresh the bank > entry?** Two shapes: > **(a) bank-only re-capture** — regenerate the file + update the bank Sample > in place; the user re-places manually if they want the new version on the > timeline. Fully honors the load-bearing principle with zero canvas coupling; > simplest; matches how every other capture behaves (capture ≠ placement). > **(b) re-capture-and-replace** — regenerate *and* swap the placed timeline item > for the new file. Convenient, but it is an auto-placement path, so it must obey > the D2 mode-aware placement rule (lands in the active mode's lane / the original > item's lane) and it touches the timeline (undo block, non-destructive to > everything else). **Lean: (a) bank-only.** It keeps M10 inside the capture > pillar's clean "capture never places" line and defers all the canvas-placement > complexity. (b) can be a later opt-in ("re-capture and replace in place") once (a) > proves the provenance thread. If Daniel wants (b), P3 and P4 below become live. ### Does provenance need to record canvas/mode membership? `Sample` (bank) and `Membership`/`LaneOwnership` (view model) are **separate pure modules today, by design** — the bank is mode-agnostic (a sample is just a file + metadata; it doesn't know it was placed in Design). The question is whether provenance must break that separation to record *which canvas/lane* the source item lived in, so re-capture can put the regenerated sample back there. Under Fork P2 = (a) bank-only, the answer is **no** — re-capture doesn't place, so it needs no canvas memory; the bank stays mode-agnostic and the two pure modules stay decoupled. Under P2 = (b) re-place, the answer becomes **yes, partially**. > **Fork P3 — (only live if P2 = re-place) does provenance store canvas/lane > membership?** If re-capture re-places, where does it land? > **(a) active-mode rule** — re-placement follows the *same* D2 auto-tag/placement > rule as any explicit placement: it lands in whatever mode is active *now*. > Provenance stores **nothing** about canvas; the view model's existing rule > governs. Keeps `bank_model` mode-agnostic. > **(b) origin-lane memory** — provenance records the source item's mode/lane at > capture time (a GUID + mode-id or lane-key) so re-capture lands the regenerated > sample back in the *original* canvas regardless of the active mode. More faithful > to "put it back where it was," but it couples `bank_model` provenance to > `view_mode_model` identifiers — a cross-module reach the architecture currently > avoids. **Lean: (a) active-mode rule**, kept in the view model; provenance stays > pure bank metadata with no view-model ids. Only reach for (b) if "re-capture > restores the exact original lane" is a stated requirement. ### Re-capture and auto-tagging D2 auto-tags **new** track/item GUIDs (detected by the panel-timer GUID diff) to the active mode. A re-placed item (P2 = b) is a *new* item GUID on the timeline, so it would be auto-tagged to the active mode automatically — which is exactly Fork P3 = (a) behavior, for free, via the existing detection path. No special-casing needed *unless* Daniel wants origin-lane memory (P3 = b), in which case re-capture must tag the new item explicitly and **suppress** the auto-tag for that GUID (or the two fight). > **Fork P4 — (only live if P2 = re-place AND P3 = origin-lane) does re-capture > preserve or re-run auto-tagging?** If provenance restores the origin lane, the > re-placed item must be tagged to the *origin* mode, not the active mode — so > re-capture has to write the membership itself and exempt that GUID from the > timer's auto-tag (same class as the manual-lane exemption already in > `autoTagNewContent`). **This fork only exists under P2=(b) + P3=(b).** Under the > leaned defaults (P2=a, or P2=b + P3=a) it does not arise: bank-only re-capture > places nothing, and active-mode re-placement rides the existing auto-tag path > unchanged. ### Summary of the leaned path If Daniel takes the leans (**P1=a thin fingerprint, P2=a bank-only re-capture**), the reconciliation collapses to almost nothing: provenance is **pure per-sample bank metadata**, `bank_model` and `view_mode_model` **stay decoupled**, and M10 touches **no** canvas code. The dual-canvas compliance is satisfied by *staying on the right side of the load-bearing line* (capture/re-capture never places), not by new coupling. P3 and P4 only wake up if Daniel wants re-capture to also re-place onto the timeline. This is the recommendation: **keep provenance in the bank, keep re-capture a bank-only regenerate, and let the existing D2 placement rule handle the timeline if and when the user manually re-places.** It is the smallest thing that delivers the useful workflow and the cleanest against the architecture. --- ## Persistence / precision-invariant implications (spec-level) - **Provenance is already-persisted metadata.** `Sample.provenance` already serializes in the `BankIndex` JSON (M1) under the existing `bank_index` / (post-Phase-B) `banks` ext-state key. **Populating it adds no new persistence surface** — the round-trip test already exercises the field. The only spec note: if Fork P1 grows `fxChainSnapshot` from a thin string to a fat FX-chunk (P1=b), the field is still one string on `Sample`, so the JSON shape is unchanged, but the blob gets heavier — a size consideration, not a schema one. Under P1=a (thin fingerprint) the field stays small. - **Under the leaned path, provenance touches `view_state` not at all.** No lane-ownership or membership data is added for provenance; the view section is unchanged. (Only P3=b would add view-model ids into provenance — and that would be the argument *against* P3=b.) - **Precision invariants are unaffected.** Re-capture is a capture: it produces a file deterministically (bit-identical repeats hold — a re-capture with an unchanged source and request is byte-identical to the original capture, which is itself a nice provenance property), it is non-destructive to source items/tracks (`FxBypassGuard` snapshot/restore, as M7), it honors exact bounds, and it writes only relative paths. **Do not design the serialization here** — this is spec-level; the implementer owns the JSON encoding of whatever P1 shape Daniel picks. - **Do not reintroduce the dry path.** The precision-invariant list in CONTEXT still names the null test as the "trust anchor." With the action cut, that line is now historical framing, not an M10 deliverable — flag for doc-keeper to reconcile when M10 lands, but M10 does **not** ship a null-test action or a pre-FX dry render. --- ## Open forks for Daniel (the decision list) 1. **P1 — `fxChainSnapshot` shape:** thin reproducibility fingerprint *(lean)* vs. fat serialized FX-chain chunk. 2. **P2 — re-capture scope:** bank-only regenerate *(lean)* vs. re-capture-and-replace-on-timeline. 3. **P3 — (only if P2=replace) canvas memory:** re-place follows the active-mode rule, provenance stores no view ids *(lean)* vs. provenance records origin mode/lane and restores it. 4. **P4 — (only if P2=replace AND P3=origin-lane) auto-tag interaction:** re-capture writes origin-mode membership and exempts the GUID from the timer auto-tag. Only live under the two non-leaned choices above. The leaned path (1a, 2a) makes 3 and 4 moot and keeps M10 a small, decoupled, capture-pillar milestone. Awaiting Daniel's picks before the PLAN M10 points are locked.