fix(persist): use project-object identity to stop forked-bank cross-contamination

M4's GUID-only classifier read a tab-switch between two Save-As forks (shared
copied GUID, different paths) as a Save-As and clobbered a bank. Thread
sameProjectObject into classifyProjectTransition: a different object always
Loads, never relocates; a forked sibling gets re-GUID'd to diverge.
This commit is contained in:
2026-07-22 21:28:54 -04:00
parent aacf8b49e8
commit 36270e2064
5 changed files with 212 additions and 116 deletions
+17 -18
View File
@@ -105,30 +105,29 @@ BankRelocation deriveRelocationPlan(const std::string& oldProjectDir,
return r;
}
ProjectTransition classifyProjectTransition(const std::string& lastGuid,
ProjectTransition classifyProjectTransition(bool sameProjectObject,
const std::string& lastGuid,
const std::string& lastPath,
const std::string& currentGuid,
const std::string& currentPath) {
// A changed GUID is the unambiguous signal of a different project — load it.
// This is the whole point of the content-based identity: it survives pointer
// recycling that the old pointer-equality check could not distinguish from a
// Save-As.
if (currentGuid != lastGuid) {
// A DIFFERENT project object is a tab-switch / open / recycled pointer — load
// ITS index; NEVER relocate a bank. This is the load-bearing safety fix: it
// holds even when currentGuid == lastGuid, which is exactly the forked-sibling
// case (Save-As copied our GUID, so two distinct projects share it on disk).
// The GUID is deliberately NOT consulted here — the object identity alone
// decides, and it cannot be fooled by a copied GUID.
(void)lastGuid;
(void)currentGuid;
if (!sameProjectObject) {
return ProjectTransition::Load;
}
// Same GUID from here on. If it is empty, we have NO proof the two ticks saw
// the same project (an unsaved project can't carry a stored GUID). A path
// change under an empty GUID is therefore a first-save or a switch between
// unsaved projects — load, never relocate (relocating would copy the wrong
// bank over a real one, the defect being fixed).
if (currentGuid.empty()) {
return (currentPath == lastPath) ? ProjectTransition::NoOp
: ProjectTransition::Load;
}
// Same NON-EMPTY GUID: proven same project. A path change is a genuine
// Save-As to a new location; an unchanged path is Save-in-place / idle.
// Same object from here on: identity is PROVEN by the pointer. A path change is
// a Save-As (or a first save, when the old path was empty); an unchanged path
// is Save-in-place / idle. Note SaveAsRelocate is safe even for a first save:
// the old project dir is empty, so deriveRelocationPlan makes `needed` false
// and nothing is physically relocated (empty-GUID safety preserved), while
// poll() still mints a GUID on that branch.
return (currentPath == lastPath) ? ProjectTransition::NoOp
: ProjectTransition::SaveAsRelocate;
}