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

@ -191,4 +191,39 @@ describe('7.x → 8.0 layout migration', () => {
expect(await captureReference(reopened)).toEqual(ref)
expect(fs.existsSync(path.join(dir, 'branches'))).toBe(false)
})
it('PARITY GUARD: branch-scoped non-entity state survives migration (not silently drained)', async () => {
const dir = freshLegacyDir()
// Simulate a 7.x engine that wrote durable state under the HEAD branch
// OUTSIDE entities/ (a branch-scoped index/blob/registry). The migration
// rescues entities/ only; the guard must PRESERVE the rest rather than let
// removeRawPrefix silently delete it — the VFS `_cow/` stranding lesson.
const raw: any = new FileSystemStorage(dir)
await raw.init()
await raw.writeRawObject('branches/main/_legacy_engine/state', { keep: 'me', n: 42 })
await raw.flush?.()
raw.stopFlushRequestWatcher?.()
await raw.releaseWriterLock?.()
// Migrate (entities collapse to the root as usual).
const brain = await openBrain(dir)
brains.push(brain)
await brain.init()
expect(await captureReference(brain)).toEqual(reference)
await brain.close()
brains.splice(brains.indexOf(brain), 1)
// The non-entity branch-scoped state is PRESERVED (branch NOT drained) —
// recoverable, not lost.
const check: any = new FileSystemStorage(dir)
await check.init()
expect(await check.readRawObject('branches/main/_legacy_engine/state')).toMatchObject({
keep: 'me',
n: 42
})
await check.flush?.()
check.stopFlushRequestWatcher?.()
await check.releaseWriterLock?.()
})
})