Cut the null-test verify action and pre-FX-dry mechanism (Daniel's call); keep provenance + re-capture-from-source. Sample.provenance already exists/round-trips (M1), so M10 populates and consumes it. Add docs/product/provenance.md with dual-canvas forks P1-P4 and the leaned bank-only path; rewrite PLAN.md M10 as a proposal pending fork picks.
14 KiB
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.wetDrystays 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> 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:
- Populate
provenanceon captures that resample from an existing bank sample. - Consume it via a "re-capture from source" action that regenerates the sample.
- 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
fxChainSnapshotcarry? 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 (TrackFXchunk) 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_modelmode-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 couplesbank_modelprovenance toview_mode_modelidentifiers — 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.provenancealready serializes in theBankIndexJSON (M1) under the existingbank_index/ (post-Phase-B)banksext-state key. Populating it adds no new persistence surface — the round-trip test already exercises the field. The only spec note: if Fork P1 growsfxChainSnapshotfrom a thin string to a fat FX-chunk (P1=b), the field is still one string onSample, 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_statenot 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
(
FxBypassGuardsnapshot/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)
- P1 —
fxChainSnapshotshape: thin reproducibility fingerprint (lean) vs. fat serialized FX-chain chunk. - P2 — re-capture scope: bank-only regenerate (lean) vs. re-capture-and-replace-on-timeline.
- 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.
- 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.