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
+24 -12
View File
@@ -26,8 +26,8 @@
#include "core/capture/tail_control.h"
#include "core/model/bank_book.h"
#include "core/model/bank_model.h"
#include "core/model/owned_manifest.h"
#include "core/reclaim/prune_reconcile.h"
#include "core/tracking/origin_ledger.h"
#include "core/version/app_version.h"
#include "core/view/view_mode_model.h"
@@ -71,9 +71,18 @@ public:
capture::TailSetting& tail() { return tail_; }
const capture::TailSetting& tail() const { return tail_; }
// Project-relative files the capture path itself created; prune consumes it.
model::OwnedFileManifest& owned() { return owned_; }
const model::OwnedFileManifest& owned() const { return owned_; }
// Birth records for the files the system itself created. Read as a PAIR —
// the ledger alone cannot say whether an absent record means never-recorded
// or unreadable, and the two demand opposite treatment. This is the reach
// any consumer outside persist uses to build a tracking::TrackingState.
const tracking::OriginLedger& tracking() const { return tracking_; }
tracking::LedgerStatus trackingStatus() const { return trackingStatus_; }
// Record a system-created file at the moment it exists — the ONLY way a
// birth record is written, so lineage can never be backfilled from a later
// guess. Lineage is read off the Sample's own provenance, the same act that
// stamped it, so the two cannot disagree. A repeat path is a no-op.
void recordCreated(const model::Sample& sample, tracking::OriginKind kind);
// The version that last wrote the active project: PreVersioning (no
// stamp), Unknown (malformed), or Stamped.
@@ -93,11 +102,10 @@ public:
// a persist happened, so a caller can skip an undo block when nothing was written.
bool saveToActiveProject();
// Report-only prune dry-run: feeds the pure core with (present,
// referenced, owned), where `referenced` = book references union every
// live instance's held captures (usage_scan + sample_usage decide
// liveness). FAIL-SAFE: an unreadable usage record sets
// abortedUnreadableUsage with an EMPTY orphan set. Read-only throughout.
// Report-only prune dry-run: feeds the pure core with (present, referenced,
// owned) — `present` from the folder enumeration, the other two from the
// tracking authority. FAIL-SAFE: tracking state the authority cannot read
// sets blockedByTracking with an EMPTY orphan set. Read-only throughout.
reclaim::PruneReport pruneDryRun() const;
// The full (untruncated) orphan set, same compute as pruneDryRun. The
@@ -110,7 +118,7 @@ public:
// file that vanished or became referenced since confirm is skipped, and
// an orphan the user did not see is never swept. Trash-preferred
// (Windows Recycle Bin; unlink elsewhere). Does not modify the book or
// OwnedFileManifest, writes no ext-state. No-ops when nothing to delete;
// the ledger, writes no ext-state. No-ops when nothing to delete;
// does not prompt.
reclaim::PruneDeletionResult pruneReclaim(
const std::vector<std::string>& confirmed) const;
@@ -144,7 +152,11 @@ private:
BankBook book_;
ViewModeModel view_; // reset to default on a project with no stored view_state
capture::TailSetting tail_; // reset to default (None / 2s) with no stored tail key
model::OwnedFileManifest owned_; // reset to empty/stored on EVERY load path, never inherited
tracking::OriginLedger tracking_; // reset to empty/stored on EVERY load path, never inherited
// Unreadable is sticky for the project's session: it halts the prune AND
// suppresses the ledger write, so a corrupt blob survives for recovery
// instead of being silently replaced by a ledger missing every earlier file.
tracking::LedgerStatus trackingStatus_ = tracking::LedgerStatus::Fresh;
version::WritingVersion writingVersion_; // recovered per load; PreVersioning default
std::int64_t bankGeneration_ = 0; // recovered per load (absent -> 0); monotonic
@@ -159,7 +171,7 @@ private:
bool reloadRequested_ = false; // raised by requestReload; drained by poll
// Load the book from `proj`'s ext state (`banks`, else legacy
// `bank_index` migrated into the pool); also restores view_/tail_/owned_.
// `bank_index` migrated into the pool); also restores view_/tail_/tracking_.
void loadFromProject(void* proj, const std::string& projectDir);
};