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
+26 -20
View File
@@ -161,10 +161,15 @@ bool ReaSamplerSession::saveToActiveProject() {
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
kProjExtTailKey, tailJson.c_str());
// Written on every save so the manifest and the bank stay in lockstep on disk.
const std::string ownedJson = owned_.serialize();
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
kProjExtOwnedKey, ownedJson.c_str());
// Written on every save so the ledger and the bank stay in lockstep on disk
// EXCEPT over a blob we could not read: replacing it would destroy the only
// record of every file created before the corruption, silently turning them
// into permanently unreclaimable foreign files.
if (trackingStatus_ != tracking::LedgerStatus::Unreadable) {
const std::string ledgerJson = tracking_.serialize();
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
kProjExtOwnedKey, ledgerJson.c_str());
}
// stampVersion() (not appVersion()) is the numeric triple only, no "-beta"
// suffix, so the stamp is byte-identical to stable regardless of channel
@@ -229,21 +234,20 @@ capture::TailSetting loadTailSetting(ReaProject* proj) {
return *loaded;
}
// Absent/empty key -> empty manifest. Malformed JSON warns and falls back to
// empty; prune then attributes nothing until the next capture rebuilds it —
// degrades safety, never correctness.
model::OwnedFileManifest loadOwnedManifest(ReaProject* proj) {
if (!proj) return model::OwnedFileManifest{};
const std::string ownedJson =
getProjExtStateString(proj, projExtNamespace(), kProjExtOwnedKey);
if (ownedJson.empty()) return model::OwnedFileManifest{}; // no stored manifest -> empty
std::optional<model::OwnedFileManifest> loaded =
model::OwnedFileManifest::deserialize(ownedJson);
if (!loaded) {
ShowConsoleMsg("ReaSampler: stored owned-file manifest is malformed -- ignoring.\n");
return model::OwnedFileManifest{};
// Absent/empty key -> Fresh (a new project, or a bank predating the ledger).
// Malformed -> Unreadable, which halts the prune and suppresses the next write
// rather than degrading to an empty ledger that looks like "nothing was ever
// created".
tracking::LedgerLoad loadOriginLedger(ReaProject* proj) {
if (!proj) return tracking::LedgerLoad{};
tracking::LedgerLoad load = tracking::loadLedger(
getProjExtStateString(proj, projExtNamespace(), kProjExtOwnedKey));
if (load.status == tracking::LedgerStatus::Unreadable) {
ShowConsoleMsg("ReaSampler: the stored file-tracking ledger is malformed. Prune "
"is halted for this project and the stored value is left intact "
"for recovery.\n");
}
return std::move(*loaded);
return load;
}
} // namespace
@@ -255,13 +259,15 @@ void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDi
// consumeLoadSignal() on the same tick.
loadPending_ = true;
// view_/tail_/owned_ are all restored on EVERY load path: switching to a
// view_/tail_/tracking_ are all restored on EVERY load path: switching to a
// project with no stored state must reset to default, never inherit the
// previous project's. An undo/redo reload must re-read the restored
// values so they match the rolled-back state.
view_ = loadViewModel(static_cast<ReaProject*>(proj));
tail_ = loadTailSetting(static_cast<ReaProject*>(proj));
owned_ = loadOwnedManifest(static_cast<ReaProject*>(proj));
tracking::LedgerLoad ledger = loadOriginLedger(static_cast<ReaProject*>(proj));
trackingStatus_ = ledger.status;
tracking_ = std::move(ledger.ledger);
// An absent stamp classifies as PreVersioning, a malformed one as Unknown
// — both silent. proj == nullptr -> "" -> default.