feat(prune): R1 prune-reconcile pure core — (owned ∩ present) − referenced

Add REAPER-free/filesystem-free prune orphan-set core + additive
BankBook::referencedPaths() union query, with full unit coverage.
This commit is contained in:
2026-07-26 18:02:42 -04:00
parent b6464f78c8
commit 3cfa9239d2
6 changed files with 426 additions and 0 deletions
+17
View File
@@ -2,6 +2,7 @@
#include <algorithm>
#include <climits>
#include <unordered_set>
// bank_book implementation.
//
@@ -321,6 +322,22 @@ bool BankBook::hashReferencedElsewhere(const std::string& hash,
return false;
}
std::vector<std::string> BankBook::referencedPaths() const {
// Union across the whole book (pool first, then named banks in ordinal order —
// banks_ is kept ordinal-sorted). De-duplicate by exact string so a file a copy
// put in two banks appears once. Skip empty paths (they reference no file).
std::vector<std::string> paths;
std::unordered_set<std::string> seen;
for (const auto& b : banks_) {
for (const auto& s : b.index.all()) {
if (s.relativePath.empty()) continue;
if (seen.insert(s.relativePath).second)
paths.push_back(s.relativePath);
}
}
return paths;
}
// ===========================================================================
// JSON — writer
// ===========================================================================
+11
View File
@@ -220,6 +220,17 @@ public:
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.
std::vector<std::string> referencedPaths() const;
// -- Query ---------------------------------------------------------------
// The bank with `id`, or nullptr. Pointer invalidated by any mutating call.
+36
View File
@@ -0,0 +1,36 @@
#include "prune_reconcile.h"
#include <unordered_set>
// prune_reconcile implementation — the one set-algebra computation, kept trivially
// auditable: build the referenced and owned lookup sets, then walk `present` once,
// keeping a path iff it is owned AND not referenced. Walking `present` (not owned)
// gives the ∩-present clause for free and yields output in folder-enumeration order.
namespace reasampler {
std::vector<std::string> pruneOrphans(const std::vector<std::string>& present,
const std::vector<std::string>& referenced,
const std::vector<std::string>& owned) {
// Exact-string membership — the model's canonical relative-path comparison
// (Sample.relativePath / OwnedFileManifest::contains). std::string hashes/compares
// byte-for-byte, so no normalization creeps in.
const std::unordered_set<std::string> referencedSet(referenced.begin(),
referenced.end());
const std::unordered_set<std::string> ownedSet(owned.begin(), owned.end());
std::vector<std::string> orphans;
std::unordered_set<std::string> emitted; // de-dup repeated spellings in `present`
for (const std::string& path : present) {
// (owned ∩ present) referenced: present is the walk; owned and !referenced
// are the two membership tests; emitted guards against a duplicate `present`.
if (ownedSet.count(path) == 0) continue; // not our leaving — skip
if (referencedSet.count(path) != 0) continue; // some bank references it
if (!emitted.insert(path).second) continue; // already emitted
orphans.push_back(path);
}
return orphans;
}
} // namespace reasampler
+63
View File
@@ -0,0 +1,63 @@
#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 ------------------------------------------------------
//
// orphans = (owned ∩ present) referenced
//
// * present — files enumerated in the resolved current bank folder (R2 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.
//
// The three settled 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).
// * 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 BankIndex 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.
#include <string>
#include <vector>
namespace reasampler {
// 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).
//
// 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).
std::vector<std::string> pruneOrphans(const std::vector<std::string>& present,
const std::vector<std::string>& referenced,
const std::vector<std::string>& owned);
} // namespace reasampler