Files
reasampler/docs/product/provenance.md
T
daniel 92827c59be Lock M10 provenance spec to Daniel's fork picks (P1=a, P2=a)
Settle the reshaped Milestone 10: thin capture-recipe fingerprint and
bank-only re-capture; P3/P4 closed as moot. Remove the pending-picks
banner in PLAN.md and stamp the provenance product note as settled.
2026-07-23 20:10:54 -04:00

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: SETTLED (2026-07-23). Reshaped from the old "provenance + null-test verify" M10. Two decisions were fixed by Daniel up front (see What was cut below). The four dual-canvas interaction forks are now resolved: P1=a thin fingerprint, P2=a bank-only re-capture, which makes P3 and P4 moot (closed). The fork analysis below is retained as the rationale record — each fork is stamped with its resolution inline; nothing here is open.


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> 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? — CHOSEN: (a) thin fingerprint (Daniel, 2026-07-23). 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. Resolved 2026-07-23: (a). fxChainSnapshot is a capture-recipe fingerprint for drift detection and re-run — not a serialized chain to restore.


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? — CHOSEN: (a) bank-only re-capture (Daniel, 2026-07-23). 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. Resolved 2026-07-23: (a). Re-capture regenerates the file into the bank and updates the Sample in place; it never places/replaces on the timeline. P3 and P4 are therefore moot — closed (b) can still be revisited as a later opt-in.

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? — CLOSED/moot under P2=a (2026-07-23). 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. Closed 2026-07-23: moot under P2=a. Re-capture never places, so it stores no canvas memory; provenance stays pure bank metadata and the two modules stay decoupled. Revisit only if re-capture-and-replace (P2=b) is later adopted.

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? — CLOSED/moot under P2=a (2026-07-23). 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. Closed 2026-07-23: moot under P2=a — bank-only re-capture places nothing, so no auto-tag interaction arises.

Summary of the settled path

Daniel took the leans (P1=a thin fingerprint, P2=a bank-only re-capture, 2026-07-23), so 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 are moot (closed) — they were only live if re-capture also re-placed onto the timeline.

The settled shape: 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.

Decision list (settled 2026-07-23)

  1. P1 — fxChainSnapshot shape: CHOSEN (a) thin reproducibility fingerprint. (Rejected: (b) fat serialized FX-chain chunk.)
  2. P2 — re-capture scope: CHOSEN (a) bank-only regenerate. (Rejected for now: (b) re-capture-and-replace-on-timeline; may return as a later opt-in.)
  3. P3 — canvas memory: CLOSED/moot under P2=a. Was only live under P2=b; lean was (a) active-mode rule, provenance stores no view ids.
  4. P4 — auto-tag interaction: CLOSED/moot under P2=a. Was only live under P2=b + P3=b.

Picks 1a + 2a make P3 and P4 moot and keep M10 a small, decoupled, capture-pillar milestone. The PLAN M10 points are locked to this path.