open-brainy/tests/integration/fact-log-v2-cutover.test.ts

398 lines
18 KiB
TypeScript
Raw Normal View History

/**
* @module tests/integration/fact-log-v2-cutover
* @description The fact log's LIVE WRITE FORMAT cutover to v2, end-to-end
* through real brains: (a) a NEW brain's tail segment carries a v2 header
* (formatVersion 2, sealSize 4096), opens with the log.genesis record
* (id-space width 64 + the manifest-persisted brainId), and scanFacts yields
* the same CommitFact shape a v1 brain would reconstruction included,
* proven by digest-equality against canonical after a reopen; (b) MIXED
* logs: an existing v1 segment stays readable forever beside a v2 tail
* (cutover-by-rotation; the v1 segment is never rewritten); (c) MINT:
* after-image records carry the metadata index id mapper's exact int
* assignments (white-box compare); (d) SEALS: every flush leaves the tail
* sector-aligned, and pads are invisible to scans; (e) REPLAY: the
* log-authority recovery path resurrects an acked write from a v2 tail
* after a crash-style abandon.
*/
import { describe, it, expect, afterEach } from 'vitest'
import * as fs from 'node:fs'
import * as path from 'node:path'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Brainy } from '../../src/brainy.js'
import { NounType } from '../../src/types/graphTypes.js'
import {
parseSegmentHeader,
decodeGroupV2,
SEGMENT_HEADER_BYTES,
FACT_LOG_FORMAT_V1,
FACT_LOG_FORMAT_V2,
type LogGenesisRecord,
type NounAfterImageRecord
} from '../../src/db/factLogFormat.js'
import type { CommitFact, FactIntMinter, FactLog } from '../../src/db/factLog.js'
import {
makeTempDir,
openBrain,
storeOf,
abandonAsCrashed,
factGenerations,
vec,
uid
} from '../helpers/durabilityKillMatrix.js'
/** The VFS root — created at init by a baseline (generation-less) write. */
const VFS_ROOT = '00000000-0000-0000-0000-000000000000'
const FACTS_DIR = ['_generations', 'facts'] as const
const MANIFEST_PATH = '_generations/facts/manifest.json'
/** White-box internals this suite instruments. */
type BrainInternals = {
storage: {
readRawObject(p: string): Promise<unknown | null>
readNounRaw(id: string): Promise<{ metadata: unknown | null; vector: unknown | null }>
}
metadataIndex: {
getIdMapper(): { getInt(uuid: string): number | undefined }
}
}
const internals = (brain: Brainy): BrainInternals => brain as unknown as BrainInternals
/** The facts manifest as stored (additive brainId included). */
interface StoredFactsManifest {
segments: Array<{ file: string }>
tailSegment: string | null
brainId?: string
}
async function readManifest(brain: Brainy): Promise<StoredFactsManifest> {
const manifest = (await internals(brain).storage.readRawObject(
MANIFEST_PATH
)) as StoredFactsManifest | null
expect(manifest, 'the facts manifest exists').toBeTruthy()
return manifest!
}
/** Raw on-disk bytes of one fact segment file. */
function segmentBytes(dir: string, file: string): Uint8Array {
return new Uint8Array(fs.readFileSync(path.join(dir, ...FACTS_DIR, file)))
}
async function allFacts(brain: Brainy): Promise<CommitFact[]> {
const scan = (brain as unknown as { scanFacts(): { batches(): AsyncGenerator<{ facts: CommitFact[] }> } | null }).scanFacts()
expect(scan, 'this storage hosts a fact log').not.toBeNull()
const facts: CommitFact[] = []
for await (const batch of scan!.batches()) facts.push(...batch.facts)
return facts
}
/** The live FactLog instance (white-box: the minter strip in scenario b). */
function factLogOf(brain: Brainy): FactLog & { intMinter: FactIntMinter | null } {
const log = storeOf(brain).getFactLog()
expect(log, 'filesystem storage hosts a fact log').not.toBeNull()
return log as FactLog & { intMinter: FactIntMinter | null }
}
describe('fact log v2 cutover — live writes land in the v2 segment format', () => {
const dirs: string[] = []
const brains: Brainy[] = []
const trackDir = (): string => {
const dir = makeTempDir()
dirs.push(dir)
return dir
}
const track = (brain: Brainy): Brainy => {
brains.push(brain)
return brain
}
afterEach(async () => {
for (const b of brains.splice(0)) {
await (b as unknown as { close?: () => Promise<void> }).close?.().catch(() => {})
}
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
})
it('(a) NEW BRAIN: v2 tail header, genesis-first, and scanFacts parity with canonical across a reopen', async () => {
const dir = trackDir()
const brain = track(await openBrain(dir))
const idA = uid('v2-new-a')
const idB = uid('v2-new-b')
await brain.add({ id: idA, data: 'alpha', type: NounType.Document, vector: vec(1), metadata: { n: 1 } })
await brain.add({ id: idB, data: 'beta', type: NounType.Document, vector: vec(2), metadata: { n: 2 } })
await brain.flush()
// The tail segment's raw header bytes: formatVersion 2, sealSize 4096.
const manifest = await readManifest(brain)
expect(manifest.tailSegment).toBeTruthy()
expect(manifest.brainId, 'the brain id was minted into the manifest').toBeTruthy()
const bytes = segmentBytes(dir, manifest.tailSegment!)
const header = parseSegmentHeader(bytes.subarray(0, SEGMENT_HEADER_BYTES))
expect(header.formatVersion).toBe(FACT_LOG_FORMAT_V2)
expect(header.sealSize).toBe(4096)
// Genesis is the FIRST record of the FIRST fact — and appears exactly once.
const group = decodeGroupV2(bytes.subarray(SEGMENT_HEADER_BYTES), { expectedIdSpaceWidth: 64 })
expect(group.facts.length).toBeGreaterThanOrEqual(2)
const firstRecord = group.facts[0].records[0]
expect(firstRecord.type).toBe('log.genesis')
const genesis = firstRecord as LogGenesisRecord
expect(genesis.idSpaceWidth).toBe(64)
expect(genesis.brainId).toBe(manifest.brainId)
const genesisCount = group.facts
.flatMap((f) => f.records)
.filter((r) => r.type === 'log.genesis').length
expect(genesisCount).toBe(1)
// Shape parity + reconstruction fidelity: REOPEN (so the tail decodes
// from disk, not from the in-session originals) and compare each add's
// CommitFact op against canonical byte truth — metadata leg (bigint
// timestamps normalized back to numbers) AND the reconstructed vector
// wrapper must equal what readNounRaw returns, exactly as a v1 log's
// byte-faithful capture would.
await (brain as unknown as { close: () => Promise<void> }).close()
brains.splice(brains.indexOf(brain), 1)
const reopened = track(await openBrain(dir))
const facts = await allFacts(reopened)
const gens = facts.map((f) => f.generation)
expect([...gens].sort((a, b) => a - b)).toEqual(gens)
expect(new Set(gens).size).toBe(gens.length)
const logGens = new Set(
((await (reopened as unknown as { transactionLog(): Promise<Array<{ generation: number }>> }).transactionLog()) ?? []).map(
(e) => e.generation
)
)
for (const g of gens) expect(logGens.has(g), `generation ${g} is a real commit`).toBe(true)
for (const id of [idA, idB]) {
const fact = facts.find((f) => f.ops.some((op) => op.id === id && op.record !== null))
expect(fact, `the add fact for ${id} survives the reopen`).toBeDefined()
const op = fact!.ops.find((o) => o.id === id)!
expect(op.kind).toBe('noun')
const canonical = await internals(reopened).storage.readNounRaw(id)
expect(op.record!.metadata).toStrictEqual(canonical.metadata)
feat(embedding): deferred-embed markers become log records — the sidecar recovery path is deleted The private recovery discipline, applied to its own machinery: pending- embed markers stop being sidecar files and become first-class log records riding the write's OWN commit fact — embed.pending lands in the same atomic append as its after-image (a marker can never be orphaned from its write, or vice versa; in durable-at-ack mode it shares the write's covering fsync — zero extra syncs), and the worker's landing commit rides embed.landed with the inline vector. Crash recovery is now a FOLD of the log (pending without a matching landed = recovered), skipped wholesale on brains with no v2 history; the one-time legacy bridge folds existing sidecar files in, migrates them as one fact, and deletes them — idempotent under a crash mid-bridge. No code path writes the sidecar again. Plus the ENTITY-TRUTH digest law, found by this train's own pins: canonical vector wrappers denormalize HNSW residue (connections + the randomly-assigned node level) that the log deliberately does not carry — the verification oracle digested it and would have reported false state-differs on ~any nonzero-level node (a ~15% flake in the cutover pin was the symptom). Both sides of every oracle comparison now normalize to entity truth (nounEntityTruth); index residue has its own rebuild path and is not entity state. Pins: embed-markers-in-log 5/5 (same-generation marker, landed+fold-to- zero, crash recovery via the log with the sidecar prefix EMPTY on disk, legacy bridge, VFS hung-embedder ack) · deferred-embedding 5/5 unchanged (the contract outlived its mechanism) · kill-matrix 11/11 · cutover 5/5 ×10 runs (flake dead) · unit 2031/2031.
2026-08-10 11:27:07 -07:00
// ENTITY TRUTH comparison: canonical wrappers denormalize HNSW residue
// (connections + the randomly-assigned level) that the log record
// deliberately reconstructs empty — strip both sides (the oracle's
// normalizer law) so a nonzero random level can't fake a divergence.
const strip = (w: unknown) => {
const { connections: _c, level: _l, ...rest } = w as Record<string, unknown>
return rest
}
expect(strip(op.record!.vector)).toStrictEqual(strip(canonical.vector))
}
})
it('(b) MIXED LOG: an existing v1 segment stays readable forever beside the v2 tail (cutover by rotation, v1 bytes untouched)', async () => {
// ROUTE: a REAL v1 segment is written by the v1 writer itself — the live
// FactLog with its minter stripped (the exact pre-cutover code path,
// still shipped for minter-less configurations) — then the minter is
// restored mid-session and the next append performs the cutover
// rotation. Stronger than hand-crafted bytes: both formats come from
// their real writers, on one log.
const dir = trackDir()
const brain = track(await openBrain(dir))
const log = factLogOf(brain)
const minter = log.intMinter
expect(minter, 'the brain wired the int minter at init').toBeTruthy()
log.intMinter = null // the pre-cutover writer
const idOld1 = uid('v1-old-1')
const idOld2 = uid('v1-old-2')
await brain.add({ id: idOld1, data: 'old one', type: NounType.Document, vector: vec(3), metadata: { era: 'v1' } })
await brain.add({ id: idOld2, data: 'old two', type: NounType.Document, vector: vec(4), metadata: { era: 'v1' } })
await brain.flush()
const before = await readManifest(brain)
expect(before.segments).toHaveLength(0)
const v1TailFile = before.tailSegment!
const v1Bytes = segmentBytes(dir, v1TailFile)
expect(parseSegmentHeader(v1Bytes.subarray(0, SEGMENT_HEADER_BYTES)).formatVersion).toBe(
FACT_LOG_FORMAT_V1
)
log.intMinter = minter // the cutover lands mid-session
const idNew = uid('v2-new')
await brain.add({ id: idNew, data: 'new era', type: NounType.Document, vector: vec(5), metadata: { era: 'v2' } })
await brain.flush()
// The v1 tail was SEALED (bytes untouched), the new tail is v2.
const after = await readManifest(brain)
expect(after.segments.map((s) => s.file)).toContain(v1TailFile)
expect(after.tailSegment).not.toBe(v1TailFile)
const sealedBytes = segmentBytes(dir, v1TailFile)
expect(parseSegmentHeader(sealedBytes.subarray(0, SEGMENT_HEADER_BYTES)).formatVersion).toBe(
FACT_LOG_FORMAT_V1
)
expect(
Buffer.compare(Buffer.from(sealedBytes), Buffer.from(v1Bytes)),
'the sealed v1 segment is byte-identical — never rewritten'
).toBe(0)
const tailBytes = segmentBytes(dir, after.tailSegment!)
expect(parseSegmentHeader(tailBytes.subarray(0, SEGMENT_HEADER_BYTES)).formatVersion).toBe(
FACT_LOG_FORMAT_V2
)
// NOT a brand-new log: no genesis on a rotated-in v2 tail.
const tailGroup = decodeGroupV2(tailBytes.subarray(SEGMENT_HEADER_BYTES), {
expectedIdSpaceWidth: 64
})
expect(
tailGroup.facts.flatMap((f) => f.records).some((r) => r.type === 'log.genesis')
).toBe(false)
// One scan spans both formats, shape-identically, in generation order.
const liveFacts = await allFacts(brain)
const liveGens = liveFacts.map((f) => f.generation)
expect([...liveGens].sort((a, b) => a - b)).toEqual(liveGens)
for (const id of [idOld1, idOld2, idNew]) {
const fact = liveFacts.find((f) => f.ops.some((op) => op.id === id))
expect(fact, `fact for ${id} is scannable`).toBeDefined()
const op = fact!.ops.find((o) => o.id === id)!
expect(op.kind).toBe('noun')
expect(op.record).not.toBeNull()
}
// The MIXED log survives a reopen and keeps appending (v2 tail).
await (brain as unknown as { close: () => Promise<void> }).close()
brains.splice(brains.indexOf(brain), 1)
const reopened = track(await openBrain(dir))
const reFacts = await allFacts(reopened)
expect(reFacts.map((f) => f.generation)).toEqual(liveGens)
// The v1 fact still reads exactly as the v1 decoder always read it.
// (Not compared byte-strict against canonical: the v1 CAPTURE has a
// known pre-existing wart — write-cache-warm objects carry
// undefined-valued engine keys that msgpack preserves as nil while the
// durable JSON drops them. v1 bytes are frozen; the v2 encoder
// sanitizes to durable truth instead — pinned in scenario (a).)
const oldOp = reFacts
.find((f) => f.ops.some((op) => op.id === idOld1))!
.ops.find((o) => o.id === idOld1)!
const canonicalOld = await internals(reopened).storage.readNounRaw(idOld1)
const oldMeta = oldOp.record!.metadata as Record<string, unknown>
expect(oldMeta.noun).toBe('document')
expect((oldMeta.metadata as Record<string, unknown>).era).toBe('v1')
const oldWrapper = oldOp.record!.vector as { id: string; vector: number[] }
const canonicalWrapper = canonicalOld.vector as { id: string; vector: number[] }
expect(oldWrapper.id).toBe(idOld1)
expect(oldWrapper.vector).toStrictEqual(canonicalWrapper.vector)
await reopened.add({ id: uid('post-reopen'), data: 'still writing', type: NounType.Document, vector: vec(6), metadata: {} })
expect((await factGenerations(reopened)).length).toBe(liveGens.length + 1)
})
it('(c) MINT-AT-APPEND: after-image records carry the id mapper\'s EXACT int assignments — distinct, nonzero, reproducible', async () => {
const dir = trackDir()
const brain = track(await openBrain(dir))
const idA = uid('mint-a')
const idB = uid('mint-b')
await brain.add({ id: idA, data: 'mint one', type: NounType.Document, vector: vec(7), metadata: { m: 1 } })
await brain.add({ id: idB, data: 'mint two', type: NounType.Document, vector: vec(8), metadata: { m: 2 } })
await brain.flush()
const manifest = await readManifest(brain)
const bytes = segmentBytes(dir, manifest.tailSegment!)
const group = decodeGroupV2(bytes.subarray(SEGMENT_HEADER_BYTES), { expectedIdSpaceWidth: 64 })
const afterImages = new Map<string, NounAfterImageRecord>()
for (const fact of group.facts) {
for (const record of fact.records) {
if (record.type === 'noun.afterImage') afterImages.set(record.id, record)
}
}
const recA = afterImages.get(idA)
const recB = afterImages.get(idB)
expect(recA, 'idA has a decoded after-image').toBeDefined()
expect(recB, 'idB has a decoded after-image').toBeDefined()
expect(recA!.entityInt).toBeGreaterThan(0n)
expect(recB!.entityInt).toBeGreaterThan(0n)
expect(recA!.entityInt).not.toBe(recB!.entityInt)
// White-box: the ints on the wire ARE the metadata index mapper's
// assignments — the exact ints a mapper rebuild must reproduce.
const mapper = internals(brain).metadataIndex.getIdMapper()
expect(recA!.entityInt).toBe(BigInt(mapper.getInt(idA)!))
expect(recB!.entityInt).toBe(BigInt(mapper.getInt(idB)!))
})
it('(d) SEALS AT SYNC: every flush leaves the tail sector-aligned; pads are invisible to scans', async () => {
const dir = trackDir()
const brain = track(await openBrain(dir))
await brain.add({ id: uid('seal-1'), data: 'one', type: NounType.Document, vector: vec(10), metadata: {} })
await brain.flush()
const manifest = await readManifest(brain)
const tailPath = path.join(dir, ...FACTS_DIR, manifest.tailSegment!)
const sizeAfterFirstFlush = fs.statSync(tailPath).size
expect(sizeAfterFirstFlush).toBeGreaterThan(0)
expect(sizeAfterFirstFlush % 4096, 'tail is sector-aligned after flush').toBe(0)
const countAfterFirstFlush = (await factGenerations(brain)).length
for (let i = 0; i < 3; i++) {
await brain.add({ id: uid(`seal-more-${i}`), data: `more ${i}`, type: NounType.Document, vector: vec(11 + i), metadata: { i } })
}
await brain.flush()
const sizeAfterSecondFlush = fs.statSync(tailPath).size
expect(sizeAfterSecondFlush).toBeGreaterThan(sizeAfterFirstFlush)
expect(sizeAfterSecondFlush % 4096, 'still aligned after more writes + flush').toBe(0)
// Pads count toward bytes, never toward facts.
expect((await factGenerations(brain)).length).toBe(countAfterFirstFlush + 3)
})
it('(e) REPLAY COMPAT: the log-authority recovery path resurrects an acked write from a v2 tail after a crash-style abandon', async () => {
// The flip idiom from the log-authority suite: seed writes, baseline
// backfill LAST (the init-time VFS root never got a fact), flush, then
// the sanctioned guarded flip — the oracle goes green over an ALL-V2
// log, which is itself the reproduction proof for the v2 record path.
const dir = mkdtempSync(join(tmpdir(), 'brainy-v2-cutover-'))
dirs.push(dir)
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
const open = async (): Promise<Brainy> => {
const b = new Brainy({
storage: { type: 'filesystem', path: dir },
requireSubtype: false,
silent: true,
dimensions: 384
})
await b.init()
return track(b)
}
const brain = await open()
const kept = await brain.add({ data: 'alpha document', type: 'document', metadata: { n: 1 } })
const removed = await brain.add({ data: 'beta document', type: 'document', metadata: { n: 2 } })
await brain.update({ id: kept, metadata: { n: 10 } })
await brain.remove(removed)
const root = await brain.get(VFS_ROOT)
expect(root, 'the VFS root exists').toBeTruthy()
await brain.update({ id: VFS_ROOT, metadata: root!.metadata }) // baseline backfill — final write
await brain.flush()
const report = await (brain as unknown as { adoptLogAuthority(): Promise<{ verdict: string }> }).adoptLogAuthority()
expect(report.verdict, 'the oracle is green over a pure-v2 log').toBe('green')
// An at-ack write: its v2 fact is fsynced (sector-sealed) at ack.
const survivor = await brain.add({
data: 'survives power loss',
type: 'document',
metadata: { s: 1 }
})
// Crash-style abandon: RAM state gone, no flush, no close.
await abandonAsCrashed(brain)
// Reopen: open() finds the acked fact ABOVE the manifest watermark in
// the v2 tail (peekFactsAbove → v2 decode) and REPLAYS it into
// canonical — an acked write is never lost.
const reopened = await open()
expect(
(reopened as unknown as { logAuthority(): { authority: string } }).logAuthority().authority
).toBe('log')
const resurrected = await reopened.get(survivor)
expect(resurrected, 'the acked write survived the crash').toBeTruthy()
expect((resurrected as { metadata?: { s?: number } }).metadata?.s).toBe(1)
expect((await factGenerations(reopened)).length).toBeGreaterThan(0)
})
})