1f24c4b095
Split root CLAUDE.md into 19 per-directory files scoped to their source area. Roll v0 history into docs/ARCHIVE.md; retire CONTEXT.md, CONTEXT-ARCHIVE.md, PLAN.md, COMPLETED.md. Move plan docs under docs/. Rescue 9 live deferrals into docs/TODO.md.
253 lines
14 KiB
Markdown
253 lines
14 KiB
Markdown
# Provenance — product notes
|
|
|
|
Framing, rationale, and the dual-canvas reconciliation behind the reshaped
|
|
**Milestone 10 (provenance)**. The tickable spec's landed history is in
|
|
`docs/ARCHIVE.md` (M10); the architecture detail is in `src/core/model/CLAUDE.md`
|
|
and `src/shell/capture/CLAUDE.md` 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: **IMPLEMENTED (2026-07-26).** Settled 2026-07-23; landed 2026-07-26.
|
|
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.
|