#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 #include #include 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. For Track scope the shell // uses TrackFX_GetFXName/GetFXGUID/GetEnabled; for Item scope it uses the TakeFX_* // equivalents over the active take's FX chain. Plain data — the shell fills it, the // pure fold turns the vector into the identity string. struct FxIdentityEntry { std::string name; // TrackFX_GetFXName / TakeFX_GetFXName std::string guid; // TrackFX_GetFXGUID / TakeFX_GetFXGUID -> guidToString bool enabled; // TrackFX_GetEnabled / TakeFX_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 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& 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& 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 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 via normalizeSlashes (which lowercases on Windows) so a // slash/case difference never spuriously matches or misses. On Windows both sides // are lowercased before they reach here; on macOS/Linux they are case-exact. std::optional detectParent( const std::vector& sourceItemFiles, const std::vector& bankFiles); } // namespace reasampler