#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 #include #include #include 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 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 pruneOrphans(const std::vector& present, const std::vector& referenced, const std::vector& 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& 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). // // 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 pruneDeletePlan(const std::vector& confirmed, const std::vector& freshOrphans); } // namespace reasampler