Reshape M10 to provenance-only, reconciled with dual-canvas
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.
This commit is contained in:
@@ -23,21 +23,47 @@ state persists via the index.
|
|||||||
- [ ] Slot model + slot↔sample assignment.
|
- [ ] Slot model + slot↔sample assignment.
|
||||||
- [ ] "Capture to slot N" / "insert slot N" actions, MIDI-bindable.
|
- [ ] "Capture to slot N" / "insert slot N" actions, MIDI-bindable.
|
||||||
|
|
||||||
## Milestone 10 — provenance + null-test verify action
|
## Milestone 10 — provenance (re-capture from source)
|
||||||
**Goal:** Provenance (parent sample id + FX-chain snapshot) and "re-capture from
|
**Goal:** Populate `Sample.provenance` (parent sample id + a capture-recipe
|
||||||
source"; ship the null-test verification action. CONTEXT.md §Precision invariants,
|
fingerprint) on resample-from-sample, and ship a **"re-capture from source"**
|
||||||
Build order 10.
|
action that regenerates a sample from its recorded source. Reconciled with the
|
||||||
**Verify (in DAW):** **Null test** — a dry offline capture of a range, re-inserted
|
dual-canvas (Phase D2) model. CONTEXT.md §Data model, §capture; product framing +
|
||||||
at its source position, nulls to silence against the source. This action is the
|
the reconciliation forks in `docs/product/provenance.md`.
|
||||||
tool's trust anchor and must pass.
|
**Verify (in DAW):** A sample resampled from a bank sample carries its parent id +
|
||||||
|
recipe fingerprint; "re-capture from source" regenerates the file into the bank
|
||||||
|
(never auto-inserting into the timeline — load-bearing principle); re-capture with
|
||||||
|
an unchanged source + request is byte-identical to the original (bit-identical
|
||||||
|
repeats); non-destructive to source items/tracks.
|
||||||
|
|
||||||
- [ ] Provenance fields populated on resample-from-sample (parent id + FX-chain
|
> **PROPOSAL — pending Daniel's fork picks (2026-07-23).** Reshaped from the old
|
||||||
snapshot string).
|
> "provenance + null-test verify" M10. **Cut (fixed by Daniel):** the null-test
|
||||||
- [ ] "Re-capture from source" action.
|
> verification *action* and the true-pre-FX-dry *mechanism* the old note required —
|
||||||
- [ ] Null-test verification action (capture → re-insert at source pos → assert
|
> both dropped, see `docs/product/provenance.md` §What was cut. **Kept:** provenance
|
||||||
silence sum).
|
> + re-capture. The `Sample.provenance` struct and its JSON round-trip **already
|
||||||
|
> exist** (M1) — M10 populates and consumes the field, it does not add it. The points
|
||||||
|
> below reflect the **leaned path** (bank-only re-capture, thin fingerprint); the
|
||||||
|
> re-place variant is deferred behind forks P2–P4. Lock the points once Daniel picks.
|
||||||
|
|
||||||
**Note (from M7):** The null test requires a TRUE pre-FX dry capture, which REAPER offline render cannot produce via RENDER_SETTINGS (there is no pre-FX bit). True dry must be obtained by bypassing the source FX around an offline render (snapshot→bypass→render→restore) OR via the M8 realtime post-fader track-tap path — so the dry-capture mechanism should be designed as part of the M10 null-test work.
|
- [ ] Populate `Sample.provenance` on resample-from-sample: `parentSampleId` (the
|
||||||
|
bank sample the capture derived from) + `fxChainSnapshot` as a **capture-recipe
|
||||||
|
fingerprint** (scope + source FX-chain identity at capture time — a drift/repro
|
||||||
|
fingerprint, NOT a pre-FX-dry chain; see product note fork P1).
|
||||||
|
- [ ] "Re-capture from source" action: regenerate a provenanced sample by re-running
|
||||||
|
its recorded capture request against the source's **current** state; update the
|
||||||
|
bank file + Sample in place. **Bank-only — never inserts/re-places into the
|
||||||
|
timeline** (load-bearing principle). Reports if the source drifted since capture.
|
||||||
|
- [ ] Verify: re-capture of an unchanged source is byte-identical to the original
|
||||||
|
capture (bit-identical repeats); non-destructive (`FxBypassGuard` snapshot/restore
|
||||||
|
as M7); relative-paths-only preserved.
|
||||||
|
|
||||||
|
**Dual-canvas reconciliation (forks for Daniel — `docs/product/provenance.md`):**
|
||||||
|
Under the leaned path (bank-only re-capture) provenance is **pure per-sample bank
|
||||||
|
metadata**, `bank_model` and `view_mode_model` **stay decoupled**, and M10 touches
|
||||||
|
no canvas code — compliance is satisfied by staying on the right side of the
|
||||||
|
capture-never-places line. Forks P3 (canvas/lane memory in provenance) and P4
|
||||||
|
(re-capture auto-tag interaction) only wake up if Daniel wants **P2 =
|
||||||
|
re-capture-and-replace-on-timeline**, which would make re-place a mode-aware
|
||||||
|
placement. Awaiting picks on P1 (fingerprint shape) and P2 (re-capture scope).
|
||||||
|
|
||||||
## Milestone 11 — polish
|
## Milestone 11 — polish
|
||||||
**Goal:** Batch capture (per selected item / per razor area),
|
**Goal:** Batch capture (per selected item / per razor area),
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
# 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> 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.
|
||||||
Reference in New Issue
Block a user