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