fix: 7→8 migration preserves branch-scoped non-entity state instead of deleting it

The one-time 7→8 layout migration rescues the head branch's entities
(branches/<head>/entities/* → entities/*), then drained the entire
branches/<head>/ directory. Any durable state a 7.x engine wrote under the head
branch OUTSIDE entities/ — a branch-scoped index, blob area, or field registry —
would be silently deleted by that drain, the same failure class as the VFS
content blobs a 7.x store kept in _cow/.

Guard it: after the entity move, list what remains under branches/<head>/ and
exclude entities/. If anything survives, it is non-entity durable state, so
PRESERVE the branch (skip removeRawPrefix) and warn loudly with the leftover
keys — recoverable, not lost. A clean branch (only the moved entities) still
drains exactly as before. Adds a PARITY GUARD test to the 7→8 migration suite.
This commit is contained in:
David Snelling 2026-07-07 16:07:10 -07:00
parent 9d5eb33c97
commit a93bb4e85f
2 changed files with 61 additions and 4 deletions

View file

@ -13014,16 +13014,38 @@ export class Brainy<T = any> implements BrainyInterface<T> {
}
// FINALIZE: mark migrated (makes re-open a no-op), then drain the head
// branch (its entities moved; the rest is stale 7.x derived state).
// Non-head branches under branches/ are 7.x version history 8.0's MVCC does
// not import — they are left in place.
// branch. Non-head branches under branches/ are 7.x version history 8.0's
// MVCC does not import — they are left in place.
await probe.writeRawObject('_system/migration-layout.json', {
layout: 'flat-v8',
version: 8,
fromBranch: head,
entitiesMoved: moved
})
await probe.removeRawPrefix(`branches/${head}`)
// Parity guard (defense-in-depth; the VFS `_cow/` stranding lesson). The
// entity move deleted every `branches/<head>/entities/*` key, so anything
// STILL under `branches/<head>/` is non-entity durable state a 7.x engine
// wrote outside `entities/` (branch-scoped blobs, an index dir, a field
// registry). Draining it unconditionally would silently DELETE it — the
// same failure class as the VFS content blobs stranded in `_cow/`. So
// drain only a clean branch (nothing but the moved entities); if any
// non-entity object survives, PRESERVE the branch (skip the drain) so the
// state stays recoverable, and surface it loudly.
const residual = (
await probe.listRawObjects(`branches/${head}`)
).filter((k: string) => !k.startsWith(`branches/${head}/entities/`))
if (residual.length === 0) {
await probe.removeRawPrefix(`branches/${head}`)
} else if (!this.config.silent) {
const sample = residual.slice(0, 8).join(', ')
console.warn(
`[brainy] 7→8 migration preserved ${residual.length} non-entity object(s) under ` +
`branches/${head}/ — branch-scoped durable state written outside entities/. The ` +
`branch was NOT drained, so this state is recoverable; inspect it before removing ` +
`branches/ manually. Keys: ${sample}${residual.length > 8 ? ', …' : ''}`
)
}
} finally {
if (canLock && typeof lockable.releaseWriterLock === 'function') {
await lockable.releaseWriterLock()