ef2d3fa846
Already-absent files no longer inflate reclaimedCount (outAlreadyAbsent distinguishes vanished-at-delete from real failures). Stale skip count de-dups confirmed before subtraction so duplicates aren't tallied as stale. prune_button.h rightInset and drawTailFooter vrc.right now cross-reference each other by name.
151 lines
9.5 KiB
C++
151 lines
9.5 KiB
C++
#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 <cstdint>
|
||
#include <string>
|
||
#include <unordered_map>
|
||
#include <vector>
|
||
|
||
namespace reasampler {
|
||
|
||
// 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.
|
||
struct PruneReport {
|
||
std::size_t count = 0;
|
||
std::uint64_t totalBytes = 0;
|
||
std::vector<std::string> orphans;
|
||
bool truncated = false;
|
||
};
|
||
|
||
// 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);
|
||
|
||
// 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
|