feat: generation fact log — after-image commit records, dual-written at every commit point
Every committed generation now also appends a FACT — an after-image
commit record (what each touched entity/relationship became, or a
body-less tombstone for a removal) — to an append-only, crc32c-framed
segment log under _generations/facts/. The before-image history and the
canonical tree remain authoritative; the fact log gives consumers ONE
sequential, self-verifying stream (index heals, incremental replays)
in place of a per-entity directory walk.
- Wire format: positional msgpack facts [generation, timestamp, ops,
meta, blobHashes]; op = [kind u8, id bin16, record | nil tombstone];
32-byte segment header (magic, formatVersion, firstGeneration,
zeroed+verified reserved); length+crc32c frame per fact; zero-padded
segment names so lexicographic order == generation order; JSON
manifest with an atomic rename flip, manifest-first rotation.
- Commit protocol: facts append+fsync BEFORE the commit point inside
the existing durability window, so a crash can only leave the log
AHEAD of committed truth — open() truncates back (torn tails detected
by CRC). Absent generation = never committed; a scan can never see an
uncommitted fact. transact() facts are durable-on-return; single-op
facts ride the group-commit flush exactly like buffered history. A
fact-append failure fails the write, loudly — a silent gap would be a
lie a later replay discovers.
- New public surface: brain.scanFacts() (sequential batches with heal
telemetry: head/segments/approx up front, per-batch generation range
+ bytes + segment id, loud abort on gaps, summary cross-check) and
brain.factSegmentPaths() (immutable sealed segments for zero-copy
consumers; the mutable tail excluded). Exported types CommitFact,
FactOp, FactScanBatch, FactScanHandle.
- Storage: optional binary raw-byte primitives (appendRawBytes,
readRawBytes, writeRawBytes, rawByteSize) on StorageAdapter —
feature-detected; filesystem + memory adapters implement them; an
adapter without them hosts no fact log. Fact segments are byte-copied
(never hard-linked) into snapshots. The _generations/facts/ namespace
is registered as a protected family (rebuildable: false): no sweeper
or GC may delete under it.
- New crc32c (Castagnoli) utility with RFC known-answer tests.
2026-07-15 10:49:02 -07:00
/ * *
* @module tests / integration / fact - log - dual - write
* @description The generation fact log end - to - end through real commits : every
* committed generation ( single - op AND transact ) appends its AFTER - IMAGE fact
* at the commit point ; removals append body - less tombstones ; an aborted
* transaction leaves no fact ; facts survive reopen and continue monotonically ;
* the scan surface ( brain . scanFacts ) carries the frozen telemetry shape ; and
* the fact - log namespace is protected against prefix - nuking .
* /
import { describe , it , expect , beforeEach , afterEach } from 'vitest'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { Brainy , ProtectedArtifactError , type CommitFact } from '../../src/index.js'
async function allFacts ( brain : any ) : Promise < CommitFact [ ] > {
const scan = brain . scanFacts ( )
expect ( scan ) . not . toBeNull ( )
const facts : CommitFact [ ] = [ ]
for await ( const batch of scan ! . batches ( ) ) facts . push ( . . . batch . facts )
return facts
}
describe ( 'fact log dual-write (memory adapter)' , ( ) = > {
let brain : any
beforeEach ( async ( ) = > {
process . env . BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
brain = new Brainy ( { requireSubtype : false , storage : { type : 'memory' } , silent : true , dimensions : 384 } )
await brain . init ( )
} )
afterEach ( async ( ) = > {
await brain . close ? . ( ) . catch ( ( ) = > { } )
} )
it ( 'every single-op write appends its after-image fact; a remove appends a tombstone' , async ( ) = > {
const id = await brain . add ( { data : 'first' , type : 'document' , metadata : { rev : 1 } } )
await brain . update ( { id , metadata : { rev : 2 } } )
await brain . remove ( id )
const facts = await allFacts ( brain )
// add + update + remove each committed a generation (the remove may span
// cascade ops but is ONE generation). Facts are monotonic.
const gens = facts . map ( ( f ) = > f . generation )
expect ( [ . . . gens ] . sort ( ( a , b ) = > a - b ) ) . toEqual ( gens )
expect ( facts . length ) . toBeGreaterThanOrEqual ( 3 )
// The add fact carries the after-image of the new entity.
const addFact = facts . find ( ( f ) = > f . ops . some ( ( op ) = > op . id === id && op . record !== null ) )
expect ( addFact ) . toBeDefined ( )
// The remove fact carries a body-less tombstone for the id.
const removeFact = facts [ facts . length - 1 ]
const tombstone = removeFact . ops . find ( ( op ) = > op . id === id )
expect ( tombstone ) . toBeDefined ( )
expect ( tombstone ! . record ) . toBeNull ( )
expect ( tombstone ! . kind ) . toBe ( 'noun' )
} )
it ( 'the update fact holds the NEW state (after-image, not before)' , async ( ) = > {
const id = await brain . add ( { data : 'versioned' , type : 'document' , metadata : { v : 'old' } } )
await brain . update ( { id , metadata : { v : 'new' } } )
const facts = await allFacts ( brain )
const updateFact = facts [ facts . length - 1 ]
const op = updateFact . ops . find ( ( o ) = > o . id === id ) !
expect ( op . record ) . not . toBeNull ( )
expect ( ( op . record ! . metadata as any ) . v ) . toBe ( 'new' )
} )
it ( 'a transact commits ONE fact carrying all its ops, with meta' , async ( ) = > {
const receipt = await brain . transact (
[
{ op : 'add' , type : 'document' , metadata : { part : 1 } , data : 'a' } ,
{ op : 'add' , type : 'document' , metadata : { part : 2 } , data : 'b' }
] ,
{ meta : { source : 'batch-import' } }
)
const facts = await allFacts ( brain )
const txFact = facts . find ( ( f ) = > f . generation === receipt . generation )
expect ( txFact ) . toBeDefined ( )
expect ( txFact ! . ops . filter ( ( op ) = > op . kind === 'noun' ) . length ) . toBeGreaterThanOrEqual ( 2 )
expect ( txFact ! . meta ) . toEqual ( { source : 'batch-import' } )
} )
it ( 'an aborted transact leaves NO fact (absent = never committed)' , async ( ) = > {
const id = await brain . add ( { data : 'cas target' , type : 'document' , metadata : { n : 1 } } )
const before = ( await allFacts ( brain ) ) . length
await expect (
brain . transact ( [ { op : 'update' , id , ifRev : 999 , metadata : { n : 2 } } ] )
) . rejects . toThrow ( )
const after = await allFacts ( brain )
expect ( after . length ) . toBe ( before )
} )
it ( 'fact generations line up with the transaction log' , async ( ) = > {
await brain . add ( { data : 'x' , type : 'document' , metadata : { } } )
await brain . add ( { data : 'y' , type : 'document' , metadata : { } } )
await brain . flush ( )
const facts = await allFacts ( brain )
const logGens = new Set ( ( await brain . transactionLog ( ) ) . map ( ( e : any ) = > e . generation ) )
for ( const f of facts ) {
expect ( logGens . has ( f . generation ) ) . toBe ( true )
}
} )
2026-07-15 12:36:27 -07:00
it ( 'the storage fact-scan capability serves a provider holding only `storage`' , async ( ) = > {
// An index provider receives `storage` — never the brain — and reaches the
// fact log through the host-wired capability (it must never construct its
// own fact-log reader: the log's open path is writer-side).
const id = await brain . add ( { data : 'via storage' , type : 'document' , metadata : { s : 1 } } )
await brain . remove ( id )
const storage = brain . storage
expect ( typeof storage . scanFacts ) . toBe ( 'function' )
expect ( storage . factLogHeadGeneration ( ) ) . toBe ( brain . scanFacts ( ) ! . headGeneration )
const viaStorage : CommitFact [ ] = [ ]
for await ( const b of storage . scanFacts ( ) ! . batches ( ) ) viaStorage . push ( . . . b . facts )
const viaBrain : CommitFact [ ] = [ ]
for await ( const b of brain . scanFacts ( ) ! . batches ( ) ) viaBrain . push ( . . . b . facts )
expect ( viaStorage . map ( ( f ) = > f . generation ) ) . toEqual ( viaBrain . map ( ( f ) = > f . generation ) )
expect ( storage . factSegmentPaths ( ) ) . toEqual ( brain . factSegmentPaths ( ) )
} )
feat: generation fact log — after-image commit records, dual-written at every commit point
Every committed generation now also appends a FACT — an after-image
commit record (what each touched entity/relationship became, or a
body-less tombstone for a removal) — to an append-only, crc32c-framed
segment log under _generations/facts/. The before-image history and the
canonical tree remain authoritative; the fact log gives consumers ONE
sequential, self-verifying stream (index heals, incremental replays)
in place of a per-entity directory walk.
- Wire format: positional msgpack facts [generation, timestamp, ops,
meta, blobHashes]; op = [kind u8, id bin16, record | nil tombstone];
32-byte segment header (magic, formatVersion, firstGeneration,
zeroed+verified reserved); length+crc32c frame per fact; zero-padded
segment names so lexicographic order == generation order; JSON
manifest with an atomic rename flip, manifest-first rotation.
- Commit protocol: facts append+fsync BEFORE the commit point inside
the existing durability window, so a crash can only leave the log
AHEAD of committed truth — open() truncates back (torn tails detected
by CRC). Absent generation = never committed; a scan can never see an
uncommitted fact. transact() facts are durable-on-return; single-op
facts ride the group-commit flush exactly like buffered history. A
fact-append failure fails the write, loudly — a silent gap would be a
lie a later replay discovers.
- New public surface: brain.scanFacts() (sequential batches with heal
telemetry: head/segments/approx up front, per-batch generation range
+ bytes + segment id, loud abort on gaps, summary cross-check) and
brain.factSegmentPaths() (immutable sealed segments for zero-copy
consumers; the mutable tail excluded). Exported types CommitFact,
FactOp, FactScanBatch, FactScanHandle.
- Storage: optional binary raw-byte primitives (appendRawBytes,
readRawBytes, writeRawBytes, rawByteSize) on StorageAdapter —
feature-detected; filesystem + memory adapters implement them; an
adapter without them hosts no fact log. Fact segments are byte-copied
(never hard-linked) into snapshots. The _generations/facts/ namespace
is registered as a protected family (rebuildable: false): no sweeper
or GC may delete under it.
- New crc32c (Castagnoli) utility with RFC known-answer tests.
2026-07-15 10:49:02 -07:00
it ( 'scan telemetry carries the frozen shape end-to-end' , async ( ) = > {
for ( let i = 0 ; i < 5 ; i ++ ) await brain . add ( { data : ` t ${ i } ` , type : 'document' , metadata : { i } } )
const scan = brain . scanFacts ( { batchSize : 2 } ) !
expect ( scan . headGeneration ) . toBeGreaterThanOrEqual ( 5 )
expect ( scan . approxFactCount ) . toBeGreaterThanOrEqual ( 5 )
let batches = 0
for await ( const b of scan . batches ( ) ) {
batches ++
expect ( b . factCount ) . toBe ( b . facts . length )
expect ( b . firstGeneration ) . toBe ( b . facts [ 0 ] . generation )
expect ( b . lastGeneration ) . toBe ( b . facts [ b . facts . length - 1 ] . generation )
expect ( b . byteSize ) . toBeGreaterThan ( 0 )
expect ( typeof b . segmentId ) . toBe ( 'string' )
}
expect ( batches ) . toBeGreaterThan ( 1 )
expect ( scan . summary ( ) . factsYielded ) . toBe ( scan . approxFactCount )
} )
} )
describe ( 'fact log dual-write (filesystem adapter — durability + protection)' , ( ) = > {
let dir : string
let brain : any
const open = async ( ) = > {
const b : any = new Brainy ( {
requireSubtype : false ,
storage : { type : 'filesystem' , path : dir } ,
silent : true ,
dimensions : 384
} )
await b . init ( )
return b
}
beforeEach ( async ( ) = > {
process . env . BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
dir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'brainy-factlog-' ) )
brain = await open ( )
} )
afterEach ( async ( ) = > {
await brain . close ? . ( ) . catch ( ( ) = > { } )
fs . rmSync ( dir , { recursive : true , force : true } )
} )
it ( 'facts survive close + reopen and appends continue monotonically' , async ( ) = > {
const id = await brain . add ( { data : 'persist me' , type : 'document' , metadata : { k : 1 } } )
await brain . remove ( id )
await brain . close ( )
brain = await open ( )
const facts = await allFacts ( brain )
expect ( facts . length ) . toBeGreaterThanOrEqual ( 2 )
const headBefore = facts [ facts . length - 1 ] . generation
await brain . add ( { data : 'after reopen' , type : 'document' , metadata : { k : 2 } } )
const facts2 = await allFacts ( brain )
expect ( facts2 [ facts2 . length - 1 ] . generation ) . toBeGreaterThan ( headBefore )
} )
it ( 'the fact segments exist on disk under _generations/facts/ with zero-padded names' , async ( ) = > {
await brain . add ( { data : 'on disk' , type : 'document' , metadata : { } } )
await brain . flush ( )
const factsDir = path . join ( dir , '_generations' , 'facts' )
const files = fs . readdirSync ( factsDir )
// The manifest rides the store's JSON object discipline (gzip on disk).
expect ( files . some ( ( f ) = > f . startsWith ( 'manifest.json' ) ) ) . toBe ( true )
const segs = files . filter ( ( f ) = > /^seg-\d{20}\.bfl$/ . test ( f ) )
expect ( segs . length ) . toBeGreaterThanOrEqual ( 1 )
} )
it ( 'the fact-log namespace is PROTECTED: a prefix-nuke is refused' , async ( ) = > {
await brain . add ( { data : 'protected' , type : 'document' , metadata : { } } )
await expect ( brain . storage . removeRawPrefix ( '_generations/facts' ) ) . rejects . toBeInstanceOf (
ProtectedArtifactError
)
// Per-generation history cleanup remains unaffected (no false intersect).
await expect ( brain . storage . removeRawPrefix ( '_generations/999999' ) ) . resolves . toBeUndefined ( )
} )
it ( 'transact facts are durable-on-return (no flush needed before reopen)' , async ( ) = > {
const receipt = await brain . transact ( [
{ op : 'add' , type : 'document' , metadata : { durable : true } , data : 'tx' }
] )
// Simulate an abrupt end: no flush(), no close() — reopen from disk.
brain = await open ( )
const facts = await allFacts ( brain )
expect ( facts . some ( ( f ) = > f . generation === receipt . generation ) ) . toBe ( true )
} )
} )