tracking: one ledger, one authority — prune protection and replace-vs-add answered from the same records, fail-safe on unreadable state

This commit is contained in:
2026-07-30 19:44:11 -04:00
parent 7bd911d58b
commit 7f70d94228
40 changed files with 1546 additions and 633 deletions
+4 -4
View File
@@ -20,9 +20,9 @@ enumeration and the actual deletion live in `shell/persist` (`prune_fs`), not he
- Referenced-set is the union across ALL banks, pool included: a file is an orphan
iff no bank in the book references it. This is the safety-critical computation —
the prune null test is *prune never deletes a file that any index references.*
- Orphan attribution is an owned-file manifest (fork R-D): the book tracks the set
of files it has created; prune reclaims `(owned ∩ on-disk) referenced`. This
rejects folder-sweep (which would delete hand-dropped files).
- Orphan attribution comes from the tracking ledger (`core/tracking`): the book
tracks the files it has created; prune reclaims `(owned ∩ on-disk) referenced`.
This rejects folder-sweep (which would delete hand-dropped files).
- **Prune null test:** a prune of a folder whose every file is referenced by some
bank deletes nothing; a prune deletes exactly the `present referenced` orphan
set and nothing else.
@@ -32,7 +32,7 @@ enumeration and the actual deletion live in `shell/persist` (`prune_fs`), not he
## Modules
- `prune_reconcile` — pure prune core: `pruneOrphans(present, referenced, owned)` computes `(owned ∩ present) referenced`; the safety-critical "which files are orphans" decision, filesystem-free and hard-tested before any I/O exists. Gains `mergeReferenced(bankRefs, liveInstanceHeldPaths)` (pS-usage) — unions live instance holds into the prune referenced-set so the pure orphan computation includes them.
- `prune_reconcile` — pure prune core: `pruneOrphans(present, referenced, owned)` computes `(owned ∩ present) referenced`; the safety-critical "which files are orphans" decision, filesystem-free and hard-tested before any I/O exists. `mergeReferenced(bankRefs, heldPaths)` unions live instance holds into the referenced-set. Two of the three inputs (`owned`, and the held half of `referenced`) come from `core/tracking`'s authority, never assembled ad hoc by a shell. `PruneReport` carries the authority's verdict: `blockedByTracking` + `ledgerUnreadable` / `unreadableUsageKeys`.
## Gotchas
+1 -1
View File
@@ -13,7 +13,7 @@ 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
// (Sample.relativePath / OriginLedger::contains). std::string hashes/compares
// byte-for-byte, so no normalization creeps in.
const std::unordered_set<std::string> referencedSet(referenced.begin(),
referenced.end());
+17 -18
View File
@@ -10,20 +10,20 @@
// 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: files the bank system itself created. A
// present-but-unowned (hand-dropped) file is NEVER reclaimed.
// * owned — the tracking ledger's paths: files the bank system itself created.
// A present-but-unowned (hand-dropped) file is NEVER reclaimed.
//
// The three 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).
// but-absent ledger record yields no orphan, no error).
// * ∩ owned — never a hand-dropped file (ownership attribution).
// * referenced — never a file any bank references (union safety, prune null test).
//
// Path representation: EXACT-STRING match everywhere — Sample.relativePath,
// OwnedFileManifest::contains, BankModel all use raw std::string equality: no
// OriginLedger::contains, BankModel all use raw std::string equality: no
// separator normalization, no case-folding, no trailing-slash trimming. Feeding a
// consistent spelling across the three inputs is the shell's contract (it enumerates
// the folder, unions the book, and reads the manifest against the SAME resolved
// the folder, unions the book, and reads the ledger 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.
@@ -48,23 +48,22 @@ namespace reasampler::reclaim {
// order (deterministic). MAY be truncated for a large set (the
// shell's display cap); `count` stays exact regardless.
// * truncated — true iff `orphans` holds fewer than `count` entries.
// * abortedUnreadableUsage — true iff the scan found a present-but-unreadable
// rsusage_* instance-usage record: 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 scan shell, never by buildPruneReport (which stays a
// pure tally).
// * offendingUsageKeys — the exact "rsusage_<guid>" key names that triggered the
// abort (non-empty iff abortedUnreadableUsage), so the operator
// can clear each key via ReaScript:
// reaper.SetProjExtState(0, "reasampler", "<key>", "")
// * blockedByTracking — true iff the tracking authority could not answer the
// protection question: 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 scan
// shell, never by buildPruneReport (which stays a pure tally).
// * ledgerUnreadable / unreadableUsageKeys — which side blocked, so the action can
// tell the operator what to recover. The key names are the exact
// "rsusage_<guid>" spellings.
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
bool blockedByTracking = false;
bool ledgerUnreadable = false;
std::vector<std::string> unreadableUsageKeys;
};
// The outcome of an actual prune DELETION. The shell fills this as it deletes the
@@ -91,7 +90,7 @@ struct PruneDeletionResult {
//
// Returns the subset of `present` that is BOTH owned AND unreferenced, in the order
// they appear in `present` (deterministic — mirrors the insertion-order determinism
// the index/manifest keep). Duplicate spellings within `present` are de-duplicated
// the index/ledger keep). Duplicate spellings within `present` are de-duplicated
// in the result (a folder enumeration yields distinct names, but the core does not
// rely on that).
//