#include "prune_reconcile.h" #include // 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 pruneOrphans(const std::vector& present, const std::vector& referenced, const std::vector& 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 referencedSet(referenced.begin(), referenced.end()); const std::unordered_set ownedSet(owned.begin(), owned.end()); std::vector orphans; std::unordered_set 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 mergeReferenced(const std::vector& primary, const std::vector& extra) { std::vector merged; merged.reserve(primary.size() + extra.size()); std::unordered_set 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& orphans, const std::unordered_map& 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 pruneDeletePlan(const std::vector& confirmed, const std::vector& 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 freshSet(freshOrphans.begin(), freshOrphans.end()); std::vector plan; std::unordered_set 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