M10: provenance populate + re-capture from source (bank-only)
Pure provenance core (recipe fingerprint, FX-chain identity, parent detection) + shell reads; capture stamps provenance on resample-from-sample; re-capture regenerates a provenanced sample from its source, never touching the timeline. Adds BankIndex/BankBook in-place update. CTest-covered.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#pragma once
|
||||
// provenance — the REAPER-free core behind Milestone 10 (re-capture from source).
|
||||
//
|
||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
||||
// vendor/ includes. Standard library only. The shell (main.cpp / actions.cpp)
|
||||
// gathers the raw inputs from REAPER — the source item media-file names, the
|
||||
// source track FX-chain identity (names / GUIDs / enabled flags), the exact
|
||||
// capture range, scope, tail — and hands plain strings/values here. This module
|
||||
// owns:
|
||||
//
|
||||
// * CaptureRecipe — the recorded capture request PLUS the source FX-chain
|
||||
// identity at capture time. Everything "re-capture from
|
||||
// source" needs to re-run the SAME request against the
|
||||
// source's CURRENT state, and to tell whether the source
|
||||
// drifted since capture.
|
||||
// * the ENCODING of a recipe into the single `Provenance.fxChainSnapshot`
|
||||
// string (M1's field already JSON-round-trips one string,
|
||||
// so the whole thin fingerprint rides in it — no schema
|
||||
// change to Sample).
|
||||
// * fxChainIdentity — folds the shell-gathered FX-chain rows into one identity
|
||||
// string (the drift-detection component of the fingerprint).
|
||||
// * detectParent — the pure parent-detection decision: given the resolved
|
||||
// absolute media-file path(s) of the capture's source item(s)
|
||||
// and the bank's path->sampleId map, decide whether this
|
||||
// capture genuinely derives from a bank sample (P1: identity
|
||||
// by resolved file path only — no fuzzy match, no false
|
||||
// parentage).
|
||||
//
|
||||
// Fork picks (docs/product/provenance.md, settled 2026-07-23): P1 = a THIN
|
||||
// reproducibility fingerprint (drift-detect + re-run the same request), NOT a
|
||||
// serialized FX chunk to restore. P2 = bank-only re-capture. So nothing here
|
||||
// stores a restorable chain, and nothing here reaches into view_mode_model.
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// Capture scope, mirrored from render_settings' CaptureScope but kept independent
|
||||
// here so the pure provenance module does not pull the whole render_settings graph
|
||||
// in. The shell maps its CaptureScope onto this two-value enum. Item = item/take FX
|
||||
// only; Track = item FX + the track's own FX (CLAUDE.md §Capture FX scope).
|
||||
enum class ProvenanceScope {
|
||||
Item,
|
||||
Track,
|
||||
};
|
||||
|
||||
// One FX-chain entry as the shell reads it from REAPER (TrackFX_GetFXName /
|
||||
// TrackFX_GetFXGUID / TrackFX_GetEnabled). Plain data — the shell fills it, the
|
||||
// pure fold turns the vector into the identity string.
|
||||
struct FxIdentityEntry {
|
||||
std::string name; // TrackFX_GetFXName
|
||||
std::string guid; // TrackFX_GetFXGUID -> guidToString (per-instance identity)
|
||||
bool enabled; // TrackFX_GetEnabled
|
||||
};
|
||||
|
||||
// The recorded capture recipe + source FX-chain identity — the thin fingerprint.
|
||||
// Re-capture replays the request fields verbatim against the source's CURRENT
|
||||
// state; fxChainIdentity is compared post-hoc to report drift. Ordinary equality
|
||||
// (via ==) is a full recipe match; fxChainIdentity difference alone is "the source
|
||||
// drifted but the recipe is the same" (the re-run still succeeds, the user is told).
|
||||
struct CaptureRecipe {
|
||||
ProvenanceScope scope = ProvenanceScope::Track;
|
||||
int sourceMode = 0; // reasampler::SourceMode as int (bank_model)
|
||||
|
||||
double startSeconds = 0.0; // exact bounds — no rounding (invariant)
|
||||
double endSeconds = 0.0;
|
||||
|
||||
int tailMode = 0; // reasampler::TailMode as int (render_settings)
|
||||
double tailMs = 0.0;
|
||||
|
||||
int sampleRate = 0; // 0 = follow project rate
|
||||
int channelCount = 2;
|
||||
|
||||
// Canonical GUID strings of the source track(s) the capture came from
|
||||
// (guidString form). Re-capture resolves these back to live tracks.
|
||||
std::vector<std::string> trackGuids;
|
||||
|
||||
// The source FX-chain identity at capture time — the drift component. A folded
|
||||
// string (fxChainIdentity) of the in-scope FX rows. Not a restorable chunk.
|
||||
std::string fxChainIdentity;
|
||||
|
||||
bool operator==(const CaptureRecipe& o) const;
|
||||
bool operator!=(const CaptureRecipe& o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
// Folds the shell-gathered FX rows into ONE identity string. Order-sensitive
|
||||
// (chain order is part of identity), delimited so a name containing the delimiter
|
||||
// cannot forge a different chain (the fields are length-prefixed). Empty vector ->
|
||||
// empty string (a no-FX source has an empty, stable identity). Pure + deterministic.
|
||||
std::string fxChainIdentity(const std::vector<FxIdentityEntry>& entries);
|
||||
|
||||
// Combines several per-track FX-chain identity strings (one per source track, in
|
||||
// track order) into ONE identity, length-prefixing each so two different per-track
|
||||
// partitions can never collide by concatenation (e.g. {"X",""} != {"","X"}). Used
|
||||
// for a multi-track Track-scope capture. A single-track capture combines to a
|
||||
// stable, unambiguous wrapping of its one identity. Pure + deterministic.
|
||||
std::string combineChainIdentities(const std::vector<std::string>& perTrack);
|
||||
|
||||
// Encodes a CaptureRecipe into the single string stored in
|
||||
// Provenance.fxChainSnapshot. Self-describing, versioned, and escape-safe so it
|
||||
// round-trips losslessly through the Sample JSON (which treats the whole thing as
|
||||
// one opaque string value). buildFingerprint(x) then parseFingerprint(...) == x.
|
||||
std::string buildFingerprint(const CaptureRecipe& recipe);
|
||||
|
||||
// Parses a fingerprint produced by buildFingerprint. Returns nullopt on any
|
||||
// malformed / unrecognized-version input (never throws, never UB) so a legacy or
|
||||
// corrupt provenance string degrades to "no recipe" gracefully rather than
|
||||
// mis-driving a re-capture.
|
||||
std::optional<CaptureRecipe> parseFingerprint(const std::string& fingerprint);
|
||||
|
||||
// --- Parent detection (P1: identity by resolved file path) -------------------
|
||||
|
||||
// One bank sample as the detector sees it: its stable id and the ABSOLUTE,
|
||||
// normalized path its file resolves to (the shell resolves relativePath against
|
||||
// the current project dir via resolveBankFile + normalizeSlashes before handing
|
||||
// it here). Plain data so the decision is pure and testable.
|
||||
struct BankFileRef {
|
||||
std::string sampleId;
|
||||
std::string absolutePath; // normalized (forward-slash, no trailing slash)
|
||||
};
|
||||
|
||||
// Decides whether a capture derives from a bank sample.
|
||||
//
|
||||
// RULE (stated for the handoff, honest — no false parentage): a capture derives
|
||||
// from a bank sample iff EVERY source item whose media file could be resolved
|
||||
// points at the SAME bank sample's file (by exact normalized absolute path). If
|
||||
// the source items resolve to files not in the bank, or to MORE THAN ONE distinct
|
||||
// bank sample (ambiguous parentage), no parent is recorded. An empty source-file
|
||||
// set (nothing resolvable) yields no parent.
|
||||
//
|
||||
// sourceItemFiles : normalized absolute paths of the capture's source items'
|
||||
// take media files (the shell gathers + normalizes them). A
|
||||
// file that could not be resolved is simply omitted by the
|
||||
// shell — it never becomes an empty string here.
|
||||
// bankFiles : the active book's samples as BankFileRefs (path -> id).
|
||||
//
|
||||
// Returns the parent sample id, or nullopt when the capture is not a genuine
|
||||
// resample-from-sample. Comparison is exact path identity; the caller normalizes
|
||||
// both sides identically so a slash/case difference never spuriously matches or
|
||||
// misses (case handling is the caller's normalization contract, not decided here).
|
||||
std::optional<std::string> detectParent(
|
||||
const std::vector<std::string>& sourceItemFiles,
|
||||
const std::vector<BankFileRef>& bankFiles);
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user