Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
#include "core/reclaim/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::reclaim {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::vector<std::string> mergeReferenced(const std::vector<std::string>& primary,
|
||||
const std::vector<std::string>& extra) {
|
||||
std::vector<std::string> merged;
|
||||
merged.reserve(primary.size() + extra.size());
|
||||
std::unordered_set<std::string> seen;
|
||||
for (const std::string& p : primary) {
|
||||
if (seen.insert(p).second) merged.push_back(p);
|
||||
}
|
||||
for (const std::string& p : extra) {
|
||||
if (seen.insert(p).second) merged.push_back(p);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
PruneReport buildPruneReport(
|
||||
const std::vector<std::string>& orphans,
|
||||
const std::unordered_map<std::string, std::uint64_t>& sizeByPath,
|
||||
std::size_t displayCap) {
|
||||
PruneReport report;
|
||||
report.count = orphans.size();
|
||||
for (const std::string& o : orphans) {
|
||||
// Sum EVERY orphan's bytes (the exact reclaimable total), not just the displayed
|
||||
// ones. A path with no stat'd size contributes 0 — never dropped, never negative.
|
||||
const auto it = sizeByPath.find(o);
|
||||
report.totalBytes += (it != sizeByPath.end()) ? it->second : 0;
|
||||
// Display list is capped (0 = uncapped). count stays exact above regardless.
|
||||
if (displayCap == 0 || report.orphans.size() < displayCap)
|
||||
report.orphans.push_back(o);
|
||||
}
|
||||
report.truncated = report.orphans.size() < report.count;
|
||||
return report;
|
||||
}
|
||||
|
||||
std::vector<std::string> pruneDeletePlan(const std::vector<std::string>& confirmed,
|
||||
const std::vector<std::string>& freshOrphans) {
|
||||
// fresh is a pure-core output (referenced/hand-dropped already excluded), so keeping a
|
||||
// confirmed path iff it is still a fresh orphan can never re-admit an unsafe file. Walk
|
||||
// `confirmed` to preserve the confirm's listing order; emitted de-dups repeats.
|
||||
const std::unordered_set<std::string> freshSet(freshOrphans.begin(), freshOrphans.end());
|
||||
|
||||
std::vector<std::string> plan;
|
||||
std::unordered_set<std::string> emitted;
|
||||
for (const std::string& path : confirmed) {
|
||||
if (freshSet.count(path) == 0) continue; // stale: vanished / now-referenced -> skip
|
||||
if (!emitted.insert(path).second) continue; // already emitted
|
||||
plan.push_back(path);
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
} // namespace reasampler::reclaim
|
||||
@@ -0,0 +1,178 @@
|
||||
#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 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.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
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.
|
||||
//
|
||||
// * 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.
|
||||
// * 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_<guid>" 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:
|
||||
// reaper.SetProjExtState(0, "reasampler", "<key>", "")
|
||||
struct PruneReport {
|
||||
std::size_t count = 0;
|
||||
std::uint64_t totalBytes = 0;
|
||||
std::vector<std::string> orphans;
|
||||
bool truncated = false;
|
||||
bool abortedUnreadableUsage = false;
|
||||
std::vector<std::string> offendingUsageKeys; // non-empty iff abortedUnreadableUsage
|
||||
};
|
||||
|
||||
// The outcome of an actual prune DELETION (Phase R, Wave 3 — R3). The prune shell fills
|
||||
// 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.
|
||||
//
|
||||
// * 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
|
||||
// (recoverable); false iff the platform fell back to hard unlink.
|
||||
struct PruneDeletionResult {
|
||||
std::size_t reclaimedCount = 0;
|
||||
std::uint64_t reclaimedBytes = 0;
|
||||
std::size_t skippedCount = 0;
|
||||
bool usedTrash = false;
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// 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).
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
std::vector<std::string> mergeReferenced(const std::vector<std::string>& primary,
|
||||
const std::vector<std::string>& 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.
|
||||
//
|
||||
// * 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).
|
||||
// * 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).
|
||||
//
|
||||
// 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.
|
||||
PruneReport buildPruneReport(const std::vector<std::string>& orphans,
|
||||
const std::unordered_map<std::string, std::uint64_t>& 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).
|
||||
//
|
||||
// This is the R3 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.
|
||||
//
|
||||
// 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<std::string> pruneDeletePlan(const std::vector<std::string>& confirmed,
|
||||
const std::vector<std::string>& freshOrphans);
|
||||
|
||||
} // namespace reasampler::reclaim
|
||||
Reference in New Issue
Block a user