Cut core/model, reclaim, json, util comment bloat ~26% (comments only, zero code change)

This commit is contained in:
2026-07-29 20:49:06 -04:00
parent 1f24c4b095
commit 65ca1e1f9d
16 changed files with 400 additions and 681 deletions
+4 -11
View File
@@ -1,6 +1,5 @@
// core/json implementation — see json.h. The bodies are the (previously // core/json implementation — see json.h. Any behavioral change here changes
// quintuplicated) bank_model / view_mode_model lexical layer, verbatim; any // every persisted-blob parser that shares this lexical layer at once.
// behavioral change here changes five persisted-blob parsers at once.
#include "core/json/json.h" #include "core/json/json.h"
@@ -11,9 +10,7 @@
namespace reasampler::json { namespace reasampler::json {
// --------------------------------------------------------------------------- // -- emit helpers -------------------------------------------------------
// emit helpers
// ---------------------------------------------------------------------------
void writeEscaped(std::string& out, const std::string& s) { void writeEscaped(std::string& out, const std::string& s) {
out += '"'; out += '"';
@@ -75,9 +72,7 @@ void writeIntArray(std::string& out, const std::vector<int>& v) {
out += ']'; out += ']';
} }
// --------------------------------------------------------------------------- // -- Reader ---------------------------------------------------------------
// Reader
// ---------------------------------------------------------------------------
void Reader::skipWs() { void Reader::skipWs() {
while (!eof()) { while (!eof()) {
@@ -117,7 +112,6 @@ bool Reader::parseString(std::string& out) {
case 'r': out += '\r'; break; case 'r': out += '\r'; break;
case 't': out += '\t'; break; case 't': out += '\t'; break;
case 'u': { case 'u': {
// Decode a \uXXXX escape to its code point.
auto readHex4 = [&](unsigned int& cp) -> bool { auto readHex4 = [&](unsigned int& cp) -> bool {
if (pos_ + 4 > s_.size()) return false; if (pos_ + 4 > s_.size()) return false;
cp = 0; cp = 0;
@@ -149,7 +143,6 @@ bool Reader::parseString(std::string& out) {
return false; // unpaired low surrogate — malformed return false; // unpaired low surrogate — malformed
} }
// Encode codePoint as UTF-8.
if (codePoint <= 0x7F) { if (codePoint <= 0x7F) {
out += static_cast<char>(codePoint); out += static_cast<char>(codePoint);
} else if (codePoint <= 0x7FF) { } else if (codePoint <= 0x7FF) {
+16 -23
View File
@@ -1,22 +1,19 @@
// core/json — the ONE hand-rolled JSON lexical layer (Q-W1; audit T2-02 / §2 // core/json — the ONE hand-rolled JSON lexical layer. Pure: standard library only
// "Parser ×4"). Pure: standard library only — NO REAPER, NO SWELL, NO VST3. // — NO REAPER, NO SWELL, NO VST3.
// //
// This module owns the lexical half of the house JSON dialect: the escape-aware // Owns the lexical half of the house JSON dialect: escape-aware string literals
// string literal (incl. \uXXXX + surrogate pairs re-encoded as UTF-8), the bare // (incl. \uXXXX + surrogate pairs re-encoded as UTF-8), bare scalar tokens, number
// scalar tokens, the number parses (strtod/strtoll with full-token + ERANGE // parsing (strtod/strtoll, full-token + ERANGE rejection), key+':' consumption,
// rejection), key+':' consumption, unknown-value skipping, and the emit side // unknown-value skipping, and the emit side (escaping, %.17g/%d/%lld rendering,
// (escaping, %.17g / %d / %lld number rendering, the scoped object writer). // the scoped object writer). Domain grammars — which keys exist, what shape each
// The DOMAIN grammars — which keys exist, what shape each value takes, what is // value takes — stay in the consumers (bank_model, bank_book, view_mode_model,
// rejected at the model boundary — stay in the consumers (bank_model, bank_book, // owned_manifest, tail_control).
// view_mode_model, owned_manifest, tail_control). One lexical definition means
// the five decoders can no longer drift on tolerance or escaping.
// //
// Byte-compatibility contract (load-bearing): the emit helpers reproduce the // Byte-compatibility contract (load-bearing): the emit helpers reproduce the prior
// prior per-module writers EXACTLY — writeEscaped's escape set, %.17g for // per-module writers EXACTLY — writeEscaped's escape set, %.17g for doubles
// doubles (shortest form that round-trips every IEEE-754 double bit-for-bit), // (shortest form that round-trips every IEEE-754 double bit-for-bit), plain
// plain decimal for ints — so a re-serialized blob is byte-identical to what // decimal for ints — so a re-serialized blob is byte-identical to what the
// the pre-extraction writers produced. This was a structural dedupe, not a // pre-extraction writers produced. Persisted .rpp ext-state must not shift by a byte.
// format change; persisted .rpp ext-state must not shift by a byte.
#pragma once #pragma once
@@ -26,9 +23,7 @@
namespace reasampler::json { namespace reasampler::json {
// --------------------------------------------------------------------------- // -- emit helpers (writer side) ----------------------------------------------
// emit helpers (writer side)
// ---------------------------------------------------------------------------
// Appends `s` as a quoted JSON string literal: the seven short escapes, \uXXXX // Appends `s` as a quoted JSON string literal: the seven short escapes, \uXXXX
// for remaining control chars, everything else verbatim (UTF-8 passes through). // for remaining control chars, everything else verbatim (UTF-8 passes through).
@@ -88,9 +83,7 @@ private:
bool first_ = true; bool first_ = true;
}; };
// --------------------------------------------------------------------------- // -- Reader — the lexical cursor (parser side) -------------------------------
// Reader — the lexical cursor (parser side)
// ---------------------------------------------------------------------------
// //
// Every method returns false on malformed input and never reads out of bounds. // Every method returns false on malformed input and never reads out of bounds.
// Only the subset the house writers emit is supported. The reader borrows the // Only the subset the house writers emit is supported. The reader borrows the
+18 -39
View File
@@ -5,19 +5,13 @@
// bank_book implementation — the registry RULES half: construction, pool // bank_book implementation — the registry RULES half: construction, pool
// privileges, bank lifecycle, active bank, sample movement/removal, slot order, // privileges, bank lifecycle, active bank, sample movement/removal, slot order,
// and the reference queries. The JSON round-trip half (serialize / deserialize — // and the reference queries. The JSON round-trip half lives in bank_book_json.cpp,
// Q-W1's golden-literal-pinned byte format) lives in bank_book_json.cpp, compiled // compiled into the same target. The one symbol both halves share is the private
// into the same bank_book target (the slot_map extraction shape: same header, a // static BankBook::nameKey display-name folding rule (declared in bank_book.h).
// second TU). The one symbol both halves share is the private static
// BankBook::nameKey display-name folding rule (declared in bank_book.h).
namespace reasampler { namespace reasampler {
// SlotMap lives in core/model/slot_map.cpp (extracted Q-W1, T4-05). // -- construction + bank lookup ----------------------------------------------
// ---------------------------------------------------------------------------
// BankBook — construction + bank lookup
// ---------------------------------------------------------------------------
BankBook::BankBook() { BankBook::BankBook() {
Bank pool; Bank pool;
@@ -59,9 +53,7 @@ const Bank& BankBook::pool() const {
return *bank(kPoolBankId); return *bank(kPoolBankId);
} }
// --------------------------------------------------------------------------- // -- Ordinal normalization ----------------------------------------------------
// Ordinal normalization
// ---------------------------------------------------------------------------
void BankBook::normalizeOrdinals() { void BankBook::normalizeOrdinals() {
// Stable-sort by ordinal with the pool pinned first, then rewrite ordinals to a // Stable-sort by ordinal with the pool pinned first, then rewrite ordinals to a
@@ -75,16 +67,13 @@ void BankBook::normalizeOrdinals() {
banks_[i].ordinal = static_cast<int>(i); banks_[i].ordinal = static_cast<int>(i);
} }
// --------------------------------------------------------------------------- // -- Display-name uniqueness (trimmed + case-insensitive, ASCII) --------------
// Display-name uniqueness (trimmed + case-insensitive, ASCII)
// ---------------------------------------------------------------------------
// Folds a display name to its uniqueness key: strip leading/trailing ASCII // Folds a display name to its uniqueness key: strip leading/trailing ASCII
// whitespace, lower-case ASCII letters. So "Drums", "drums", and " Drums " share one // whitespace, lower-case ASCII letters — so "Drums"/"drums"/" Drums " share one
// key and cannot coexist. ASCII-only by design — the pure core carries no locale // key. ASCII-only by design — the pure core carries no locale facility; bank names
// facility and must not grow one; bank names are short user labels, not full Unicode // are short user labels, not Unicode case-folding candidates. Private static: the
// case-folding candidates. Private static member (Q-W5): the one folding rule shared // one folding rule shared with bank_book_json.cpp's parse-time coalesce.
// with bank_book_json.cpp's parse-time duplicate-display-name coalesce.
std::string BankBook::nameKey(const std::string& s) { std::string BankBook::nameKey(const std::string& s) {
std::size_t b = 0, e = s.size(); std::size_t b = 0, e = s.size();
auto isWs = [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }; auto isWs = [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; };
@@ -109,9 +98,7 @@ bool BankBook::displayNameTaken(const std::string& name, const std::string& exce
return false; return false;
} }
// --------------------------------------------------------------------------- // -- Bank lifecycle ------------------------------------------------------------
// Bank lifecycle
// ---------------------------------------------------------------------------
bool BankBook::createBank(const std::string& id, const std::string& displayName) { bool BankBook::createBank(const std::string& id, const std::string& displayName) {
if (id.empty()) return false; // ids key the registry if (id.empty()) return false; // ids key the registry
@@ -208,9 +195,7 @@ bool BankBook::evacuate(const std::string& id) {
return true; return true;
} }
// --------------------------------------------------------------------------- // -- Active bank ----------------------------------------------------------------
// Active bank
// ---------------------------------------------------------------------------
bool BankBook::setActiveBank(const std::string& id) { bool BankBook::setActiveBank(const std::string& id) {
if (bank(id) == nullptr) return false; // unknown id never corrupts state if (bank(id) == nullptr) return false; // unknown id never corrupts state
@@ -227,9 +212,7 @@ const BankModel& BankBook::activeIndex() const {
return bank(activeBankId_)->index; return bank(activeBankId_)->index;
} }
// --------------------------------------------------------------------------- // -- Sample movement (index-only) -----------------------------------------------
// Sample movement (index-only)
// ---------------------------------------------------------------------------
namespace { namespace {
@@ -276,9 +259,7 @@ TransferResult BankBook::copySample(const std::string& sampleId,
return applyDestAdd(to->index, copy, TransferResult::Copied); return applyDestAdd(to->index, copy, TransferResult::Copied);
} }
// --------------------------------------------------------------------------- // -- Sample removal (index-only) + the last-reference query ---------------------
// Sample removal (index-only) + the last-reference query
// ---------------------------------------------------------------------------
RemoveResult BankBook::removeSample(const std::string& sampleId, RemoveResult BankBook::removeSample(const std::string& sampleId,
const std::string& fromBankId, const std::string& fromBankId,
@@ -310,9 +291,7 @@ bool BankBook::updateSampleInPlace(const std::string& sampleId, const Sample& up
return false; // no bank holds the id return false; // no bank holds the id
} }
// --------------------------------------------------------------------------- // -- Sample display order — SlotMap driven, index membership untouched -----------
// Sample display order (L7) — SlotMap driven, index membership untouched
// ---------------------------------------------------------------------------
namespace { namespace {
@@ -323,9 +302,9 @@ std::vector<std::string> indexIds(const BankModel& idx) {
return ids; return ids;
} }
// Squares one bank's SlotMap with its index membership. A map with NO overlap with the // Squares one bank's SlotMap with its index membership. A map with NO overlap with
// index (the pre-L7 migration case, or a freshly-constructed bank) is seeded dense from // the index (a bank with no persisted slot data, or freshly constructed) is seeded
// insertion order; an existing map is reconciled (drop stale markers, append unmapped). // dense from insertion order; an existing map is reconciled (drop stale, append unmapped).
void reconcileBankSlots(Bank& b) { void reconcileBankSlots(Bank& b) {
const std::vector<std::string> live = indexIds(b.index); const std::vector<std::string> live = indexIds(b.index);
if (b.slots.empty()) { if (b.slots.empty()) {
+108 -208
View File
@@ -1,41 +1,13 @@
#pragma once #pragma once
// bank_book — the pure core of the multi-bank phase (Phase B), deliberately free // bank_book — pure multi-bank registry: wraps N BankModel instances (bank_model
// of any REAPER type so it compiles and unit-tests OUTSIDE the DAW. It is the // itself is untouched — additive, no bankId on Sample). Movement between banks is
// third instance of the same "pure registry + JSON round-trip, unit-tested outside // index-only; files never relocate, banks are logical groupings over one shared pool.
// the DAW" pattern as bank_model and view_mode_model.
// //
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO // The pool is bank-zero (fixed id/name, ordinal 0), privileged and enforced HERE:
// vendor/ includes. Standard library only. // always exists, un-deletable, un-renamable, un-evacuable (evacuate's destination
// // only). createBank takes a caller-supplied id — REAPER GUID minting stays in the
// -- What it is -------------------------------------------------------------- // shell so this model stays pure and deterministic; the model still enforces
// // non-empty/unique/not-reserved.
// An ordered registry of banks. Each bank = { stable id, display name, ordinal,
// BankModel }. The book WRAPS N BankModel instances — bank_model / BankModel are
// UNTOUCHED (additive: no bankId on Sample). Movement of samples between banks is
// index-only (remove from source's BankModel, add to destination's); files never
// relocate — banks are logical groupings over one shared file pool.
//
// -- The pool (privileged, not special-cased) --------------------------------
//
// Structurally the pool is bank-zero — one Bank among many, seeded on construction
// with a fixed id (kPoolBankId) and fixed display name (kPoolBankName), ordinal 0.
// Semantically it is privileged, and the privileges are enforced HERE in the pure
// rules layer (CONTEXT.md §Multi-bank guardrail — not deferred to a shell):
// * always exists (seeded on construction; the book never reaches zero banks)
// * un-deletable (deleteBank rejects the pool)
// * un-renamable (renameBank rejects the pool)
// * un-evacuable (evacuate rejects the pool — the pool is evacuation's
// destination, not a source)
//
// -- Id minting is the CALLER'S job (design decision) ------------------------
//
// createBank takes a caller-supplied stable id, mirroring bank_model's "id
// assigned by the caller" and view_mode_model's mode ids. The pure core has no
// REAPER genGuid / RNG and deliberately introduces none: a fake in-model id source
// would not be a real GUID anyway, and keeping ids caller-supplied lets the B2
// shell mint a genuine REAPER GUID while the model stays pure and deterministically
// testable. The model still enforces the invariants: non-empty, unique, not the
// reserved pool id.
#include <optional> #include <optional>
#include <string> #include <string>
@@ -47,26 +19,22 @@
namespace reasampler { namespace reasampler {
// Q-W1 interim: this god module re-namespaces in its own split wave; until then the // Interim: this module re-namespaces later; the model types it wraps live in
// clean model types it wraps live in reasampler::model. // reasampler::model.
using namespace model; using namespace model;
// The pool's fixed identity. The id is reserved: createBank rejects it, and the // The pool's fixed identity: createBank rejects this id; renameBank rejects this name.
// pool is always bank-zero. The name is fixed: renameBank rejects the pool.
inline constexpr const char* kPoolBankId = "pool"; inline constexpr const char* kPoolBankId = "pool";
inline constexpr const char* kPoolBankName = "Pool"; inline constexpr const char* kPoolBankName = "Pool";
// SlotMap — extracted to its own TU/header pair (Q-W1, T4-05): core/model/slot_map.h. // One bank: id/display/ordinal/BankModel/SlotMap. The pool is the bank whose
// Included above because Bank carries one per bank. // id == kPoolBankId.
// One bank: a stable id, a display name, an ordinal (tab/display order), and its
// own BankModel. The pool is the bank whose id == kPoolBankId.
struct Bank { struct Bank {
std::string id; // stable, persisted; the pool's is kPoolBankId std::string id; // stable, persisted; the pool's is kPoolBankId
std::string displayName; // mutable for named banks; fixed "Pool" for the pool std::string displayName; // mutable for named banks; fixed "Pool" for the pool
int ordinal = 0; // display order; pool is 0, named banks 1..N int ordinal = 0; // display order; pool is 0, named banks 1..N
BankModel index; // this bank's samples BankModel index; // this bank's samples
SlotMap slots; // L7 display positions of this bank's samples (gap-preserving) SlotMap slots; // display positions of this bank's samples (gap-preserving)
bool isPool() const { return id == kPoolBankId; } bool isPool() const { return id == kPoolBankId; }
@@ -76,16 +44,12 @@ struct Bank {
} }
}; };
// Outcome of a cross-bank sample move/copy. Mirrors AddResult's honesty: the op // Outcome of a cross-bank move/copy — reports what happened rather than mutating
// reports what happened rather than silently mutating on a bad request. // silently on a bad request.
// - Moved / Copied: the sample was transferred to the destination as a new entry. // - Moved / Copied: transferred to the destination as a new entry.
// - Collapsed: the destination already held the hash; it collapsed onto the // - Collapsed: destination already held the hash, collapsed onto it (move
// existing entry (a no-op add on the destination side). For a // still removes the source; copy keeps it, as always).
// MOVE the source entry is STILL removed; for a COPY the source // - RejectedUnknownBank / RejectedSampleAbsent / RejectedSameBank: no-op guards.
// entry is (as always) retained.
// - RejectedUnknownBank: a source or destination id named no bank.
// - RejectedSampleAbsent: the sample id was not in the source bank.
// - RejectedSameBank: source and destination were the same bank (no-op).
enum class TransferResult { enum class TransferResult {
Moved, Moved,
Copied, Copied,
@@ -95,21 +59,18 @@ enum class TransferResult {
RejectedSameBank, RejectedSameBank,
}; };
// Scope of a sample-remove (fork R-A, settled 2026-07-24). ThisBank is the default // Scope of a sample-remove. ThisBank is the only behavior surfaced in the UI;
// and the ONLY behavior surfaced in the UI/action layer; AllBanks is a latent seam // AllBanks is a tested latent seam, not wired to any affordance.
// live and tested at the model level, promotable later behind this parameter without // - ThisBank: drop the entry from the one named source bank only (no cross-bank
// a rewrite, but never wired to an affordance in B5. // cascade — dedup is per-bank).
// - ThisBank: drop the entry from the one named source bank only. A same-hash entry // - AllBanks: drop the sample's entry from every bank holding it ("purge from
// in another bank survives (no cross-bank cascade — dedup is per-bank). // the library").
// - AllBanks: drop the sample's entry from EVERY bank that holds the source id
// ("purge from the library"). Latent; unsurfaced.
enum class RemoveScope { enum class RemoveScope {
ThisBank, ThisBank,
AllBanks, AllBanks,
}; };
// Outcome of BankBook::removeSample. Mirrors TransferResult's honesty: the op reports // Outcome of BankBook::removeSample — same honesty as TransferResult.
// what happened rather than silently mutating on a bad request.
// - Removed: at least one index entry was dropped. // - Removed: at least one index entry was dropped.
// - RejectedUnknownBank: the source bank id named no bank (ThisBank scope only). // - RejectedUnknownBank: the source bank id named no bank (ThisBank scope only).
// - RejectedSampleAbsent: the sample id was in no bank in scope (nothing removed). // - RejectedSampleAbsent: the sample id was in no bank in scope (nothing removed).
@@ -120,8 +81,7 @@ enum class RemoveResult {
}; };
// An ordered registry of banks with the pool seeded as bank-zero, per-bank sample // An ordered registry of banks with the pool seeded as bank-zero, per-bank sample
// indices, an active-bank pointer, and lossless JSON round-trip. The heart of the // indices, an active-bank pointer, and lossless JSON round-trip.
// multi-bank phase — mirror of bank_model / view_mode_model.
class BankBook { class BankBook {
public: public:
BankBook(); // seeds the pool (id kPoolBankId, name kPoolBankName, ordinal 0); BankBook(); // seeds the pool (id kPoolBankId, name kPoolBankName, ordinal 0);
@@ -129,34 +89,29 @@ public:
// -- Bank lifecycle ------------------------------------------------------ // -- Bank lifecycle ------------------------------------------------------
// Creates a named bank with the caller-supplied stable id and display name, // Creates a named bank with a caller-supplied id/display name (next ordinal
// assigning the next ordinal. Rejects (returns false, no mutation) an empty id, // assigned automatically). Rejects (false, no mutation) an empty/duplicate id,
// a duplicate id, the reserved pool id, or a display name that duplicates an // the reserved pool id, or a duplicate display name (trimmed + case-insensitive,
// existing bank's name (including the pool's "Pool"). Display-name uniqueness is // ASCII — "Drums"/"drums"/" Drums " collide, including against the pool's "Pool").
// trimmed + case-insensitive (ASCII): "Drums", "drums", and " Drums " collide.
bool createBank(const std::string& id, const std::string& displayName); bool createBank(const std::string& id, const std::string& displayName);
// Renames a named bank. Rejects (false, no mutation) an unknown id, the pool, or a // Renames a named bank. Rejects (false, no mutation) an unknown id, the pool, or
// target name already used by a DIFFERENT bank (trimmed + case-insensitive, as // a name already used by another bank. Renaming to its own current name is a
// createBank). Renaming a bank to its own current name is a no-op success. // no-op success.
bool renameBank(const std::string& id, const std::string& displayName); bool renameBank(const std::string& id, const std::string& displayName);
// Deletes a NAMED bank, removing it (and its member index entries) from the // Deletes a named bank and its member entries (files untouched — a shell/prune
// registry. Files are a shell/prune concern and are NOT touched here. Rejects // concern). Rejects (false, no mutation) an unknown id or the pool. Remaining
// (false, no mutation) an unknown id or the pool. Remaining banks' ordinals are // ordinals compact after; if the deleted bank was active, falls back to the pool.
// compacted so the pool stays 0 and named banks stay contiguous 1..N. If the
// deleted bank was active, the active bank falls back to the pool.
bool deleteBank(const std::string& id); bool deleteBank(const std::string& id);
// Reorders a NAMED bank to `newOrdinal` (clamped into the named-bank range), // Reorders a named bank to newOrdinal (clamped into range, others shift to stay
// shifting the others to keep ordinals contiguous. The pool is pinned at 0 and // contiguous). The pool is pinned at 0. Rejects an unknown id or the pool.
// cannot be reordered. Rejects (false, no mutation) an unknown id or the pool.
bool reorderBank(const std::string& id, int newOrdinal); bool reorderBank(const std::string& id, int newOrdinal);
// Moves EVERY member of a named bank into the pool (index-only, observing the // Moves every member of a named bank into the pool (index-only, same
// same destination-collapse-by-hash as a move), leaving the bank empty. Rejects // destination-collapse-by-hash as a move). Rejects an unknown id or the pool
// (false, no mutation) an unknown id or the pool (the pool is the destination, // (the pool is only ever a destination). Returns true even if already empty.
// never a source). Returns true on success even if the bank was already empty.
bool evacuate(const std::string& id); bool evacuate(const std::string& id);
// -- Active bank --------------------------------------------------------- // -- Active bank ---------------------------------------------------------
@@ -164,123 +119,83 @@ public:
// The active bank's id (the capture target). Defaults to the pool. // The active bank's id (the capture target). Defaults to the pool.
const std::string& activeBankId() const { return activeBankId_; } const std::string& activeBankId() const { return activeBankId_; }
// Sets the active bank. Rejects (returns false, no change) an id that names no // Sets the active bank. Rejects (false, no change) an id that names no bank.
// bank — an invalid set never corrupts state.
bool setActiveBank(const std::string& id); bool setActiveBank(const std::string& id);
// The active bank's BankModel — the index the capture layer adds to. Always // The active bank's BankModel — always valid (falls back to the pool).
// valid (the active id always names a live bank; it falls back to the pool).
BankModel& activeIndex(); BankModel& activeIndex();
const BankModel& activeIndex() const; const BankModel& activeIndex() const;
// -- Sample movement (index-only; files never relocate) ------------------ // -- Sample movement (index-only; files never relocate) ------------------
// Moves a sample by id from `fromBankId` to `toBankId`: removes it from the // Moves a sample by id between banks (destination collapse-by-hash observed).
// source index and adds it to the destination (observing destination // See TransferResult for the full outcome set.
// collapse-by-hash). See TransferResult for the full outcome set.
TransferResult moveSample(const std::string& sampleId, TransferResult moveSample(const std::string& sampleId,
const std::string& fromBankId, const std::string& fromBankId,
const std::string& toBankId); const std::string& toBankId);
// Copies a sample by id from `fromBankId` to `toBankId`: the source entry is // Copies a sample by id between banks, source retained (destination
// retained, the destination gains it (observing destination collapse-by-hash). // collapse-by-hash observed). Cross-bank dedup is NOT enforced — the same hash
// Same hash may then live in both banks — cross-bank dedup is NOT enforced. // may then live in both banks.
TransferResult copySample(const std::string& sampleId, TransferResult copySample(const std::string& sampleId,
const std::string& fromBankId, const std::string& fromBankId,
const std::string& toBankId); const std::string& toBankId);
// -- Sample removal (index-only; the file is NEVER touched — orphaned until prune) -- // -- Sample removal (index-only; the file is NEVER touched — orphaned until prune) --
// Drops a sample's index entry (the sample-level sibling of move/copy/evacuate). // Drops a sample's index entry — non-destructive to the file (a last-reference
// Index-only and non-destructive to the file: a last-reference remove leaves the // remove leaves it on disk, orphaned until prune reclaims it). See RemoveScope/
// file on disk, orphaned until Phase R prune — remove NEVER deletes bytes. // RemoveResult for scope and outcome. No mutation on any Rejected outcome.
//
// Scope (fork R-A): ThisBank (default, the only surfaced verb) drops the entry from
// `fromBankId` alone; AllBanks (latent seam) drops the sample id from every bank
// that holds it. See RemoveResult for the outcome set.
// * ThisBank: RejectedUnknownBank if `fromBankId` names no bank; RejectedSampleAbsent
// if that bank does not hold the id; Removed on a drop.
// * AllBanks: `fromBankId` is ignored (the id is purged book-wide);
// RejectedSampleAbsent if NO bank held the id; Removed otherwise.
// No mutation occurs on any Rejected outcome (no-op guardrail for the undo layer).
RemoveResult removeSample(const std::string& sampleId, RemoveResult removeSample(const std::string& sampleId,
const std::string& fromBankId, const std::string& fromBankId,
RemoveScope scope = RemoveScope::ThisBank); RemoveScope scope = RemoveScope::ThisBank);
// -- Sample display order (L7; index membership untouched) --------------- // -- Sample display order (index membership untouched) -------------------
// The bank's sample ids in DISPLAY (slot) order — the deterministic order the grid // The bank's sample ids in display (slot) order, reconciled against live index
// iterates, sourced from the bank's SlotMap. Reconciles the map against live index // membership first (drops stale markers, appends unmapped samples densely). An
// membership first (drops stale markers, appends unmapped samples densely), so a // unknown bank id yields an empty vector.
// freshly-migrated or out-of-band-mutated bank always yields a complete order. An
// unknown bank id yields an empty vector. Const-logical but reconciles lazily, so
// it is a non-const member.
std::vector<std::string> orderedSampleIds(const std::string& bankId); std::vector<std::string> orderedSampleIds(const std::string& bankId);
// Ensures every bank's SlotMap is consistent with its index membership: seeds a // Ensures every bank's SlotMap is consistent with its index membership (seeds a
// map that has NO overlap with its index from insertion order (the pre-L7 migration // dense order, or reconciles a partial map). Idempotent — call after deserialize
// default — dense, no gaps), and reconciles a partially-populated map (drop stale, // or any out-of-band membership change.
// append unmapped). Idempotent. Called after deserialize and after any capture/
// transfer that added samples out-of-band of the L7 reorder path.
void reconcileSlots(); void reconcileSlots();
// Reorders sample `id` within `bankId` to `targetSlot` (gap-preserving; see // Reorders sample `id` within `bankId` to targetSlot (gap-preserving; index/file/
// SlotMap::reorder). INDEX-ONLY of positions — the sample's membership, file, and // metadata untouched). Returns false (no mutation) on an unknown bank or id.
// metadata are untouched (capture != placement holds). Reconciles the bank's slots
// first so the target space is complete. Returns false (no mutation) on an unknown
// bank or an id the bank does not hold.
bool reorderSample(const std::string& id, const std::string& bankId, int targetSlot); bool reorderSample(const std::string& id, const std::string& bankId, int targetSlot);
// Alt-replace (L7 F3): the dragged sample `newId` (already a member of `bankId`) // Alt-replace: the dragged `newId` (already a member of bankId) takes the slot of
// takes the slot of the occupant `oldId`, and `oldId` is REMOVED from `bankId`'s // `oldId`, and `oldId` is removed from the index (same semantics as removeSample
// index (index-only, same semantics as removeSample ThisBank — the file stays on // ThisBank). Position is preserved; only the occupant changes.
// disk; owned-manifest/prune govern bytes; hashReferencedElsewhere handles the
// last-reference case). Position of the slot is preserved; only its occupant changes.
// //
// POOL GUARD (settled): the index-removal of `oldId` passes the SAME guard the // Applies the same pool guard as removeSample — per-sample removal from the pool
// remove verb applies — removeSample(oldId, bankId, ThisBank) must return Removed. // is allowed (the pool's guards are un-delete/rename/evacuate, never per-sample
// For the pool this is permitted whenever the occupant exists (per-sample removal // remove). Rejects (false, no mutation of either index or slots) an unknown bank,
// is not a pool privilege violation — the pool's guards are un-delete/rename/evacuate, // a newId/oldId the bank doesn't hold, or newId == oldId.
// never per-sample remove). If the removal would be rejected (occupant absent), the
// whole replace is rejected: false, NO mutation (neither the index nor the slots
// change), so the shell can fall back to the default insert-shift or a no-op.
// Rejects (false, no mutation) an unknown bank, a `newId`/`oldId` the bank does not
// hold, or `newId == oldId`. NEVER touches disk; introduces no new deletion authority.
bool replaceSample(const std::string& newId, const std::string& oldId, bool replaceSample(const std::string& newId, const std::string& oldId,
const std::string& bankId); const std::string& bankId);
// Refreshes a sample IN PLACE wherever it lives in the book (M10 re-capture): // Refreshes a sample in place wherever it lives (re-capture): finds the bank
// finds the bank holding `sampleId` and replaces its entry with `updated` // holding sampleId and replaces its entry with `updated` (order-preserving, no
// (order-preserving, no dedup — see BankModel::updateInPlace). Scans banks in // dedup). Updates the FIRST holder in ordinal order if the id lives in multiple
// ordinal order and updates the FIRST holder (a sample id is unique within a // banks via copy. Returns false (no mutation) if no bank holds the id or the
// bank; the same id living in two banks via copy would update the earliest, which // replacement's path is absolute.
// is acceptable — re-capture operates on the panel's focused single selection).
// Returns false (no mutation) if no bank holds the id or the replacement's path
// is absolute. Index-only and non-destructive to the timeline.
bool updateSampleInPlace(const std::string& sampleId, const Sample& updated); bool updateSampleInPlace(const std::string& sampleId, const Sample& updated);
// Reference-count query backing the confirm-on-last-reference guardrail: does any // Does any bank other than exceptBankId still hold an entry whose contentHash
// bank OTHER than `exceptBankId` still hold an entry whose contentHash == `hash`? // == hash? Backs the confirm-on-last-reference guardrail: two entries sharing a
// // hash share one file, so this answers "would removing here orphan the file."
// Identity is the CONTENT HASH, not the file path: hash is the canonical dedup key // An empty hash never matches (mirrors findByHash) — reads as
// the whole model already reasons in (findByHash / collapse-by-hash), and two // referenced-nowhere-else, the safe confirm-eliciting default.
// entries that share content share one file — so "some other bank still references
// this hash" is exactly "removing here does not orphan the file." An EMPTY hash is
// never matched (it does not participate in dedup, mirroring findByHash), so an
// empty-hash sample reads as referenced-nowhere-else — the safe, confirm-eliciting
// direction (we cannot prove another bank shares an unhashed file).
bool hashReferencedElsewhere(const std::string& hash, bool hashReferencedElsewhere(const std::string& hash,
const std::string& exceptBankId) const; const std::string& exceptBankId) const;
// Every project-relative file path referenced by ANY bank in the book, pool // Every project-relative path referenced by any bank (pool included) — the union
// included — the union across the whole book (Phase R, prune). This is the // prune subtracts against. Paths are verbatim (no normalization), first-seen
// safety-critical referenced-set the prune core subtracts: a file referenced by // order across banks in ordinal then insertion order, de-duplicated. An empty
// any bank (INCLUDING via a copy into a second bank) appears here, so prune never // relativePath is skipped.
// reclaims it. Paths are returned VERBATIM (Sample.relativePath, exact strings —
// no normalization), first-seen order across banks in ordinal order then sample
// insertion order, and DE-DUPLICATED (one file referenced by N banks appears
// once). An empty relativePath is skipped (it references no file). Additive
// read-only query; adds no mutation and no coupling to Phase R.
std::vector<std::string> referencedPaths() const; std::vector<std::string> referencedPaths() const;
// -- Query --------------------------------------------------------------- // -- Query ---------------------------------------------------------------
@@ -308,33 +223,25 @@ public:
// -- Persistence --------------------------------------------------------- // -- Persistence ---------------------------------------------------------
// Serializes the whole book to a JSON string (lossless round-trip): the pool // Serializes the whole book to JSON (lossless): pool as bank-zero + named banks
// folded in as bank-zero + named banks + per-bank indices + ordinals + active // + per-bank indices + ordinals + active id. deserialize(serialize(x)) == x.
// id. deserialize(serialize(x)) == x.
std::string serialize() const; std::string serialize() const;
// Parses a book JSON produced by serialize(). std::nullopt on malformed input. // Parses a book JSON produced by serialize(). std::nullopt on malformed input.
// //
// LEGACY MIGRATION: a bare legacy bank_index JSON (the pre-multi-bank shape, an // A bare legacy bank_index JSON (pre-multi-bank shape: a "samples" array, no
// object with a "samples" array and no "banks" key) is promoted into the pool's // "banks" key) is promoted into the pool's index — one-way, lossless — yielding
// index, yielding a book of { pool } with zero named banks — one-way, lossless. // a book of { pool } with zero named banks.
// After migration the book blob is authoritative (the caller persists the book
// shape going forward; the legacy key is retired by the B2 shell).
static std::optional<BankBook> deserialize(const std::string& json); static std::optional<BankBook> deserialize(const std::string& json);
// Resolve a BankBook from the two persisted ext-state values a project may carry: // Resolves a BankBook from the two persisted ext-state values a project may
// the authoritative `banks` blob and the retired-but-possibly-present legacy // carry: the authoritative `banksJson` and the retired legacy `bank_index` blob.
// `bank_index` blob. The persist shell (B2) hands both raw strings straight here so // 1. non-empty banksJson -> deserialize it. If malformed, do NOT fall back to
// the load-source decision stays REAPER-free and unit-tested. Precedence: // legacy — a corrupt banks blob is an error, not an absence; returns an
// 1. non-empty `banksJson` present -> deserialize it (authoritative). If it is // empty book so a stale legacy key can never resurrect superseded state.
// MALFORMED, do NOT silently fall back to the legacy blob — a corrupt `banks` // 2. else non-empty legacyJson -> deserialize it (pool migration).
// blob is an error, not an absence; return an empty book so a stale legacy key // 3. else -> a fresh empty book (pool only).
// can never resurrect a superseded single-bank state over a broken book. // Never returns nullopt — an unloadable input degrades to the empty book.
// 2. else non-empty `legacyJson` -> deserialize it (one-way pool migration).
// 3. else (both absent/empty) -> a fresh empty book (pool only).
// Never returns nullopt: an unloadable input degrades to the empty book (matching
// the shell's existing "malformed -> ignore, start empty" behaviour), so the caller
// has one branchless install path.
static BankBook loadFromPersisted(const std::string& banksJson, static BankBook loadFromPersisted(const std::string& banksJson,
const std::string& legacyJson); const std::string& legacyJson);
@@ -343,41 +250,34 @@ private:
std::string activeBankId_; // always names a live bank; defaults to pool std::string activeBankId_; // always names a live bank; defaults to pool
// Folds a display name to its uniqueness key: strip leading/trailing ASCII // Folds a display name to its uniqueness key: strip leading/trailing ASCII
// whitespace, lower-case ASCII letters. So "Drums", "drums", and " Drums " share // whitespace, lower-case ASCII letters — so "Drums"/"drums"/" Drums " share one
// one key and cannot coexist. ASCII-only by design — the pure core carries no // key. ASCII-only by design — the pure core carries no locale facility. Private
// locale facility and must not grow one. A private STATIC member (Q-W5, settled) // static because both halves of the split implementation (rules + JSON) need the
// because BOTH halves of the split implementation need the ONE folding rule: the // one folding rule; a drifted second copy would let a parsed book violate the
// rules TU (bank_book.cpp, displayNameTaken) and the JSON TU (bank_book_json.cpp, // create/rename uniqueness invariant.
// deserialize's duplicate-display-name coalesce) — a drifted second copy would let
// a parsed book violate the create/rename uniqueness invariant.
static std::string nameKey(const std::string& s); static std::string nameKey(const std::string& s);
// True if a bank OTHER than `exceptId` already carries `name`'s uniqueness key // True if a bank other than exceptId already carries name's uniqueness key.
// (trimmed + case-insensitive, ASCII). Backs the create/rename uniqueness check; // Backs the create/rename uniqueness check; pass exceptId=id to let a bank keep
// pass exceptId=id to let a bank keep (or re-case/-space) its own name. // (or re-case/-space) its own name.
bool displayNameTaken(const std::string& name, const std::string& exceptId) const; bool displayNameTaken(const std::string& name, const std::string& exceptId) const;
// Re-sorts banks_ by ordinal (pool pinned first) and rewrites ordinals to a // Re-sorts banks_ by ordinal (pool pinned first) and rewrites ordinals to a
// contiguous 0..N-1 so the pool is 0 and named banks are 1..N. Called after any // contiguous 0..N-1. Called after any structural change (create/delete/reorder).
// structural change (create / delete / reorder).
void normalizeOrdinals(); void normalizeOrdinals();
// Replaces the book's banks with a parsed set, normalizes ordinals, and resolves // Replaces the book's banks with a parsed set, normalizes ordinals, and resolves
// the active bank (falling back to the pool if the id names no bank). Used only // the active bank (falling back to the pool if the id names no bank). Used only
// by deserialize; kept private so the public surface stays create/rename/etc. // by deserialize.
void adoptBanks(std::vector<Bank>&& banks, const std::string& activeBank); void adoptBanks(std::vector<Bank>&& banks, const std::string& activeBank);
}; };
// The next bank id to activate when cycling the active bank forward, in ordinal // The next bank id to activate when cycling the active bank forward, in ordinal
// order (the ids arrive pool-first, named 1..N, matching banks()). Wraps: the id // order (pool -> named -> ... -> pool, wraps). Free function (not a member) so it's
// after the last returns the first (pool → named → … → pool). This is the pure // unit-testable against a bare id vector without a full book.
// decision behind the "cycle active bank" action — the shell reads the book's
// ordered bank ids + current active id, asks for the next, and activates it.
// * empty list -> "" (nothing to cycle to) // * empty list -> "" (nothing to cycle to)
// * single id (pool-only) -> that id (a one-bank book stays put) // * single id (pool-only) -> that id (a one-bank book stays put)
// * currentBankId not present -> the first id (a sane home to jump to) // * currentBankId not present -> the first id (a sane home to jump to)
// Exposed as a free function (not a BankBook member) so it is unit-testable against
// a bare id vector without a full book. Mirror of view_mode_model's nextModeId.
std::string nextBankId(const std::vector<std::string>& orderedBankIds, std::string nextBankId(const std::vector<std::string>& orderedBankIds,
const std::string& currentBankId); const std::string& currentBankId);
+27 -43
View File
@@ -6,33 +6,25 @@
#include "core/json/json.h" #include "core/json/json.h"
// bank_book JSON round-trip (Q-W5 extraction out of bank_book.cpp — same header, // bank_book JSON round-trip — a sibling TU to bank_book.cpp, sharing its header
// compiled into the same bank_book target; the slot_map second-TU shape). The // and target. The registry RULES half stays in bank_book.cpp; the one shared
// registry RULES half stays in bank_book.cpp; the ONE shared symbol is the private // symbol is the private static BankBook::nameKey folding rule — the parse-time
// static BankBook::nameKey folding rule (declared in bank_book.h) — the parse-time // duplicate-display-name coalesce below must fold names EXACTLY as create/rename
// duplicate-display-name coalesce below must fold names EXACTLY as the create/rename // uniqueness does, or a parsed book could violate the in-model invariant.
// uniqueness check does, or a parsed book could violate the in-model invariant.
// //
// JSON rides on the shared core/json lexical layer (Q-W1), matching bank_model // The book blob nests one bank object per bank, each carrying that bank's
// and view_mode_model. The book blob nests one bank object per bank, each carrying that // BankModel serialized by bank_model's OWN writer, so per-bank sample
// bank's BankModel serialized by bank_model's OWN writer (BankModel::serialize), // serialization stays owned by bank_model and is not duplicated here. The book
// so per-bank sample serialization stays owned by bank_model and is not duplicated // writer emits the bank envelope (id / displayName / ordinal) plus a raw "index"
// here. The book writer emits the bank envelope (id / displayName / ordinal) plus a // member whose value is the BankModel blob verbatim; the parser splits the book
// raw "index" member whose value is the BankModel blob verbatim; the parser splits // envelope, then hands each nested index blob straight to BankModel::deserialize.
// the book envelope, then hands each nested index blob straight to
// BankModel::deserialize. Ints use %d; strings are escaped by writeEscaped.
// BYTE-IDENTICAL to the pre-extraction writer — the Q-W1 golden-literal test pins it.
namespace reasampler { namespace reasampler {
// =========================================================================== // -- JSON — writer ----------------------------------------------------------
// JSON — writer
// ===========================================================================
namespace { namespace {
// Shared core/json emit helpers (Q-W1): the same escape set + %d rendering the
// prior file-local writer carried, so the emitted blob is byte-identical.
std::string intToStr(int v) { return json::numToStr(v); } std::string intToStr(int v) { return json::numToStr(v); }
using ObjWriter = json::Writer; using ObjWriter = json::Writer;
@@ -58,8 +50,8 @@ std::string BankBook::serialize() const {
// The nested index is bank_model's own JSON, emitted verbatim so the // The nested index is bank_model's own JSON, emitted verbatim so the
// per-sample shape stays owned by BankModel::serialize (not duplicated). // per-sample shape stays owned by BankModel::serialize (not duplicated).
b.keyRaw("index", banks_[i].index.serialize()); b.keyRaw("index", banks_[i].index.serialize());
// L7 display positions (gap-preserving). Absent on a pre-L7 blob; the // Display positions (gap-preserving). Absent on a pre-existing blob;
// parser defaults such a bank's slots from insertion order on load. // the parser defaults such a bank's slots from insertion order on load.
b.keyRaw("slots", banks_[i].slots.serialize()); b.keyRaw("slots", banks_[i].slots.serialize());
} }
out += ']'; out += ']';
@@ -67,13 +59,10 @@ std::string BankBook::serialize() const {
return out; return out;
} }
// =========================================================================== // -- JSON — parser (recursive descent; std::nullopt on malformed input, never UB) --
// JSON — parser (recursive descent; std::nullopt on any malformed input, never UB)
// ===========================================================================
namespace { namespace {
// The book DOMAIN grammar over the shared core/json lexical layer (Q-W1).
// parseBank parses one bank object; parseSlots the "slots" array ([{id, slot}, // parseBank parses one bank object; parseSlots the "slots" array ([{id, slot},
// ...]) into (id, slot) pairs (empty array valid; the pair-level defensive // ...]) into (id, slot) pairs (empty array valid; the pair-level defensive
// repair — dupes/conflicts — lives in SlotMap::fromEntries); parseBook the root // repair — dupes/conflicts — lives in SlotMap::fromEntries); parseBook the root
@@ -112,9 +101,9 @@ bool parseBank(json::Reader& r, Bank& b) {
b.index = std::move(*idx); b.index = std::move(*idx);
haveIndex = true; haveIndex = true;
} else if (key == "slots") { } else if (key == "slots") {
// L7 display positions. Absent on a pre-L7 blob (the else-branch skips // Display positions. Absent on a pre-existing blob; when present it
// nothing because the key never appears); when present it drives the // drives the bank's SlotMap. reconcileSlots() (post-adopt) squares it
// bank's SlotMap. reconcileSlots() (post-adopt) squares it with membership. // with membership.
std::vector<std::pair<std::string, int>> pairs; std::vector<std::pair<std::string, int>> pairs;
if (!parseSlots(r, pairs)) return false; if (!parseSlots(r, pairs)) return false;
b.slots = SlotMap::fromEntries(pairs); b.slots = SlotMap::fromEntries(pairs);
@@ -186,9 +175,7 @@ bool parseBook(json::Reader& r, const std::string& raw, std::vector<Bank>& banks
} else if (key == "activeBank") { } else if (key == "activeBank") {
if (!r.parseString(activeBank)) return false; if (!r.parseString(activeBank)) return false;
} else if (key == "samples") { } else if (key == "samples") {
// Legacy marker. The legacy index is re-parsed from the whole input below // Legacy marker; the legacy index is re-parsed from the whole input below.
// (BankModel::deserialize owns that shape); here we only skip the value to
// keep the scan well-formed and note that we saw it.
sawSamples = true; sawSamples = true;
if (!r.skipValue()) return false; if (!r.skipValue()) return false;
} else { } else {
@@ -253,23 +240,20 @@ std::optional<BankBook> BankBook::deserialize(const std::string& blob) {
json::Reader r(blob); json::Reader r(blob);
if (!parseBook(r, blob, banks, activeBank)) return std::nullopt; if (!parseBook(r, blob, banks, activeBank)) return std::nullopt;
// --- Coalesce duplicate folded display names (B4 re-review fold-in). -------- // --- Coalesce duplicate folded display names. --------------------------
// The in-model create/rename path enforces unique display names under nameKey, // The in-model create/rename path enforces unique display names under nameKey,
// but a hand-edited .rpp blob can smuggle in two banks whose names fold to the // but a hand-edited .rpp blob can smuggle in two banks whose names fold to the
// same key ("Drums" and " drums "). Rejecting the whole book over one collision // same key ("Drums" and " drums "). Rejecting the whole book over one collision
// would degrade the user's entire library to empty, so instead we AUTO- // would degrade the user's entire library to empty, so instead we AUTO-
// DISAMBIGUATE the later duplicate deterministically: scan in parse order, and // DISAMBIGUATE the later duplicate: scan in parse order, and the first time a
// the first time a folded key repeats, suffix that bank's display name (" 2", // folded key repeats, suffix that bank's display name (" 2", " 3", …) until its
// " 3", …) until its folded key is unique among all names seen so far. The FIRST // folded key is unique among names seen so far — the first bank to carry a key
// bank to carry a key keeps its name verbatim; only subsequent collisions are // keeps its name verbatim. No bank or sample is lost, ids are untouched, and the
// renamed. No bank or sample is lost, and ids are untouched. The pool is included // pool's reserved "Pool" key is seeded first so a named bank folding to "pool"
// in the seen-set (its "Pool" key is reserved) so a named bank folding to "pool"
// is disambiguated away from it, never the reverse. // is disambiguated away from it, never the reverse.
// //
// Hosted HERE (a static member, Q-W5) rather than in the free parseBook because // Hosted here (not in the free parseBook) because it folds through the PRIVATE
// it folds through the PRIVATE BankBook::nameKey the same rule the // BankBook::nameKey the create/rename uniqueness check also uses.
// create/rename uniqueness check applies. Runs after parseBook on BOTH shapes;
// the legacy path yields { pool } alone, where the scan is a trivial no-op.
{ {
std::vector<std::string> seenKeys; std::vector<std::string> seenKeys;
seenKeys.reserve(banks.size()); seenKeys.reserve(banks.size());
+21 -40
View File
@@ -4,21 +4,14 @@
#include "core/json/json.h" #include "core/json/json.h"
// bank_model implementation. // bank_model implementation. JSON rides on the shared core/json lexical layer;
// // only the Sample/index DOMAIN grammar lives here. Doubles are emitted with 17
// JSON rides on the shared core/json lexical layer (Q-W1: one reader/writer, // significant digits (%.17g), the shortest form that round-trips every
// no per-module Parser copy). The field set is a flat struct of primitives, // IEEE-754 double exactly, so deserialize(serialize(x)) == x holds bit-for-bit.
// strings, one enum, a small string array, and a few optionals, so a compact
// writer + recursive-descent DOMAIN parser over json::Reader is the simplest
// thing that works. Doubles are emitted with 17 significant digits (%.17g), the
// shortest form that round-trips every IEEE-754 double exactly, so the
// deserialize(serialize(x)) == x invariant holds bit-for-bit.
namespace reasampler::model { namespace reasampler::model {
// --------------------------------------------------------------------------- // -- equality -----------------------------------------------------------
// equality
// ---------------------------------------------------------------------------
bool SourceRange::operator==(const SourceRange& o) const { bool SourceRange::operator==(const SourceRange& o) const {
return startSeconds == o.startSeconds && endSeconds == o.endSeconds && return startSeconds == o.startSeconds && endSeconds == o.endSeconds &&
@@ -51,20 +44,15 @@ bool Sample::operator==(const Sample& o) const {
provenance == o.provenance && createdTimestamp == o.createdTimestamp; provenance == o.provenance && createdTimestamp == o.createdTimestamp;
} }
// --------------------------------------------------------------------------- // -- path invariant -------------------------------------------------------
// path invariant
// ---------------------------------------------------------------------------
// DECISION: reject absolute paths rather than normalize them. The pure model has // Rejects absolute paths rather than normalizing them: the pure model has no
// no knowledge of the project root, so it cannot correctly relativize an absolute // knowledge of the project root, so any "normalization" would be a guess that
// path — any "normalization" would be a guess that could point at the wrong file. // could point at the wrong file. Covers POSIX ("/x"), Windows drive ("C:\x",
// Rejecting at the boundary is honest and deterministic; the capture backend (M3) // "C:/x", "C:foo" drive-relative), and UNC ("\\host\share") forms. Any leading
// is responsible for handing us an already-relative path. Covers POSIX ("/x"), // <alpha>: is rejected regardless of what follows — drive-relative paths
// Windows drive ("C:\x", "C:/x", "C:foo" drive-relative), and UNC ("\\host\share") // ("C:foo.wav") resolve against the drive's current directory, not the project
// forms. Any leading <alpha>: is rejected regardless of the character that follows — // root, so they violate relative-paths-only just as much as "C:\foo.wav" does.
// drive-relative paths ("C:foo.wav") resolve against the drive's current directory,
// not the project root, so they violate the relative-paths-only invariant just as
// much as "C:\foo.wav" does.
static bool isAbsolutePath(const std::string& p) { static bool isAbsolutePath(const std::string& p) {
if (p.empty()) return false; if (p.empty()) return false;
if (p[0] == '/' || p[0] == '\\') return true; // POSIX root or UNC if (p[0] == '/' || p[0] == '\\') return true; // POSIX root or UNC
@@ -73,9 +61,7 @@ static bool isAbsolutePath(const std::string& p) {
return false; return false;
} }
// --------------------------------------------------------------------------- // -- BankModel ------------------------------------------------------------
// BankModel
// ---------------------------------------------------------------------------
AddResult BankModel::add(const Sample& sample) { AddResult BankModel::add(const Sample& sample) {
if (sample.id.empty()) return AddResult::RejectedEmptyId; if (sample.id.empty()) return AddResult::RejectedEmptyId;
@@ -139,9 +125,7 @@ std::vector<Sample> BankModel::byTier(Tier tier) const {
return out; return out;
} }
// --------------------------------------------------------------------------- // -- JSON writer ------------------------------------------------------------
// JSON writer
// ---------------------------------------------------------------------------
namespace { namespace {
@@ -182,9 +166,9 @@ void writeSample(std::string& out, const Sample& s) {
w.keyBegin("key"); w.keyBegin("key");
if (s.key) writeEscaped(out, *s.key); else out += "null"; if (s.key) writeEscaped(out, *s.key); else out += "null";
// Phase S seam fields (D-B). Emitted as null when absent (same shape as `key` // Emitted as null when absent (same shape as `key`/`provenance`) so JSON that
// and `provenance`) so pre-Phase-S JSON — which lacks these keys entirely — // lacks these keys entirely parses to empty optionals and re-serializes
// parses to empty optionals and re-serializes without invention. // without invention.
w.keyBegin("rootNote"); w.keyBegin("rootNote");
if (s.rootNote) out += numToStr(*s.rootNote); else out += "null"; if (s.rootNote) out += numToStr(*s.rootNote); else out += "null";
@@ -240,12 +224,9 @@ std::string BankModel::serialize() const {
return out; return out;
} }
// --------------------------------------------------------------------------- // -- JSON parser (recursive descent over the shared json::Reader) -----------
// JSON parser (recursive descent over the shared json::Reader). Returns false // Returns false on any malformed input; never reads out of bounds. Only
// on any malformed input; never reads out of bounds. Only supports the subset // supports the subset our writer emits.
// our writer emits. The lexical layer (strings, numbers, skip) lives in
// core/json; only the Sample/index DOMAIN grammar lives here.
// ---------------------------------------------------------------------------
namespace { namespace {
+35 -44
View File
@@ -1,11 +1,7 @@
#pragma once #pragma once
// bank_model — the HEART of ReaSampler, deliberately free of any REAPER type so // bank_model — the HEART of ReaSampler: the per-project sample bank. `Sample`
// it compiles and unit-tests OUTSIDE the DAW. It owns the per-project sample // metadata struct + `BankModel` (add/remove/query/tier moves/dedup-by-hash + JSON
// bank: the `Sample` metadata struct and the `BankModel` (add / remove / query / // round-trip to/from std::string).
// tier moves / dedup-by-hash + JSON round-trip to/from std::string).
//
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
// vendor/ includes. Standard library only.
#include <cstdint> #include <cstdint>
#include <optional> #include <optional>
@@ -14,8 +10,8 @@
namespace reasampler::model { namespace reasampler::model {
// How the source audio was obtained. Kept in the pure core (no REAPER coupling); // How the source audio was obtained; the capture backends map their own notion
// the capture backends (M3/M8) map their own notion onto these. // onto these.
enum class SourceMode { enum class SourceMode {
MasterMix, // offline render of the master output MasterMix, // offline render of the master output
SelectedTracks, // offline render of selected tracks SelectedTracks, // offline render of selected tracks
@@ -31,9 +27,8 @@ enum class Tier {
Archive, Archive,
}; };
// Sample-accurate source bounds, in both project seconds and PPQ (ticks). Both // Sample-accurate source bounds, in both project seconds and PPQ (ticks) — both
// are stored because capture needs seconds and musical placement needs PPQ; we // stored so capture doesn't re-derive one from the other and risk rounding.
// refuse to re-derive one from the other and risk rounding (precision invariant).
struct SourceRange { struct SourceRange {
double startSeconds = 0.0; double startSeconds = 0.0;
double endSeconds = 0.0; double endSeconds = 0.0;
@@ -44,9 +39,9 @@ struct SourceRange {
}; };
// Present only when a sample was resampled FROM another sample. Carries the // Present only when a sample was resampled FROM another sample. Carries the
// parent's id and the FX-chain snapshot string (a thin drift fingerprint, NOT a // parent's id and an FX-chain snapshot (a thin drift fingerprint, NOT a restorable
// restorable chunk) captured at resample time; the re-capture-from-source action // chunk) — re-capture-from-source uses it to detect chain drift and replay the
// (M10) uses it to detect chain drift and replay the original capture request. // original capture request.
struct Provenance { struct Provenance {
std::string parentSampleId; std::string parentSampleId;
std::string fxChainSnapshot; std::string fxChainSnapshot;
@@ -63,15 +58,13 @@ struct Levels {
bool operator==(const Levels& o) const; bool operator==(const Levels& o) const;
}; };
// Sample-accurate sustain-loop bounds, as frame indices into the captured file // Sample-accurate sustain-loop bounds, as frame indices into the captured file — a
// (Phase S seam field, D-B). A bank intrinsic — a fact about the file, like // bank intrinsic (like sampleRate or length) the MIDI-playback instrument uses to
// sampleRate or length — consumed by the future MIDI-playback instrument to hold // hold notes past the recorded length. One optional struct (not two loose
// notes past the recorded length. Modeled as one optional struct (not two loose // optionals) so "both points or neither" is structural, not a rule to re-check at
// optionals) so "both points or neither" is a structural invariant, not a rule to // every boundary. Frame indices, not seconds — the instrument relates them to time
// re-check at every boundary. Frame indices, not seconds, because the loop is a // via the file's sample rate. Invariant (enforced at deserialize): 0 <= start <=
// per-sample-frame contract; the instrument reads the file's sample rate to relate // end; start == end is a valid zero-length loop marker.
// them to time. Invariant (enforced at the deserialize boundary): 0 <= start <= end.
// start == end is a valid zero-length loop marker.
struct LoopPoints { struct LoopPoints {
std::int64_t start = 0; std::int64_t start = 0;
std::int64_t end = 0; std::int64_t end = 0;
@@ -102,23 +95,23 @@ struct Sample {
double lengthBeats = 0.0; double lengthBeats = 0.0;
double captureTempo = 0.0; // project tempo (BPM) at capture time double captureTempo = 0.0; // project tempo (BPM) at capture time
// Time signature at capture time (L7 F1 — stamped alongside captureTempo so the // Time signature at capture time, stamped alongside captureTempo so the
// bars.beats.subdivisions read-out is stable under later project meter changes). // bars.beats.subdivisions read-out is stable under later project meter changes.
// 0/0 means UNSTAMPED (pre-L7 sample, or a capture that could not read the meter); // 0/0 means UNSTAMPED (pre-existing sample, or a capture that could not read the
// the metadata formatter renders a blank musical read-out for 0/0 and keeps s.ms. // meter); the metadata formatter renders a blank musical read-out then, keeping s.ms.
int captureTimeSigNum = 0; // meter numerator (e.g. 4 in 4/4); 0 = unstamped int captureTimeSigNum = 0; // meter numerator (e.g. 4 in 4/4); 0 = unstamped
int captureTimeSigDenom = 0; // meter denominator (e.g. 4 in 4/4); 0 = unstamped int captureTimeSigDenom = 0; // meter denominator (e.g. 4 in 4/4); 0 = unstamped
std::optional<std::string> key; // musical key, when known std::optional<std::string> key; // musical key, when known
// Phase S seam fields (D-B) — bank intrinsics for the MIDI-playback instrument, // Bank intrinsics for the MIDI-playback instrument, additive like `provenance`.
// additive like `provenance` (M1). Both default cleanly empty: pre-Phase-S // Both default cleanly empty: pre-existing samples deserialize without them and
// samples deserialize without them and re-serialize without inventing values. // re-serialize without inventing values.
// - rootNote: MIDI note (0..127) the sample was recorded at, so the instrument // - rootNote: MIDI note (0..127) the sample was recorded at, so the instrument
// can repitch it across the keyboard. DISTINCT from the musical `key` above: // can repitch it across the keyboard. DISTINCT from the musical `key` above:
// `key` is a human label ("F#m"); `rootNote` is the exact pitch for repitch. // `key` is a human label ("F#m"); `rootNote` is the exact pitch for repitch.
// Populated at/after capture only where derivable — left empty (never guessed) // Populated only where derivable — never guessed when the source isn't a
// when the source is not a single played note. // single played note.
// - loop: sustain-loop bounds, populated only where explicitly set. // - loop: sustain-loop bounds, populated only where explicitly set.
std::optional<int> rootNote; std::optional<int> rootNote;
std::optional<LoopPoints> loop; std::optional<LoopPoints> loop;
@@ -156,7 +149,7 @@ enum class AddResult {
// An ordered, id-keyed collection of Samples with content-hash dedup, tier // An ordered, id-keyed collection of Samples with content-hash dedup, tier
// moves/filtering, and lossless JSON round-trip. Insertion order is preserved // moves/filtering, and lossless JSON round-trip. Insertion order is preserved
// so a future panel (M5) can iterate in stable order. // so a panel can iterate in stable order.
class BankModel { class BankModel {
public: public:
// Adds a sample. Enforces the relative-paths-only invariant and dedups by // Adds a sample. Enforces the relative-paths-only invariant and dedups by
@@ -168,16 +161,14 @@ public:
bool remove(const std::string& id); bool remove(const std::string& id);
// Replaces the sample carrying `id` IN PLACE (preserving its position in // Replaces the sample carrying `id` IN PLACE (preserving its position in
// insertion order), with `updated`. Used by M10 re-capture-from-source: a // insertion order) with `updated`. Used by re-capture-from-source: a
// provenanced sample's file is regenerated and its metadata (relativePath, // provenanced sample's file is regenerated and its metadata refreshed while
// contentHash, levels, timestamp, ...) refreshed while its identity (id) and // its identity (id) and slot are kept, so the panel shows the same tile
// slot are kept, so the bank panel shows the same tile updated rather than a // updated rather than a reordered new entry. `updated.id` should equal `id`;
// reordered new entry. `updated.id` should equal `id` (the caller keeps the id // a differing id is written through as given. Does NOT dedup — an in-place
// stable); a differing id is written through as given (the caller's contract). // refresh is not a new insert, so collapse-by-hash (which guards inserts)
// Does NOT dedup — an in-place refresh of one entry is not a new insert, so the // does not apply. Returns false (no mutation) if `id` is absent or
// collapse-by-hash rule (which guards NEW inserts) does not apply. Returns false // `updated.relativePath` is absolute (relative-paths-only still holds here).
// (no mutation) if `id` is absent or `updated.relativePath` is absolute
// (the relative-paths-only invariant still holds for the replacement).
bool updateInPlace(const std::string& id, const Sample& updated); bool updateInPlace(const std::string& id, const Sample& updated);
// Returns the sample with `id`, or nullptr if absent. The pointer is // Returns the sample with `id`, or nullptr if absent. The pointer is
+10 -23
View File
@@ -4,27 +4,21 @@
#include "core/json/json.h" #include "core/json/json.h"
// owned_manifest implementation. // owned_manifest implementation. JSON shape is a single object with one string
// // array:
// JSON rides on the shared core/json lexical layer (Q-W1, mirror of bank_model /
// bank_book / tail_control). The shape is a single object with one string array:
// //
// {"owned":["reasampler_bank/a.wav","reasampler_bank/b.wav"]} // {"owned":["reasampler_bank/a.wav","reasampler_bank/b.wav"]}
//
// so a compact writer + a focused string-array domain parse is all it needs.
namespace reasampler::model { namespace reasampler::model {
// --------------------------------------------------------------------------- // -- path invariant (mirror of bank_model's isAbsolutePath) ----------------
// path invariant (mirror of bank_model's isAbsolutePath)
// ---------------------------------------------------------------------------
namespace { namespace {
// Any leading '/' or '\' (POSIX root / UNC), or a leading <alpha>: (Windows drive, // Any leading '/' or '\' (POSIX root / UNC), or a leading <alpha>: (Windows drive,
// incl. drive-relative "C:foo") is absolute. Same rejection bank_model applies to // incl. drive-relative "C:foo") is absolute — same rejection bank_model applies to
// Sample.relativePath the manifest holds the SAME kind of path, so the invariant // Sample.relativePath; the manifest holds the same kind of path, so the invariant
// must match exactly (a path the index accepts must be recordable, and vice versa). // must match exactly.
bool isAbsolutePath(const std::string& p) { bool isAbsolutePath(const std::string& p) {
if (p.empty()) return false; if (p.empty()) return false;
if (p[0] == '/' || p[0] == '\\') return true; if (p[0] == '/' || p[0] == '\\') return true;
@@ -35,9 +29,7 @@ bool isAbsolutePath(const std::string& p) {
} // namespace } // namespace
// --------------------------------------------------------------------------- // -- mutation / query -------------------------------------------------------
// mutation / query
// ---------------------------------------------------------------------------
ManifestAddResult OwnedFileManifest::add(const std::string& relativePath) { ManifestAddResult OwnedFileManifest::add(const std::string& relativePath) {
if (relativePath.empty()) return ManifestAddResult::RejectedEmptyPath; if (relativePath.empty()) return ManifestAddResult::RejectedEmptyPath;
@@ -53,9 +45,7 @@ bool OwnedFileManifest::contains(const std::string& relativePath) const {
return false; return false;
} }
// --------------------------------------------------------------------------- // -- JSON writer --------------------------------------------------------
// JSON writer (shared core/json escape — byte-identical to the prior local one)
// ---------------------------------------------------------------------------
std::string OwnedFileManifest::serialize() const { std::string OwnedFileManifest::serialize() const {
std::string out = "{\"owned\":["; std::string out = "{\"owned\":[";
@@ -67,11 +57,8 @@ std::string OwnedFileManifest::serialize() const {
return out; return out;
} }
// --------------------------------------------------------------------------- // JSON parser: string-array-only grammar. Tolerates unknown keys and requires
// JSON parser (string-array-only DOMAIN grammar over the shared core/json // the "owned" value to be an array of strings.
// lexical layer). Tolerates unknown keys (forward-compat) and requires the
// "owned" value to be an array of strings.
// ---------------------------------------------------------------------------
namespace { namespace {
+14 -29
View File
@@ -1,31 +1,16 @@
#pragma once #pragma once
// owned_manifest — the pure core of the owned-file manifest seam (Phase B, B-cap). // owned_manifest — the set of files the bank system ITSELF created; every file the
// capture path writes gets recorded here so prune can tell the system's own orphans
// (owned ∩ present referenced) apart from hand-dropped files. Writes and persists
// the manifest only — no prune logic lives here.
// //
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO // NOT a mirror of the bank index: removing/moving an index entry does NOT remove
// vendor/ includes. Standard library only. Unit-tested outside the DAW — the same // the file's manifest record (the manifest tracks files *created*; prune reconciles
// "small pure type + JSON round-trip" pattern as wav_codec / tab_strip. // manifest-vs-index later). Only the capture add-path adds to it — no remove verb.
// //
// -- What it is -------------------------------------------------------------- // Paths are ALWAYS project-relative (same invariant as Sample.relativePath). add()
// // rejects an absolute path rather than guess a relativization — the pure model has
// The set of files the bank system ITSELF created — every file the capture path // no project root, so "normalizing" could point at the wrong file.
// writes gets recorded here. Phase R prune consumes it to tell the system's own
// orphans (owned ∩ present referenced) apart from hand-dropped files. B-cap only
// WRITES and PERSISTS the manifest; no prune logic lives here (fork R-D, settled
// 2026-07-24: "defer the feature, design the seam").
//
// -- What it is NOT ----------------------------------------------------------
//
// It is NOT a mirror of the bank index. Removing or moving an index entry does NOT
// remove the file's manifest record: the manifest tracks files *created*, and prune
// (Phase R) reconciles manifest-vs-index later. The ONLY thing that adds to it is
// the capture add-path. There is deliberately no remove verb here.
//
// -- The relative-paths-only invariant ---------------------------------------
//
// A manifest path is ALWAYS project-relative (same invariant as Sample.relativePath
// and the persisted BankModel). add() rejects an absolute path rather than guess a
// relativization — the pure model has no project root, so a "normalization" would be
// a guess that could point at the wrong file (mirror of BankModel::add's rejection).
#include <optional> #include <optional>
#include <string> #include <string>
@@ -58,12 +43,12 @@ public:
// capture of an identical request does not double-record. // capture of an identical request does not double-record.
ManifestAddResult add(const std::string& relativePath); ManifestAddResult add(const std::string& relativePath);
// True iff the exact path string is recorded. Phase R uses this to attribute a // True iff the exact path string is recorded. Prune uses this to attribute a
// present file to the bank system. Exact string match — path normalization (if any) // present file to the bank system. Exact string match — path normalization (if
// is the caller's concern, consistent across add and query. // any) is the caller's concern, consistent across add and query.
bool contains(const std::string& relativePath) const; bool contains(const std::string& relativePath) const;
// The owned paths in insertion order. Phase R unions this with the on-disk file // The owned paths in insertion order. Prune unions this with the on-disk file
// set; here it is the round-trip + query surface. // set; here it is the round-trip + query surface.
const std::vector<std::string>& paths() const { return paths_; } const std::vector<std::string>& paths() const { return paths_; }
+15 -28
View File
@@ -4,26 +4,16 @@
#include "core/wire/wire.h" #include "core/wire/wire.h"
// provenance implementation — pure, self-contained (no third-party lib, mirror of // provenance implementation — pure, self-contained encoding.
// bank_model's hand-rolled encoding discipline).
// //
// ENCODING (the fingerprint string): a length-prefixed, field-ordered format so it // Fingerprint grammar: magic "rsprov1" + fixed-order length-prefixed fields
// is unambiguous and forge-proof (a value containing the separator cannot shift // (<len>':'<bytes>), so a value containing the separator can never shift the
// the parse). Grammar: // parse. Numbers render as decimal/%.17g text before prefixing (same %.17g the
// // bank model uses, so doubles round-trip bit-for-bit). Trailing fields: the
// "rsprov1" -- magic + version tag // track-GUID count + that many GUIDs, then the folded fxChainIdentity — itself
// then, in fixed order, each field as <len>':'<bytes> // length-prefixed per entry field, so it nests safely as one more field. Any
// // deviation (bad magic, short read, bad number) -> parseFingerprint returns
// Every field — including numbers — is emitted as its decimal / %.17g text then // nullopt.
// length-prefixed, so the parser never has to guess a field boundary. A trailing
// field is the track-GUID count followed by that many length-prefixed GUIDs, then
// the folded fxChainIdentity. Numbers use the SAME %.17g the bank model uses so a
// double round-trips bit-for-bit. Any deviation (wrong magic, short read, bad
// number) -> parseFingerprint returns nullopt.
//
// The fxChainIdentity fold is itself length-prefixed per entry field, so it is
// injection-proof on its own and can be embedded whole as one more length-prefixed
// field of the fingerprint.
namespace reasampler::model { namespace reasampler::model {
@@ -39,10 +29,9 @@ namespace {
constexpr const char* kMagic = "rsprov1"; constexpr const char* kMagic = "rsprov1";
// The shared core/wire codec (Q-W1, T2-01b) carries the field grammar + the full // The shared core/wire codec carries the field grammar + range-checked fieldInt.
// hardening (incl. the fixed fieldInt range check that closes the old strtol // Only the %.17g double rendering stays local — this writer's convention, shared
// silent-narrowing TODO). Only the %.17g double rendering stays local — it is // with the bank model's JSON doubles.
// this writer's convention, shared with the bank model's JSON doubles.
using wire::putField; using wire::putField;
using Cursor = wire::Cursor; using Cursor = wire::Cursor;
@@ -112,10 +101,9 @@ std::optional<CaptureRecipe> parseFingerprint(const std::string& fingerprint) {
std::size_t guidCount = 0; std::size_t guidCount = 0;
if (!c.fieldSizeT(guidCount)) return std::nullopt; if (!c.fieldSizeT(guidCount)) return std::nullopt;
// Q-W0 T2-01a (the sample_usage count-sanity pattern): each GUID field costs at least // Each GUID field costs at least 2 wire bytes ("0:"), so a count past size/2 is
// 2 wire bytes ("0:"), so a count past size/2 is provably bogus — reject BEFORE the // provably bogus — reject BEFORE the reserve, so a corrupt/crafted persisted
// reserve, so a corrupt/crafted persisted fingerprint can never drive reserve(huge) // fingerprint can never drive reserve(huge) into std::length_error / bad_alloc.
// into std::length_error / bad_alloc through the shell.
if (guidCount > fingerprint.size() / 2u + 1u) return std::nullopt; if (guidCount > fingerprint.size() / 2u + 1u) return std::nullopt;
r.trackGuids.reserve(guidCount); r.trackGuids.reserve(guidCount);
for (std::size_t i = 0; i < guidCount; ++i) { for (std::size_t i = 0; i < guidCount; ++i) {
@@ -139,7 +127,6 @@ std::optional<std::string> detectParent(
std::optional<std::string> parent; // the single bank sample all sources point at std::optional<std::string> parent; // the single bank sample all sources point at
for (const std::string& src : sourceItemFiles) { for (const std::string& src : sourceItemFiles) {
// Resolve this source file against the bank by exact normalized path.
const std::string* matchedId = nullptr; const std::string* matchedId = nullptr;
for (const BankFileRef& ref : bankFiles) { for (const BankFileRef& ref : bankFiles) {
if (!ref.absolutePath.empty() && ref.absolutePath == src) { if (!ref.absolutePath.empty() && ref.absolutePath == src) {
+23 -47
View File
@@ -1,35 +1,17 @@
#pragma once #pragma once
// provenance — the REAPER-free core behind Milestone 10 (re-capture from source). // provenance — the REAPER-free core behind re-capture-from-source. The shell
// gathers raw inputs from REAPER (source media-file names, FX-chain identity,
// capture range/scope/tail) and hands plain strings/values here. Owns:
// * CaptureRecipe — the recorded request + source FX-chain identity, so
// re-capture can re-run the same request and detect drift.
// * fingerprint codec — encodes/decodes a recipe into the single
// Provenance.fxChainSnapshot string (no schema change).
// * fxChainIdentity — folds FX-chain rows into one drift-detection string.
// * detectParent — pure parent-detection: resolved file path only, no
// fuzzy match, no false parentage.
// //
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO // A THIN reproducibility fingerprint (drift-detect + re-run) — NOT a serialized FX
// vendor/ includes. Standard library only. The shell (main.cpp / the action families) // chunk to restore; nothing here stores a restorable chain.
// 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 <optional>
#include <string> #include <string>
@@ -111,7 +93,7 @@ std::string buildFingerprint(const CaptureRecipe& recipe);
// mis-driving a re-capture. // mis-driving a re-capture.
std::optional<CaptureRecipe> parseFingerprint(const std::string& fingerprint); std::optional<CaptureRecipe> parseFingerprint(const std::string& fingerprint);
// --- Parent detection (P1: identity by resolved file path) ------------------- // -- Parent detection: identity by resolved file path only --------------------
// One bank sample as the detector sees it: its stable id and the ABSOLUTE, // 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 // normalized path its file resolves to (the shell resolves relativePath against
@@ -122,26 +104,20 @@ struct BankFileRef {
std::string absolutePath; // normalized (forward-slash, no trailing slash) std::string absolutePath; // normalized (forward-slash, no trailing slash)
}; };
// Decides whether a capture derives from a bank sample. // Decides whether a capture derives from a bank sample. No false parentage: a
// // capture derives from a bank sample iff EVERY source item whose media file could
// RULE (stated for the handoff, honest — no false parentage): a capture derives // be resolved points at the SAME bank sample's file (exact normalized absolute
// from a bank sample iff EVERY source item whose media file could be resolved // path). Files not in the bank, or matching MORE THAN ONE distinct bank sample
// points at the SAME bank sample's file (by exact normalized absolute path). If // (ambiguous), yield no parent. An empty source-file set yields no parent.
// 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' // sourceItemFiles : normalized absolute paths of the capture's source items'
// take media files (the shell gathers + normalizes them). A // take media files, gathered by the shell. A file that could
// file that could not be resolved is simply omitted by the // not be resolved is simply omitted — never an empty string.
// shell — it never becomes an empty string here.
// bankFiles : the active book's samples as BankFileRefs (path -> id). // bankFiles : the active book's samples as BankFileRefs (path -> id).
// //
// Returns the parent sample id, or nullopt when the capture is not a genuine // Returns the parent sample id, or nullopt otherwise. Both sides are normalized
// resample-from-sample. Comparison is exact path identity; the caller normalizes // identically via normalizeSlashes (lowercased on Windows) so a slash/case
// both sides identically via normalizeSlashes (which lowercases on Windows) so a // difference never spuriously matches or misses.
// 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<std::string> detectParent( std::optional<std::string> detectParent(
const std::vector<std::string>& sourceItemFiles, const std::vector<std::string>& sourceItemFiles,
const std::vector<BankFileRef>& bankFiles); const std::vector<BankFileRef>& bankFiles);
+2 -6
View File
@@ -4,12 +4,10 @@
#include "core/json/json.h" #include "core/json/json.h"
// slot_map implementation (extracted from bank_book, Q-W1 T4-05). // slot_map implementation.
// //
// The invariant: entries_ is kept sorted ascending by slot, one id per slot, one // The invariant: entries_ is kept sorted ascending by slot, one id per slot, one
// slot per id. Every mutator restores it; queries assume it. serialize rides the // slot per id. Every mutator restores it; queries assume it.
// shared core/json emit helpers — the emitted fragment is byte-identical to the
// pre-extraction bank_book writer.
namespace reasampler::model { namespace reasampler::model {
@@ -128,8 +126,6 @@ SlotMap SlotMap::fromEntries(const std::vector<std::pair<std::string, int>>& pai
std::string SlotMap::serialize() const { std::string SlotMap::serialize() const {
// Array of {id, slot} objects in ascending slot order (entries_ is kept sorted). // Array of {id, slot} objects in ascending slot order (entries_ is kept sorted).
// json::Writer + numToStr are the same emit path the pre-extraction writer used,
// so the fragment is byte-identical.
std::string out; std::string out;
out += '['; out += '[';
for (std::size_t i = 0; i < entries_.size(); ++i) { for (std::size_t i = 0; i < entries_.size(); ++i) {
+12 -20
View File
@@ -1,21 +1,13 @@
#pragma once #pragma once
// slot_map — the L7 gap-preserving display-position carrier for ONE bank (F2 settled: // slot_map — the gap-preserving display-position carrier for ONE bank: plain
// plain interchangeable slots, NOT M9 fixed/addressable slots). A slot is just a // interchangeable slots, not fixed/addressable ones. A slot is a display position a
// display position a sample id occupies; the map is sample id -> slot (>= 0). Gaps // sample id occupies; the map is sample id -> slot (>= 0). Gaps are first-class (a
// are first-class: a bank may have a sample at slot 1 with slot 0 empty (an empty // bank may have slot 1 occupied with slot 0 empty). At most one id per slot, at
// first row above an occupied second row). At most one id per slot (a slot is never // most one slot per id.
// double-occupied) and at most one slot per id (an id sits in exactly one place).
// //
// Position lives HERE, not on Sample (CLAUDE.md wrapping discipline): a copy of one // Position lives HERE, not on Sample: a copy of one sample into two banks may sit
// sample into two banks may sit at different slots, so position is a per-bank display // at different slots, so position is a per-bank display concern owned by the
// concern owned by the bank's membership. bank_model / Sample stay untouched. // bank's membership. bank_model / Sample stay untouched.
//
// Extracted from bank_book (Q-W1, T4-05): a self-contained ordered-slot container
// with its own serialize, distinct from the multi-bank registry that carries it.
// Behavior covered by bank_book_tests (the round-trip + reorder/reconcile suites);
// a dedicated slot_map_tests target is a welcome follow-up, not a Q-W1 requirement.
//
// PURE: standard library + core/json (serialize) only.
#include <cstddef> #include <cstddef>
#include <string> #include <string>
@@ -50,7 +42,7 @@ public:
// keeps its position. Returns true if the id was mapped. // keeps its position. Returns true if the id was mapped.
bool remove(const std::string& id); bool remove(const std::string& id);
// Moves `id` to `targetSlot`, gap-preserving (F3 reorder semantics): // Moves `id` to `targetSlot`, gap-preserving:
// * target slot EMPTY -> `id` moves there; its old slot is left empty. // * target slot EMPTY -> `id` moves there; its old slot is left empty.
// * target slot OCCUPIED -> insert-before-and-shift: `id` takes targetSlot and // * target slot OCCUPIED -> insert-before-and-shift: `id` takes targetSlot and
// every occupant at slot >= targetSlot (except `id` itself) shifts up by one, // every occupant at slot >= targetSlot (except `id` itself) shifts up by one,
@@ -62,9 +54,9 @@ public:
bool reorder(const std::string& id, int targetSlot); bool reorder(const std::string& id, int targetSlot);
// Rebuilds the map densely from `ids` in the given order (slot i = ids[i]), // Rebuilds the map densely from `ids` in the given order (slot i = ids[i]),
// dropping any prior state. The migration path: a pre-L7 bank with no persisted // dropping any prior state. The migration path: a bank with no persisted slot
// slot data is seeded from its BankModel insertion order, densely packed (no gaps), // data is seeded from its BankModel insertion order, densely packed (no gaps),
// so it is visually identical on first post-L7 load. Empty/duplicate ids skipped. // so it is visually identical on first load. Empty/duplicate ids skipped.
void resetDense(const std::vector<std::string>& ids); void resetDense(const std::vector<std::string>& ids);
// Drops any mapping whose id is NOT in `liveIds` (a stale marker whose sample left // Drops any mapping whose id is NOT in `liveIds` (a stale marker whose sample left
+88 -109
View File
@@ -1,45 +1,32 @@
#pragma once #pragma once
// prune_reconcile — the pure core of Phase R (Reclaim), Wave 1. The safety-critical // prune_reconcile — the safety-critical "which files are orphans" decision, computed
// "which files are orphans" decision, computed with NO filesystem I/O and NO REAPER // with NO filesystem I/O and NO REAPER types. Unit-tested outside the DAW before any
// types. The mirror of view_mode_model's reconcile(liveGuids), one level DOWN: it // I/O exists — this decides which bytes get deleted.
// reconciles FILES ON DISK against REFERENCED FILES (the union across every bank),
// where reconcile reconciled membership entries against live tracks.
//
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
// vendor/ includes, NO filesystem calls. Standard library only. Unit-tested outside
// the DAW — this is the safety-critical part (it decides which bytes get deleted in
// R2/R3), so it is hard-tested here before any I/O exists.
//
// -- The one computation ------------------------------------------------------
// //
// The one computation:
// orphans = (owned ∩ present) referenced // orphans = (owned ∩ present) referenced
// // * present — files enumerated in the resolved current bank folder (shell).
// * present — files enumerated in the resolved current bank folder (R2 shell).
// * referenced — every project-relative path referenced by ANY bank in the book, // * referenced — every project-relative path referenced by ANY bank in the book,
// pool included (union across the whole book — see BankBook:: // pool included (union across the whole book — see BankBook::
// referencedPaths). A file referenced by any bank — including via a // referencedPaths). A file referenced by any bank — including via a
// COPY into a second bank — is NEVER an orphan (the prune null test). // COPY into a second bank — is NEVER an orphan (the prune null test).
// * owned — the owned-file manifest: the files the bank system itself created // * owned — the owned-file manifest: files the bank system itself created. A
// (OwnedFileManifest). A present-but-unowned (hand-dropped) file is // present-but-unowned (hand-dropped) file is NEVER reclaimed.
// NEVER reclaimed — prune reclaims only the system's own leavings.
// //
// The three settled guardrails fall straight out of the set algebra: // The three guardrails fall straight out of the set algebra:
// * ∩ present — never proposes deleting a file that is not on disk (an owned- // * ∩ present — never proposes deleting a file that is not on disk (an owned-
// but-absent manifest entry yields no orphan, no error). // but-absent manifest entry yields no orphan, no error).
// * ∩ owned — never a hand-dropped file (ownership attribution, fork R-D). // * ∩ owned — never a hand-dropped file (ownership attribution).
// * referenced — never a file any bank references (union safety, prune null test). // * referenced — never a file any bank references (union safety, prune null test).
// //
// -- Path representation: EXACT-STRING match (safety-critical) ----------------- // Path representation: EXACT-STRING match everywhere — Sample.relativePath,
// // OwnedFileManifest::contains, BankModel all use raw std::string equality: no
// Every path in the model is a project-relative string compared VERBATIM: Sample. // separator normalization, no case-folding, no trailing-slash trimming. Feeding a
// relativePath, OwnedFileManifest::contains (p == relativePath), and BankModel all // consistent spelling across the three inputs is the shell's contract (it enumerates
// use raw std::string equality — no separator normalization, no case-folding, no // the folder, unions the book, and reads the manifest against the SAME resolved
// trailing-slash trimming. This core MATCHES that convention exactly: it compares // current folder). Diverging from exact match here (e.g. case-insensitive compare)
// the raw strings the shell supplies. Feeding a consistent spelling across the three // would be the unsafe direction — it could let one spelling of a referenced file be
// inputs is the R2 shell's contract (it enumerates the folder, unions the book, and // treated as an orphan under another.
// reads the manifest against the SAME resolved current folder). Diverging from exact
// match here (e.g. case-insensitive compare) would be the unsafe direction — it could
// let one spelling of a referenced file be treated as an orphan under another.
#include <cstdint> #include <cstdint>
#include <string> #include <string>
@@ -48,34 +35,28 @@
namespace reasampler::reclaim { namespace reasampler::reclaim {
// The dry-run prune result (Phase R, Wave 2 — report only, no deletion). The thin // The dry-run prune result — report only, no deletion. The shell fills this from
// prune shell (persist) fills this from pruneOrphans() + a per-file size stat and hands // pruneOrphans() + a per-file size stat; a confirmed delete later acts on the SAME
// it to the report surface; R3 will act on the SAME set behind the confirm guardrail. // set behind the confirm guardrail. Filesystem-free by design (the shell does the
// REAPER-free / filesystem-free by design (the shell does the I/O; this is just the // I/O; this is the tallied outcome), so the aggregation is unit-testable.
// tallied outcome), so the count/size aggregation is unit-testable outside the DAW.
// //
// * count — number of orphan files (== orphans.size(); the AUTHORITATIVE tally, // * count — number of orphan files (authoritative tally, exact even when
// exact even when `orphans` below is a truncated display list). // `orphans` below is a truncated display list).
// * totalBytes — sum of the on-disk sizes of the orphan files, in bytes (reclaimable // * totalBytes — sum of the on-disk sizes of the orphan files, in bytes. A file
// space). A file the stat could not size contributes 0 (never negative). // the stat could not size contributes 0 (never negative).
// * orphans — the orphan file list as project-relative index-spelled paths, in // * orphans — the orphan file list, project-relative, in folder-enumeration
// folder-enumeration order (deterministic). MAY be truncated for a large // order (deterministic). MAY be truncated for a large set (the
// set (the shell's display cap); `count` stays exact regardless, and // shell's display cap); `count` stays exact regardless.
// `truncated` says whether the list was clipped. // * truncated — true iff `orphans` holds fewer than `count` entries.
// * truncated — true iff `orphans` holds fewer than `count` entries (a large set was
// clipped for display); false when the list is complete.
// * abortedUnreadableUsage — true iff the scan found a present-but-unreadable // * abortedUnreadableUsage — true iff the scan found a present-but-unreadable
// rsusage_* instance-usage record (pS-usage fail-safe): the orphan // rsusage_* instance-usage record: the orphan computation was NOT
// computation was NOT performed (count 0, empty list) and the prune // performed (count 0, empty list) and the prune must HALT —
// must HALT — deleting with degraded protection is the data-loss // deleting with degraded protection is the data-loss direction.
// direction. Set by the session's scan shell, never by // Set by the scan shell, never by buildPruneReport (which stays a
// buildPruneReport (which stays a pure tally). // pure tally).
// * offendingUsageKeys — the exact "rsusage_<guid>" ext-state key names that // * offendingUsageKeys — the exact "rsusage_<guid>" key names that triggered the
// triggered the abort (non-empty iff abortedUnreadableUsage). Named // abort (non-empty iff abortedUnreadableUsage), so the operator
// so the action can print them for operator recovery: a corrupt/ // can clear each key via ReaScript:
// oversized key whose owning instance no longer exists is never
// automatically rewritten, so the abort would be permanent without
// a way to clear it. The operator can clear each key via ReaScript:
// reaper.SetProjExtState(0, "reasampler", "<key>", "") // reaper.SetProjExtState(0, "reasampler", "<key>", "")
struct PruneReport { struct PruneReport {
std::size_t count = 0; std::size_t count = 0;
@@ -86,20 +67,18 @@ struct PruneReport {
std::vector<std::string> offendingUsageKeys; // non-empty iff abortedUnreadableUsage std::vector<std::string> offendingUsageKeys; // non-empty iff abortedUnreadableUsage
}; };
// The outcome of an actual prune DELETION (Phase R, Wave 3 — R3). The prune shell fills // The outcome of an actual prune DELETION. The shell fills this as it deletes the
// this as it deletes the confirmed orphan set, reporting what it ACTUALLY reclaimed (not // confirmed orphan set, reporting what it ACTUALLY reclaimed (not what it intended)
// what it intended to) so a locked/vanished file shows up as a skip, never a false claim. // so a locked/vanished file shows up as a skip, never a false claim. Filesystem-free
// REAPER-free / filesystem-free by design (the shell does the deletion; this is the // by design, so the count/byte aggregation is unit-testable.
// tallied outcome), so the count/byte aggregation is unit-testable outside the DAW.
// //
// * reclaimedCount — number of files actually removed from disk BY THIS CALL (trash or // * reclaimedCount — files actually removed from disk BY THIS CALL (trash or
// unlink). Already-absent files are NOT counted here. // unlink). Already-absent files are NOT counted.
// * reclaimedBytes — sum of the on-disk sizes of the files actually removed, in bytes. // * reclaimedBytes — sum of the on-disk sizes of the files actually removed.
// * skippedCount — files that could not be or were not reclaimed: stale entries that // * skippedCount — files not reclaimed: stale entries dropped from the
// dropped out of the fresh-orphan intersection, files that vanished // fresh-orphan intersection, files that vanished between plan
// between the plan and the delete call (already absent), and real // and delete, and real delete failures. Never an error/crash.
// delete failures (locked, conversion error). Never an error/crash. // * usedTrash — true iff deletions were routed to the OS trash/recycle bin
// * usedTrash — true iff the deletions were routed to the OS trash/recycle bin
// (recoverable); false iff the platform fell back to hard unlink. // (recoverable); false iff the platform fell back to hard unlink.
struct PruneDeletionResult { struct PruneDeletionResult {
std::size_t reclaimedCount = 0; std::size_t reclaimedCount = 0;
@@ -110,11 +89,11 @@ struct PruneDeletionResult {
// Computes the prune orphan set: (owned ∩ present) referenced. // Computes the prune orphan set: (owned ∩ present) referenced.
// //
// Returns the subset of `present` that is BOTH owned AND unreferenced, in the ORDER // Returns the subset of `present` that is BOTH owned AND unreferenced, in the order
// they appear in `present` (deterministic output — mirror of the insertion-order // they appear in `present` (deterministic — mirrors the insertion-order determinism
// determinism the index / manifest keep; the R2 dry-run reports a stable file list). // the index/manifest keep). Duplicate spellings within `present` are de-duplicated
// Duplicate spellings within `present` are de-duplicated in the result (a folder // in the result (a folder enumeration yields distinct names, but the core does not
// enumeration yields distinct names, but the core does not rely on that). // rely on that).
// //
// Pure: no I/O, no REAPER, no hidden state. All three inputs are project-relative // Pure: no I/O, no REAPER, no hidden state. All three inputs are project-relative
// path strings, compared by exact std::string equality (see header note). // path strings, compared by exact std::string equality (see header note).
@@ -122,56 +101,56 @@ std::vector<std::string> pruneOrphans(const std::vector<std::string>& present,
const std::vector<std::string>& referenced, const std::vector<std::string>& referenced,
const std::vector<std::string>& owned); const std::vector<std::string>& owned);
// Union two referenced-path sets into one (pS-usage): the bank's own referencedPaths() // Unions two referenced-path sets into one: the bank's own referencedPaths() PLUS
// PLUS the paths held by live ReaSampler 9000 instances (sample_usage::usageHeldPaths). // the paths held by live ReaSampler 9000 instances (sample_usage::usageHeldPaths).
// Order-preserving (`primary` first, then the `extra` paths not already present), // Order-preserving (`primary` first, then the `extra` paths not already present),
// exact-string de-dup — the same comparison convention as everything above, so feeding // exact-string de-dup — the same comparison convention as everything above, so
// the result to pruneOrphans keeps the ` referenced` guardrail byte-exact. A path held // feeding the result to pruneOrphans keeps the ` referenced` guardrail byte-exact.
// ONLY by an instance (e.g. its bank entry was deleted while the instance kept its v10 // A path held ONLY by an instance (its bank entry was deleted while the instance
// ref) is protected exactly like a bank-referenced one. // kept its ref) is protected exactly like a bank-referenced one.
// //
// Pure: no I/O, no REAPER. Kept here (not in the shells) so the "instance usage makes a // Pure: no I/O, no REAPER. Kept here (not in the shells) so "instance usage makes a
// file un-prunable" property is provable at the prune layer itself. // file un-prunable" is provable at the prune layer itself.
std::vector<std::string> mergeReferenced(const std::vector<std::string>& primary, std::vector<std::string> mergeReferenced(const std::vector<std::string>& primary,
const std::vector<std::string>& extra); const std::vector<std::string>& extra);
// Tallies a dry-run PruneReport from a computed orphan set and a per-path size lookup. // Tallies a dry-run PruneReport from a computed orphan set and a per-path size
// PURE (no I/O): the shell does the folder stat and passes the sizes in `sizeByPath`; // lookup. Pure (no I/O): the shell does the folder stat and passes sizes in
// this owns the count / byte-sum / display-truncation decision so it is unit-testable. // `sizeByPath`; this owns the count/byte-sum/display-truncation decision.
// //
// * count == orphans.size() (the authoritative tally, exact regardless of the cap). // * count == orphans.size() (authoritative tally, exact regardless of the cap).
// * totalBytes == the sum of sizeByPath[o] over EVERY orphan o (not just the displayed // * totalBytes == sum of sizeByPath[o] over EVERY orphan o (not just displayed); a
// ones); a path missing from sizeByPath contributes 0 (an orphan whose // path missing from sizeByPath contributes 0 (never negative).
// size could not be stat'd — never negative, never dropped from the sum).
// * orphans == the first `displayCap` orphans in input order (the deterministic // * orphans == the first `displayCap` orphans in input order (the deterministic
// folder-enumeration order pruneOrphans preserved); the whole set when // order pruneOrphans preserved); the whole set when count <=
// count <= displayCap. displayCap == 0 means "no display cap" (whole set). // displayCap. displayCap == 0 means "no display cap".
// * truncated == count > orphans.size() (a large set was clipped for display). // * truncated == count > orphans.size().
// //
// Kept separate from pruneOrphans so the safety-critical set algebra stays a pure function // Kept separate from pruneOrphans so the safety-critical set algebra stays a pure
// of three sets, while the presentation tally (which the R2 dry-run and R3 confirm both // function of three sets, while the presentation tally is its own testable step.
// need) is its own small, testable step.
PruneReport buildPruneReport(const std::vector<std::string>& orphans, PruneReport buildPruneReport(const std::vector<std::string>& orphans,
const std::unordered_map<std::string, std::uint64_t>& sizeByPath, const std::unordered_map<std::string, std::uint64_t>& sizeByPath,
std::size_t displayCap); std::size_t displayCap);
// Computes the confirm-time delete plan: the intersection of the set the user was SHOWN // Computes the confirm-time delete plan: the intersection of the set the user was
// and confirmed (`confirmed`) with a FRESH pure-core orphan output (`freshOrphans`) taken // SHOWN and confirmed (`confirmed`) with a FRESH pure-core orphan output
// at delete time. Returns exactly `confirmed ∩ freshOrphans`, in the order of `confirmed` // (`freshOrphans`) taken at delete time. Returns exactly `confirmed ∩ freshOrphans`,
// (deterministic — the same order the confirm listed). // in the order of `confirmed` (deterministic — the same order the confirm listed).
// //
// This is the R3 staleness guard, and it protects in BOTH directions so that "what was // This is the staleness guard, and it protects in BOTH directions so that "what was
// shown is what is deleted" holds no matter what changed between confirm and delete: // shown is what is deleted" holds no matter what changed between confirm and delete:
// * A confirmed path that is NO LONGER a fresh orphan — a file that vanished (gone from // * A confirmed path that is NO LONGER a fresh orphan — vanished (gone from
// `present`), or that some bank now references (gone from ` referenced`), or whose // `present`), now referenced (gone from ` referenced`), or ownership changed —
// ownership changed — is DROPPED (a skip, never an error, never a wrongful delete of a // is DROPPED (a skip, never a wrongful delete of a now-referenced file). Because
// now-referenced file). Because `freshOrphans` is itself a pure-core output, the plan // `freshOrphans` is itself a pure-core output, the plan can never contain a
// can never contain a referenced or hand-dropped file: the guard survives recompute. // referenced or hand-dropped file: the guard survives recompute.
// * A path that became an orphan AFTER the confirm (in `freshOrphans` but not `confirmed`) // * A path that became an orphan AFTER the confirm (in `freshOrphans` but not
// is NOT deleted — it was never shown, so it is never swept without its own confirm. // `confirmed`) is NOT deleted — it was never shown, so it's never swept without
// its own confirm.
// //
// PURE: no I/O, no REAPER. Duplicate spellings in `confirmed` are de-duplicated in the // Pure: no I/O, no REAPER. Duplicate spellings in `confirmed` are de-duplicated in
// result (mirrors pruneOrphans; a confirmed set from a real scan holds distinct names). // the result (mirrors pruneOrphans; a confirmed set from a real scan holds distinct
// names).
std::vector<std::string> pruneDeletePlan(const std::vector<std::string>& confirmed, std::vector<std::string> pruneDeletePlan(const std::vector<std::string>& confirmed,
const std::vector<std::string>& freshOrphans); const std::vector<std::string>& freshOrphans);
+4 -7
View File
@@ -1,11 +1,8 @@
#pragma once #pragma once
// clamp01 — the ONE unit-interval clamp (Q-W1, T4-24). Replaces the per-module // clamp01 — the ONE unit-interval clamp, replacing several per-module static
// static copies (master_gain / param_slider / envelope_overlay / reasampler_editor). // copies. Deliberately the ternary form: comparisons with NaN are false, so a NaN
// Deliberately the ternary form: comparisons with NaN are false, so a NaN input // input passes through unchanged rather than silently collapsing to a bound — the
// passes through unchanged rather than silently collapsing to a bound — the // behavior most of the retired copies already had.
// behavior of the majority of the retired copies.
//
// PURE: standard library only (not even that).
namespace reasampler::util { namespace reasampler::util {
+3 -4
View File
@@ -1,7 +1,6 @@
// core/util/file_bytes — the ONE whole-file byte loader (Q-W1; audit T2-03). // core/util/file_bytes — the ONE whole-file byte loader. Pure standard library —
// Pure standard library — NO REAPER, NO SWELL, NO VST3 — but it does blocking // NO REAPER, NO SWELL, NO VST3 — but it does blocking file I/O: NEVER call it on
// file I/O: NEVER call it on the audio thread (off-thread only, the same rule // the audio thread. Linked by both artifacts.
// every prior hand-rolled copy carried). Linked by both artifacts.
#pragma once #pragma once