2026-06-30 13:37:23 -07:00
/ * *
* @module tests / unit / brainy / migration - deference
* @description rc . 8 no - freeze auto - upgrade hooks — the deference seam that lets a
* native provider run an ONLINE , background build - new → verify → swap index migration
* while brainy stays out of the way ( never a minutes - long blocking
* rebuild - on - open / first - query on a large - brain epoch - drift upgrade ) .
*
* Three hooks , locked with the cor team :
*
* - Hook 1 ( deference ) : an OPTIONAL sync ` isMigrating(): boolean ` on each index
* provider ( metadata / vector / graph ) . While it returns true , brainy SKIPS its
* own rebuild of that index — both in { @link Brainy . rebuildIndexesIfNeeded }
* ( even under epoch - drift or ` size()===0 ` ) and on the large - path first - query
* lazy force - rebuild . A NON - migrating sibling still rebuilds when it needs to .
* - Hook 2 : the public ` brain.stampBrainFormat() ` the provider calls once its
* background migration has verified - and - swapped , authoring the shared
* ` _system/brain-format.json ` marker .
* - Hook 3 : the marker module is re - exported at ` @soulcraft/brainy/brain-format `
* so cor reads the SAME ` EXPECTED_INDEX_EPOCH ` / ` CURRENT_DATA_FORMAT ` constants
* ( single source of truth , no duplicated value ) .
*
* These paths are never exercised by brainy CI ( cor registers no provider there ) ,
* so the deference gates and the public stamp are pinned here with a white - box
* test - double provider — the same pattern as the cold - graph / handshake tests .
* /
import { describe , it , expect , afterEach , vi } from 'vitest'
import { Brainy } from '../../../src/index.js'
import { NounType } from '../../../src/types/graphTypes.js'
import { createTestConfig } from '../../helpers/test-factory.js'
import { BaseStorage } from '../../../src/storage/baseStorage.js'
import {
BRAIN_FORMAT_PATH ,
CURRENT_DATA_FORMAT ,
EXPECTED_INDEX_EPOCH
} from '../../../src/storage/brainFormat.js'
const CURRENT_MARKER = { dataFormat : CURRENT_DATA_FORMAT , indexEpoch : EXPECTED_INDEX_EPOCH }
/** The white-box surface this suite drives on a live brain instance. */
interface BrainInternals {
index : { size ( ) : number ; rebuild ( . . . a : unknown [ ] ) : Promise < unknown > }
metadataIndex : { rebuild ( . . . a : unknown [ ] ) : Promise < unknown > }
graphIndex : { size ( ) : number ; rebuild ( . . . a : unknown [ ] ) : Promise < unknown > }
_indexEpochStale : boolean
lazyRebuildCompleted : boolean
rebuildIndexesIfNeeded ( force? : boolean ) : Promise < void >
ensureIndexesLoaded ( ) : Promise < void >
storage : { readRawObject ( p : string ) : Promise < unknown > }
}
const brains : Brainy [ ] = [ ]
/** Open (and track) a warmed memory brain holding `count` document entities. */
async function makeWarmBrain ( count = 2 , extraConfig : Record < string , unknown > = { } ) : Promise < Brainy > {
const brain = new Brainy ( createTestConfig ( extraConfig ) )
await brain . init ( )
brains . push ( brain )
for ( let i = 0 ; i < count ; i ++ ) {
await brain . add ( { data : ` doc- ${ i } ` , type : NounType . Document , metadata : { k : i } } )
}
return brain
}
/** Cast a brain to its white-box internals. */
function internalsOf ( brain : Brainy ) : BrainInternals {
return brain as unknown as BrainInternals
}
/** Force `provider.isMigrating()` to a fixed value (the native provider's deference flag). */
function setMigrating ( provider : object , value : boolean ) : void {
; ( provider as { isMigrating ? : ( ) = > boolean } ) . isMigrating = ( ) = > value
}
afterEach ( async ( ) = > {
for ( const brain of brains . splice ( 0 ) ) {
try {
await brain . close ( )
} catch {
// already closed by the test
}
}
vi . restoreAllMocks ( )
} )
describe ( 'rc.8 no-freeze migration deference (isMigrating / stampBrainFormat / brain-format export)' , ( ) = > {
// --- Hook 1: per-index deference in rebuildIndexesIfNeeded ----------------
it ( 'metadata provider isMigrating(): its rebuild is skipped under epoch-drift; vector + graph siblings still rebuild' , async ( ) = > {
const brain = await makeWarmBrain ( )
const internals = internalsOf ( brain )
const miSpy = vi . spyOn ( internals . metadataIndex , 'rebuild' ) . mockResolvedValue ( undefined )
const idxSpy = vi . spyOn ( internals . index , 'rebuild' ) . mockResolvedValue ( undefined )
const giSpy = vi . spyOn ( internals . graphIndex , 'rebuild' ) . mockResolvedValue ( undefined )
setMigrating ( internals . metadataIndex , true )
// Epoch-drift would normally force ALL three to rebuild past the warm fast path.
internals . _indexEpochStale = true
await internals . rebuildIndexesIfNeeded ( )
// The migrating provider owns its index — brainy does NOT rebuild it.
expect ( miSpy ) . toHaveBeenCalledTimes ( 0 )
// Non-migrating siblings still rebuild under the drift.
expect ( idxSpy ) . toHaveBeenCalledTimes ( 1 )
expect ( giSpy ) . toHaveBeenCalledTimes ( 1 )
// The marker is NOT advanced while a migration is in flight — cor stamps it
// when its build-new→verify→swap completes, so the stale flag stays set.
expect ( internals . _indexEpochStale ) . toBe ( true )
} )
it ( 'vector provider isMigrating(): its rebuild is skipped under epoch-drift; metadata + graph siblings still rebuild' , async ( ) = > {
const brain = await makeWarmBrain ( )
const internals = internalsOf ( brain )
const miSpy = vi . spyOn ( internals . metadataIndex , 'rebuild' ) . mockResolvedValue ( undefined )
const idxSpy = vi . spyOn ( internals . index , 'rebuild' ) . mockResolvedValue ( undefined )
const giSpy = vi . spyOn ( internals . graphIndex , 'rebuild' ) . mockResolvedValue ( undefined )
setMigrating ( internals . index , true )
internals . _indexEpochStale = true
await internals . rebuildIndexesIfNeeded ( )
expect ( idxSpy ) . toHaveBeenCalledTimes ( 0 )
expect ( miSpy ) . toHaveBeenCalledTimes ( 1 )
expect ( giSpy ) . toHaveBeenCalledTimes ( 1 )
expect ( internals . _indexEpochStale ) . toBe ( true )
} )
it ( 'graph provider isMigrating(): its rebuild is skipped under epoch-drift; metadata + vector siblings still rebuild' , async ( ) = > {
const brain = await makeWarmBrain ( )
const internals = internalsOf ( brain )
const miSpy = vi . spyOn ( internals . metadataIndex , 'rebuild' ) . mockResolvedValue ( undefined )
const idxSpy = vi . spyOn ( internals . index , 'rebuild' ) . mockResolvedValue ( undefined )
const giSpy = vi . spyOn ( internals . graphIndex , 'rebuild' ) . mockResolvedValue ( undefined )
setMigrating ( internals . graphIndex , true )
internals . _indexEpochStale = true
await internals . rebuildIndexesIfNeeded ( )
expect ( giSpy ) . toHaveBeenCalledTimes ( 0 )
expect ( miSpy ) . toHaveBeenCalledTimes ( 1 )
expect ( idxSpy ) . toHaveBeenCalledTimes ( 1 )
expect ( internals . _indexEpochStale ) . toBe ( true )
} )
fix: cold-open no longer re-derives durable indexes — complete the readiness contract for all three providers
A production deployment measured ~48 seconds on EVERY reopen of an
11k-entity brain. Root cause: brainy's rebuild gate decided from in-memory
size()/count, which read 0 for a durable-but-not-resident index, so it
re-read every entity file to rebuild from scratch. At GA we gave only the
GRAPH provider a readiness contract (init() eager cold-load + isReady()
honest signal) so it would never eat that spurious rebuild; the vector and
metadata providers never got it, and brainy never even eager-inited the
vector provider.
Complete the contract symmetrically:
- plugin.ts: VectorIndexProvider gains optional init()+isReady();
MetadataIndexProvider gains isReady() — mirroring GraphIndexProvider.
Additive and optional; a provider that exposes nothing keeps today's
behavior.
- brainy.ts: eager-init every provider that exposes init() (after metadata
init() so the id-mapper is hydrated first), then decide per leg in
precedence order — migrating (skip) -> epoch drift (rebuild) -> isReady()
-> a per-leg empty fallback. The old instant fast-path keyed off
this.index.size()>0, a dishonest proxy that skipped the metadata/graph
checks whenever the vector was warm and never fired on a real cold process
anyway; removed.
The per-leg fallbacks differ because "empty" means different things: the JS
vector's rebuild() IS its load, so size()===0 correctly triggers it; the
id-mapper backs metadata, so totalEntries===0 (past the empty-store return)
is a real load failure; but entities do not imply edges, so a graph
size()===0 is a valid empty state, not a load failure.
- The JS graph now COLD-LOADS its durable LSM instead of re-deriving from a
full canonical verb scan on every boot (baseStorage._initializeGraphIndex
loads the persisted SSTables via a new GraphAdjacencyIndex.init(); it
self-heals from canonical only when the durable state is genuinely missing).
This removes an O(E)-per-open cost every filesystem consumer paid.
- LSMTree.loadManifest loads its SSTables BEFORE publishing the relationship
count, and resets to an honest-empty state on load failure — a tree can no
longer claim persisted relationships while holding none (the silent-empty
cold-load class the query-time guards exist to prevent).
Verified end-to-end against a built brain: a warm reopen (with edges and
edgeless) reloads only the JS vector; the graph and metadata cold-load with
no rebuild, and queries return correct results. New tests in
cold-open-rebuild-gate.test.ts pin the contract (isReady() defers, self-heal
still fires); migration-deference updated to drive size-based deference
through the vector, the leg where empty->rebuild remains correct.
Pairs with the native provider's isReady()/init() implementation — brainy's
gate defers only to a signal the provider exposes.
2026-07-07 10:39:00 -07:00
it ( 'a migrating provider is skipped even though its empty-signal would trigger a rebuild; clearing the flag lets the empty leg rebuild' , async ( ) = > {
2026-06-30 13:37:23 -07:00
const brain = await makeWarmBrain ( )
const internals = internalsOf ( brain )
const miSpy = vi . spyOn ( internals . metadataIndex , 'rebuild' ) . mockResolvedValue ( undefined )
const idxSpy = vi . spyOn ( internals . index , 'rebuild' ) . mockResolvedValue ( undefined )
const giSpy = vi . spyOn ( internals . graphIndex , 'rebuild' ) . mockResolvedValue ( undefined )
fix: cold-open no longer re-derives durable indexes — complete the readiness contract for all three providers
A production deployment measured ~48 seconds on EVERY reopen of an
11k-entity brain. Root cause: brainy's rebuild gate decided from in-memory
size()/count, which read 0 for a durable-but-not-resident index, so it
re-read every entity file to rebuild from scratch. At GA we gave only the
GRAPH provider a readiness contract (init() eager cold-load + isReady()
honest signal) so it would never eat that spurious rebuild; the vector and
metadata providers never got it, and brainy never even eager-inited the
vector provider.
Complete the contract symmetrically:
- plugin.ts: VectorIndexProvider gains optional init()+isReady();
MetadataIndexProvider gains isReady() — mirroring GraphIndexProvider.
Additive and optional; a provider that exposes nothing keeps today's
behavior.
- brainy.ts: eager-init every provider that exposes init() (after metadata
init() so the id-mapper is hydrated first), then decide per leg in
precedence order — migrating (skip) -> epoch drift (rebuild) -> isReady()
-> a per-leg empty fallback. The old instant fast-path keyed off
this.index.size()>0, a dishonest proxy that skipped the metadata/graph
checks whenever the vector was warm and never fired on a real cold process
anyway; removed.
The per-leg fallbacks differ because "empty" means different things: the JS
vector's rebuild() IS its load, so size()===0 correctly triggers it; the
id-mapper backs metadata, so totalEntries===0 (past the empty-store return)
is a real load failure; but entities do not imply edges, so a graph
size()===0 is a valid empty state, not a load failure.
- The JS graph now COLD-LOADS its durable LSM instead of re-deriving from a
full canonical verb scan on every boot (baseStorage._initializeGraphIndex
loads the persisted SSTables via a new GraphAdjacencyIndex.init(); it
self-heals from canonical only when the durable state is genuinely missing).
This removes an O(E)-per-open cost every filesystem consumer paid.
- LSMTree.loadManifest loads its SSTables BEFORE publishing the relationship
count, and resets to an honest-empty state on load failure — a tree can no
longer claim persisted relationships while holding none (the silent-empty
cold-load class the query-time guards exist to prevent).
Verified end-to-end against a built brain: a warm reopen (with edges and
edgeless) reloads only the JS vector; the graph and metadata cold-load with
no rebuild, and queries return correct results. New tests in
cold-open-rebuild-gate.test.ts pin the contract (isReady() defers, self-heal
still fires); migration-deference updated to drive size-based deference
through the vector, the leg where empty->rebuild remains correct.
Pairs with the native provider's isReady()/init() implementation — brainy's
gate defers only to a signal the provider exposes.
2026-07-07 10:39:00 -07:00
// No epoch drift: the only rebuild trigger is a leg's own empty signal. The
// JS vector's rebuild() IS its load path, so size()===0 is its trigger —
// the leg where "empty → rebuild" is architecturally correct — so we drive
// deference through it. (The JS graph cold-loads before this gate, so its
// size()===0 is a valid empty state, not a rebuild trigger; graph deference
// is covered by the epoch-drift case above.)
2026-06-30 13:37:23 -07:00
internals . _indexEpochStale = false
vi . spyOn ( internals . index , 'size' ) . mockReturnValue ( 0 )
fix: cold-open no longer re-derives durable indexes — complete the readiness contract for all three providers
A production deployment measured ~48 seconds on EVERY reopen of an
11k-entity brain. Root cause: brainy's rebuild gate decided from in-memory
size()/count, which read 0 for a durable-but-not-resident index, so it
re-read every entity file to rebuild from scratch. At GA we gave only the
GRAPH provider a readiness contract (init() eager cold-load + isReady()
honest signal) so it would never eat that spurious rebuild; the vector and
metadata providers never got it, and brainy never even eager-inited the
vector provider.
Complete the contract symmetrically:
- plugin.ts: VectorIndexProvider gains optional init()+isReady();
MetadataIndexProvider gains isReady() — mirroring GraphIndexProvider.
Additive and optional; a provider that exposes nothing keeps today's
behavior.
- brainy.ts: eager-init every provider that exposes init() (after metadata
init() so the id-mapper is hydrated first), then decide per leg in
precedence order — migrating (skip) -> epoch drift (rebuild) -> isReady()
-> a per-leg empty fallback. The old instant fast-path keyed off
this.index.size()>0, a dishonest proxy that skipped the metadata/graph
checks whenever the vector was warm and never fired on a real cold process
anyway; removed.
The per-leg fallbacks differ because "empty" means different things: the JS
vector's rebuild() IS its load, so size()===0 correctly triggers it; the
id-mapper backs metadata, so totalEntries===0 (past the empty-store return)
is a real load failure; but entities do not imply edges, so a graph
size()===0 is a valid empty state, not a load failure.
- The JS graph now COLD-LOADS its durable LSM instead of re-deriving from a
full canonical verb scan on every boot (baseStorage._initializeGraphIndex
loads the persisted SSTables via a new GraphAdjacencyIndex.init(); it
self-heals from canonical only when the durable state is genuinely missing).
This removes an O(E)-per-open cost every filesystem consumer paid.
- LSMTree.loadManifest loads its SSTables BEFORE publishing the relationship
count, and resets to an honest-empty state on load failure — a tree can no
longer claim persisted relationships while holding none (the silent-empty
cold-load class the query-time guards exist to prevent).
Verified end-to-end against a built brain: a warm reopen (with edges and
edgeless) reloads only the JS vector; the graph and metadata cold-load with
no rebuild, and queries return correct results. New tests in
cold-open-rebuild-gate.test.ts pin the contract (isReady() defers, self-heal
still fires); migration-deference updated to drive size-based deference
through the vector, the leg where empty->rebuild remains correct.
Pairs with the native provider's isReady()/init() implementation — brainy's
gate defers only to a signal the provider exposes.
2026-07-07 10:39:00 -07:00
// Migrating: the provider's background swap owns the index, so the
// size()===0 load trigger is suppressed.
setMigrating ( internals . index , true )
2026-06-30 13:37:23 -07:00
await internals . rebuildIndexesIfNeeded ( )
expect ( idxSpy ) . toHaveBeenCalledTimes ( 0 )
fix: cold-open no longer re-derives durable indexes — complete the readiness contract for all three providers
A production deployment measured ~48 seconds on EVERY reopen of an
11k-entity brain. Root cause: brainy's rebuild gate decided from in-memory
size()/count, which read 0 for a durable-but-not-resident index, so it
re-read every entity file to rebuild from scratch. At GA we gave only the
GRAPH provider a readiness contract (init() eager cold-load + isReady()
honest signal) so it would never eat that spurious rebuild; the vector and
metadata providers never got it, and brainy never even eager-inited the
vector provider.
Complete the contract symmetrically:
- plugin.ts: VectorIndexProvider gains optional init()+isReady();
MetadataIndexProvider gains isReady() — mirroring GraphIndexProvider.
Additive and optional; a provider that exposes nothing keeps today's
behavior.
- brainy.ts: eager-init every provider that exposes init() (after metadata
init() so the id-mapper is hydrated first), then decide per leg in
precedence order — migrating (skip) -> epoch drift (rebuild) -> isReady()
-> a per-leg empty fallback. The old instant fast-path keyed off
this.index.size()>0, a dishonest proxy that skipped the metadata/graph
checks whenever the vector was warm and never fired on a real cold process
anyway; removed.
The per-leg fallbacks differ because "empty" means different things: the JS
vector's rebuild() IS its load, so size()===0 correctly triggers it; the
id-mapper backs metadata, so totalEntries===0 (past the empty-store return)
is a real load failure; but entities do not imply edges, so a graph
size()===0 is a valid empty state, not a load failure.
- The JS graph now COLD-LOADS its durable LSM instead of re-deriving from a
full canonical verb scan on every boot (baseStorage._initializeGraphIndex
loads the persisted SSTables via a new GraphAdjacencyIndex.init(); it
self-heals from canonical only when the durable state is genuinely missing).
This removes an O(E)-per-open cost every filesystem consumer paid.
- LSMTree.loadManifest loads its SSTables BEFORE publishing the relationship
count, and resets to an honest-empty state on load failure — a tree can no
longer claim persisted relationships while holding none (the silent-empty
cold-load class the query-time guards exist to prevent).
Verified end-to-end against a built brain: a warm reopen (with edges and
edgeless) reloads only the JS vector; the graph and metadata cold-load with
no rebuild, and queries return correct results. New tests in
cold-open-rebuild-gate.test.ts pin the contract (isReady() defers, self-heal
still fires); migration-deference updated to drive size-based deference
through the vector, the leg where empty->rebuild remains correct.
Pairs with the native provider's isReady()/init() implementation — brainy's
gate defers only to a signal the provider exposes.
2026-07-07 10:39:00 -07:00
// Metadata has entries; the graph has no edges; neither drifted → no rebuild.
2026-06-30 13:37:23 -07:00
expect ( miSpy ) . toHaveBeenCalledTimes ( 0 )
fix: cold-open no longer re-derives durable indexes — complete the readiness contract for all three providers
A production deployment measured ~48 seconds on EVERY reopen of an
11k-entity brain. Root cause: brainy's rebuild gate decided from in-memory
size()/count, which read 0 for a durable-but-not-resident index, so it
re-read every entity file to rebuild from scratch. At GA we gave only the
GRAPH provider a readiness contract (init() eager cold-load + isReady()
honest signal) so it would never eat that spurious rebuild; the vector and
metadata providers never got it, and brainy never even eager-inited the
vector provider.
Complete the contract symmetrically:
- plugin.ts: VectorIndexProvider gains optional init()+isReady();
MetadataIndexProvider gains isReady() — mirroring GraphIndexProvider.
Additive and optional; a provider that exposes nothing keeps today's
behavior.
- brainy.ts: eager-init every provider that exposes init() (after metadata
init() so the id-mapper is hydrated first), then decide per leg in
precedence order — migrating (skip) -> epoch drift (rebuild) -> isReady()
-> a per-leg empty fallback. The old instant fast-path keyed off
this.index.size()>0, a dishonest proxy that skipped the metadata/graph
checks whenever the vector was warm and never fired on a real cold process
anyway; removed.
The per-leg fallbacks differ because "empty" means different things: the JS
vector's rebuild() IS its load, so size()===0 correctly triggers it; the
id-mapper backs metadata, so totalEntries===0 (past the empty-store return)
is a real load failure; but entities do not imply edges, so a graph
size()===0 is a valid empty state, not a load failure.
- The JS graph now COLD-LOADS its durable LSM instead of re-deriving from a
full canonical verb scan on every boot (baseStorage._initializeGraphIndex
loads the persisted SSTables via a new GraphAdjacencyIndex.init(); it
self-heals from canonical only when the durable state is genuinely missing).
This removes an O(E)-per-open cost every filesystem consumer paid.
- LSMTree.loadManifest loads its SSTables BEFORE publishing the relationship
count, and resets to an honest-empty state on load failure — a tree can no
longer claim persisted relationships while holding none (the silent-empty
cold-load class the query-time guards exist to prevent).
Verified end-to-end against a built brain: a warm reopen (with edges and
edgeless) reloads only the JS vector; the graph and metadata cold-load with
no rebuild, and queries return correct results. New tests in
cold-open-rebuild-gate.test.ts pin the contract (isReady() defers, self-heal
still fires); migration-deference updated to drive size-based deference
through the vector, the leg where empty->rebuild remains correct.
Pairs with the native provider's isReady()/init() implementation — brainy's
gate defers only to a signal the provider exposes.
2026-07-07 10:39:00 -07:00
expect ( giSpy ) . toHaveBeenCalledTimes ( 0 )
// Clearing the flag: the same empty, non-migrating vector now rebuilds — and
// this second call re-evaluating at all confirms the gate is not latched.
setMigrating ( internals . index , false )
await internals . rebuildIndexesIfNeeded ( )
expect ( idxSpy ) . toHaveBeenCalledTimes ( 1 )
2026-06-30 13:37:23 -07:00
} )
// --- Hook 1: large-path first-query lazy force-rebuild deference ----------
it ( 'lazy first-query force-rebuild is SKIPPED when the vector provider isMigrating()' , async ( ) = > {
// disableAutoRebuild routes first queries through ensureIndexesLoaded() (the
// large-brain lazy path that would otherwise force a blocking rebuild).
const brain = await makeWarmBrain ( 2 , { disableAutoRebuild : true } )
const internals = internalsOf ( brain )
const rebuildSpy = vi . spyOn ( internals , 'rebuildIndexesIfNeeded' ) . mockResolvedValue ( undefined )
// Simulate a cold/empty live vector index (cor is mid-swap, serving canonical).
vi . spyOn ( internals . index , 'size' ) . mockReturnValue ( 0 )
internals . lazyRebuildCompleted = false
setMigrating ( internals . index , true )
await internals . ensureIndexesLoaded ( )
// A query during cor's background swap must not trigger brainy's blocking rebuild.
expect ( rebuildSpy ) . toHaveBeenCalledTimes ( 0 )
} )
it ( 'lazy first-query force-rebuild STILL fires when the vector provider is not migrating (control)' , async ( ) = > {
const brain = await makeWarmBrain ( 2 , { disableAutoRebuild : true } )
const internals = internalsOf ( brain )
const rebuildSpy = vi . spyOn ( internals , 'rebuildIndexesIfNeeded' ) . mockResolvedValue ( undefined )
vi . spyOn ( internals . index , 'size' ) . mockReturnValue ( 0 )
internals . lazyRebuildCompleted = false
// No isMigrating → not deferring.
await internals . ensureIndexesLoaded ( )
// Without deference, the cold empty index drives the lazy force-rebuild.
expect ( rebuildSpy ) . toHaveBeenCalledTimes ( 1 )
expect ( rebuildSpy ) . toHaveBeenCalledWith ( true )
} )
// --- Hook 2: public stampBrainFormat() -----------------------------------
it ( 'brain.stampBrainFormat() writes _system/brain-format.json with the current {dataFormat, indexEpoch}' , async ( ) = > {
const brain = new Brainy ( createTestConfig ( ) )
await brain . init ( )
brains . push ( brain )
const internals = internalsOf ( brain )
// Spy AFTER init so only the stamp call's write is observed (init already
// stamped the fresh brain).
const writeSpy = vi . spyOn ( BaseStorage . prototype , 'writeRawObject' )
await brain . stampBrainFormat ( )
const stampWrites = writeSpy . mock . calls . filter ( ( [ p ] ) = > p === BRAIN_FORMAT_PATH )
expect ( stampWrites . length ) . toBe ( 1 )
expect ( stampWrites [ 0 ] [ 1 ] ) . toEqual ( CURRENT_MARKER )
// And the marker is durable on disk at the current epoch.
const onDisk = await internals . storage . readRawObject ( BRAIN_FORMAT_PATH )
expect ( onDisk ) . toEqual ( CURRENT_MARKER )
} )
// --- Hook 3: marker module export ----------------------------------------
it ( 'the brain-format marker module exports the compiled epoch + data-format constants' , ( ) = > {
// cor imports these from '@soulcraft/brainy/brain-format' (Hook 3) so both
// sides share ONE source of truth — no duplicated constant to drift.
expect ( EXPECTED_INDEX_EPOCH ) . toBe ( 1 )
expect ( CURRENT_DATA_FORMAT ) . toBe ( '8.0' )
} )
} )