diff --git a/PLAN.md b/PLAN.md index 3f8f5d4..4044378 100644 --- a/PLAN.md +++ b/PLAN.md @@ -23,21 +23,47 @@ state persists via the index. - [ ] Slot model + slot↔sample assignment. - [ ] "Capture to slot N" / "insert slot N" actions, MIDI-bindable. -## Milestone 10 — provenance + null-test verify action -**Goal:** Provenance (parent sample id + FX-chain snapshot) and "re-capture from -source"; ship the null-test verification action. CONTEXT.md §Precision invariants, -Build order 10. -**Verify (in DAW):** **Null test** — a dry offline capture of a range, re-inserted -at its source position, nulls to silence against the source. This action is the -tool's trust anchor and must pass. +## Milestone 10 — provenance (re-capture from source) +**Goal:** Populate `Sample.provenance` (parent sample id + a capture-recipe +fingerprint) on resample-from-sample, and ship a **"re-capture from source"** +action that regenerates a sample from its recorded source. Reconciled with the +dual-canvas (Phase D2) model. CONTEXT.md §Data model, §capture; product framing + +the reconciliation forks in `docs/product/provenance.md`. +**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 - snapshot string). -- [ ] "Re-capture from source" action. -- [ ] Null-test verification action (capture → re-insert at source pos → assert - silence sum). +> **PROPOSAL — pending Daniel's fork picks (2026-07-23).** Reshaped from the old +> "provenance + null-test verify" M10. **Cut (fixed by Daniel):** the null-test +> verification *action* and the true-pre-FX-dry *mechanism* the old note required — +> both dropped, see `docs/product/provenance.md` §What was cut. **Kept:** provenance +> + 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 **Goal:** Batch capture (per selected item / per razor area), diff --git a/docs/product/provenance.md b/docs/product/provenance.md new file mode 100644 index 0000000..b649b2b --- /dev/null +++ b/docs/product/provenance.md @@ -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; // 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.