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'
feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
The read gate stops consulting the unnamed isReady() boolean: every provider
may expose healthReport() (sync, O(1), composed from exact ledgers —
HealthReport with a monotonic generation, per-invariant source
ledger|deep|unledgered, missing {count, sample}), and one readiness
authority (assessProviderHealth) derives the verdict. Unledgered families
are UNKNOWN — never healthy, never broken; a report that throws is a loud
not-ready, never a shrug. Reads at the four index choke points refuse with
the typed NotReady errors, narrated once per (provider, generation) — a
read NEVER starts a store walk:
- the first-read lazy build retires (open builds instead, regardless of
size — the ≥10k deferral and the "lazy loading on first query" branch go;
disableAutoRebuild is re-meant honestly in its docs);
- the verify*Live read-path rebuild triggers retire (refuse-or-serve);
- the read-time consistency probe that could launch a dark rebuild from an
ordinary find() retires;
- repairIndex({ rebuild: ['metadata'|'graph'|'vector'] | 'all' }) is the
one explicit door: rebuilds the named leg unconditionally and reports
rebuilt per family; bare repairIndex() stays report-driven.
test(lifecycle): the biography lane — a store's whole life, refereed
tests/lifecycle/: an independent shadow model referees every read after
every chapter (founding, a working day, clean restart, crash, repair,
second life). Chapters 1-3 green. Chapters 4-6 assert the true contract and
are marked .fails as a release-blocking finding (the kill-matrix
convention): after a crash + adopt reopen the metadata index computes its
'catchup' watermark verdict and nothing consumes it — find() serves the
pre-crash index while canonical and counts recover. The catchup wiring is
the cure; a passing .fails will force the marker's removal. The lane runs
in the integration gate (config + coverage guard).
2026-08-24 12:45:51 -07:00
import { Brainy , VectorIndexNotReadyError } from '../../../src/index.js'
2026-06-30 13:37:23 -07:00
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
rebuildIndexesIfNeeded ( force? : boolean ) : Promise < void >
feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
The read gate stops consulting the unnamed isReady() boolean: every provider
may expose healthReport() (sync, O(1), composed from exact ledgers —
HealthReport with a monotonic generation, per-invariant source
ledger|deep|unledgered, missing {count, sample}), and one readiness
authority (assessProviderHealth) derives the verdict. Unledgered families
are UNKNOWN — never healthy, never broken; a report that throws is a loud
not-ready, never a shrug. Reads at the four index choke points refuse with
the typed NotReady errors, narrated once per (provider, generation) — a
read NEVER starts a store walk:
- the first-read lazy build retires (open builds instead, regardless of
size — the ≥10k deferral and the "lazy loading on first query" branch go;
disableAutoRebuild is re-meant honestly in its docs);
- the verify*Live read-path rebuild triggers retire (refuse-or-serve);
- the read-time consistency probe that could launch a dark rebuild from an
ordinary find() retires;
- repairIndex({ rebuild: ['metadata'|'graph'|'vector'] | 'all' }) is the
one explicit door: rebuilds the named leg unconditionally and reports
rebuilt per family; bare repairIndex() stays report-driven.
test(lifecycle): the biography lane — a store's whole life, refereed
tests/lifecycle/: an independent shadow model referees every read after
every chapter (founding, a working day, clean restart, crash, repair,
second life). Chapters 1-3 green. Chapters 4-6 assert the true contract and
are marked .fails as a release-blocking finding (the kill-matrix
convention): after a crash + adopt reopen the metadata index computes its
'catchup' watermark verdict and nothing consumes it — find() serves the
pre-crash index while canonical and counts recover. The catchup wiring is
the cure; a passing .fails will force the marker's removal. The lane runs
in the integration gate (config + coverage guard).
2026-08-24 12:45:51 -07:00
ensureIndexesLoaded ( ) : void
2026-06-30 13:37:23 -07:00
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
} )
feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
The read gate stops consulting the unnamed isReady() boolean: every provider
may expose healthReport() (sync, O(1), composed from exact ledgers —
HealthReport with a monotonic generation, per-invariant source
ledger|deep|unledgered, missing {count, sample}), and one readiness
authority (assessProviderHealth) derives the verdict. Unledgered families
are UNKNOWN — never healthy, never broken; a report that throws is a loud
not-ready, never a shrug. Reads at the four index choke points refuse with
the typed NotReady errors, narrated once per (provider, generation) — a
read NEVER starts a store walk:
- the first-read lazy build retires (open builds instead, regardless of
size — the ≥10k deferral and the "lazy loading on first query" branch go;
disableAutoRebuild is re-meant honestly in its docs);
- the verify*Live read-path rebuild triggers retire (refuse-or-serve);
- the read-time consistency probe that could launch a dark rebuild from an
ordinary find() retires;
- repairIndex({ rebuild: ['metadata'|'graph'|'vector'] | 'all' }) is the
one explicit door: rebuilds the named leg unconditionally and reports
rebuilt per family; bare repairIndex() stays report-driven.
test(lifecycle): the biography lane — a store's whole life, refereed
tests/lifecycle/: an independent shadow model referees every read after
every chapter (founding, a working day, clean restart, crash, repair,
second life). Chapters 1-3 green. Chapters 4-6 assert the true contract and
are marked .fails as a release-blocking finding (the kill-matrix
convention): after a crash + adopt reopen the metadata index computes its
'catchup' watermark verdict and nothing consumes it — find() serves the
pre-crash index while canonical and counts recover. The catchup wiring is
the cure; a passing .fails will force the marker's removal. The lane runs
in the integration gate (config + coverage guard).
2026-08-24 12:45:51 -07:00
// --- Hook 1: read-gate deference (RE-POINTED — the health-gate law retired
// the first-query lazy force-rebuild entirely: ensureIndexesLoaded() is now
// a pure CHECK that never calls rebuildIndexesIfNeeded, migrating or not.
// What survives from the original law is the DEFERENCE itself: a migrating
// provider's report is never judged by the gate — it neither throws nor
// rebuilds — while the exact same not-ready report on a NON-migrating
// provider throws the typed error instead of ever rebuilding.) ------------
2026-06-30 13:37:23 -07:00
feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
The read gate stops consulting the unnamed isReady() boolean: every provider
may expose healthReport() (sync, O(1), composed from exact ledgers —
HealthReport with a monotonic generation, per-invariant source
ledger|deep|unledgered, missing {count, sample}), and one readiness
authority (assessProviderHealth) derives the verdict. Unledgered families
are UNKNOWN — never healthy, never broken; a report that throws is a loud
not-ready, never a shrug. Reads at the four index choke points refuse with
the typed NotReady errors, narrated once per (provider, generation) — a
read NEVER starts a store walk:
- the first-read lazy build retires (open builds instead, regardless of
size — the ≥10k deferral and the "lazy loading on first query" branch go;
disableAutoRebuild is re-meant honestly in its docs);
- the verify*Live read-path rebuild triggers retire (refuse-or-serve);
- the read-time consistency probe that could launch a dark rebuild from an
ordinary find() retires;
- repairIndex({ rebuild: ['metadata'|'graph'|'vector'] | 'all' }) is the
one explicit door: rebuilds the named leg unconditionally and reports
rebuilt per family; bare repairIndex() stays report-driven.
test(lifecycle): the biography lane — a store's whole life, refereed
tests/lifecycle/: an independent shadow model referees every read after
every chapter (founding, a working day, clean restart, crash, repair,
second life). Chapters 1-3 green. Chapters 4-6 assert the true contract and
are marked .fails as a release-blocking finding (the kill-matrix
convention): after a crash + adopt reopen the metadata index computes its
'catchup' watermark verdict and nothing consumes it — find() serves the
pre-crash index while canonical and counts recover. The catchup wiring is
the cure; a passing .fails will force the marker's removal. The lane runs
in the integration gate (config + coverage guard).
2026-08-24 12:45:51 -07:00
it ( 'the read gate defers to a migrating vector provider — a not-ready report neither throws nor rebuilds' , async ( ) = > {
2026-06-30 13:37:23 -07:00
const brain = await makeWarmBrain ( 2 , { disableAutoRebuild : true } )
const internals = internalsOf ( brain )
const rebuildSpy = vi . spyOn ( internals , 'rebuildIndexesIfNeeded' ) . mockResolvedValue ( undefined )
feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
The read gate stops consulting the unnamed isReady() boolean: every provider
may expose healthReport() (sync, O(1), composed from exact ledgers —
HealthReport with a monotonic generation, per-invariant source
ledger|deep|unledgered, missing {count, sample}), and one readiness
authority (assessProviderHealth) derives the verdict. Unledgered families
are UNKNOWN — never healthy, never broken; a report that throws is a loud
not-ready, never a shrug. Reads at the four index choke points refuse with
the typed NotReady errors, narrated once per (provider, generation) — a
read NEVER starts a store walk:
- the first-read lazy build retires (open builds instead, regardless of
size — the ≥10k deferral and the "lazy loading on first query" branch go;
disableAutoRebuild is re-meant honestly in its docs);
- the verify*Live read-path rebuild triggers retire (refuse-or-serve);
- the read-time consistency probe that could launch a dark rebuild from an
ordinary find() retires;
- repairIndex({ rebuild: ['metadata'|'graph'|'vector'] | 'all' }) is the
one explicit door: rebuilds the named leg unconditionally and reports
rebuilt per family; bare repairIndex() stays report-driven.
test(lifecycle): the biography lane — a store's whole life, refereed
tests/lifecycle/: an independent shadow model referees every read after
every chapter (founding, a working day, clean restart, crash, repair,
second life). Chapters 1-3 green. Chapters 4-6 assert the true contract and
are marked .fails as a release-blocking finding (the kill-matrix
convention): after a crash + adopt reopen the metadata index computes its
'catchup' watermark verdict and nothing consumes it — find() serves the
pre-crash index while canonical and counts recover. The catchup wiring is
the cure; a passing .fails will force the marker's removal. The lane runs
in the integration gate (config + coverage guard).
2026-08-24 12:45:51 -07:00
// Simulate a not-ready live vector index (cor is mid-swap, serving canonical).
; ( internals . index as unknown as { isReady ? : ( ) = > boolean } ) . isReady = ( ) = > false
2026-06-30 13:37:23 -07:00
setMigrating ( internals . index , true )
feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
The read gate stops consulting the unnamed isReady() boolean: every provider
may expose healthReport() (sync, O(1), composed from exact ledgers —
HealthReport with a monotonic generation, per-invariant source
ledger|deep|unledgered, missing {count, sample}), and one readiness
authority (assessProviderHealth) derives the verdict. Unledgered families
are UNKNOWN — never healthy, never broken; a report that throws is a loud
not-ready, never a shrug. Reads at the four index choke points refuse with
the typed NotReady errors, narrated once per (provider, generation) — a
read NEVER starts a store walk:
- the first-read lazy build retires (open builds instead, regardless of
size — the ≥10k deferral and the "lazy loading on first query" branch go;
disableAutoRebuild is re-meant honestly in its docs);
- the verify*Live read-path rebuild triggers retire (refuse-or-serve);
- the read-time consistency probe that could launch a dark rebuild from an
ordinary find() retires;
- repairIndex({ rebuild: ['metadata'|'graph'|'vector'] | 'all' }) is the
one explicit door: rebuilds the named leg unconditionally and reports
rebuilt per family; bare repairIndex() stays report-driven.
test(lifecycle): the biography lane — a store's whole life, refereed
tests/lifecycle/: an independent shadow model referees every read after
every chapter (founding, a working day, clean restart, crash, repair,
second life). Chapters 1-3 green. Chapters 4-6 assert the true contract and
are marked .fails as a release-blocking finding (the kill-matrix
convention): after a crash + adopt reopen the metadata index computes its
'catchup' watermark verdict and nothing consumes it — find() serves the
pre-crash index while canonical and counts recover. The catchup wiring is
the cure; a passing .fails will force the marker's removal. The lane runs
in the integration gate (config + coverage guard).
2026-08-24 12:45:51 -07:00
expect ( ( ) = > internals . ensureIndexesLoaded ( ) ) . not . toThrow ( )
// A query during cor's background swap must not trigger brainy's own
// rebuild — reads never rebuild in any case, migrating or not.
2026-06-30 13:37:23 -07:00
expect ( rebuildSpy ) . toHaveBeenCalledTimes ( 0 )
} )
feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
The read gate stops consulting the unnamed isReady() boolean: every provider
may expose healthReport() (sync, O(1), composed from exact ledgers —
HealthReport with a monotonic generation, per-invariant source
ledger|deep|unledgered, missing {count, sample}), and one readiness
authority (assessProviderHealth) derives the verdict. Unledgered families
are UNKNOWN — never healthy, never broken; a report that throws is a loud
not-ready, never a shrug. Reads at the four index choke points refuse with
the typed NotReady errors, narrated once per (provider, generation) — a
read NEVER starts a store walk:
- the first-read lazy build retires (open builds instead, regardless of
size — the ≥10k deferral and the "lazy loading on first query" branch go;
disableAutoRebuild is re-meant honestly in its docs);
- the verify*Live read-path rebuild triggers retire (refuse-or-serve);
- the read-time consistency probe that could launch a dark rebuild from an
ordinary find() retires;
- repairIndex({ rebuild: ['metadata'|'graph'|'vector'] | 'all' }) is the
one explicit door: rebuilds the named leg unconditionally and reports
rebuilt per family; bare repairIndex() stays report-driven.
test(lifecycle): the biography lane — a store's whole life, refereed
tests/lifecycle/: an independent shadow model referees every read after
every chapter (founding, a working day, clean restart, crash, repair,
second life). Chapters 1-3 green. Chapters 4-6 assert the true contract and
are marked .fails as a release-blocking finding (the kill-matrix
convention): after a crash + adopt reopen the metadata index computes its
'catchup' watermark verdict and nothing consumes it — find() serves the
pre-crash index while canonical and counts recover. The catchup wiring is
the cure; a passing .fails will force the marker's removal. The lane runs
in the integration gate (config + coverage guard).
2026-08-24 12:45:51 -07:00
it ( 'the read gate THROWS for the same not-ready vector provider once migration clears (control)' , async ( ) = > {
2026-06-30 13:37:23 -07:00
const brain = await makeWarmBrain ( 2 , { disableAutoRebuild : true } )
const internals = internalsOf ( brain )
const rebuildSpy = vi . spyOn ( internals , 'rebuildIndexesIfNeeded' ) . mockResolvedValue ( undefined )
feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
The read gate stops consulting the unnamed isReady() boolean: every provider
may expose healthReport() (sync, O(1), composed from exact ledgers —
HealthReport with a monotonic generation, per-invariant source
ledger|deep|unledgered, missing {count, sample}), and one readiness
authority (assessProviderHealth) derives the verdict. Unledgered families
are UNKNOWN — never healthy, never broken; a report that throws is a loud
not-ready, never a shrug. Reads at the four index choke points refuse with
the typed NotReady errors, narrated once per (provider, generation) — a
read NEVER starts a store walk:
- the first-read lazy build retires (open builds instead, regardless of
size — the ≥10k deferral and the "lazy loading on first query" branch go;
disableAutoRebuild is re-meant honestly in its docs);
- the verify*Live read-path rebuild triggers retire (refuse-or-serve);
- the read-time consistency probe that could launch a dark rebuild from an
ordinary find() retires;
- repairIndex({ rebuild: ['metadata'|'graph'|'vector'] | 'all' }) is the
one explicit door: rebuilds the named leg unconditionally and reports
rebuilt per family; bare repairIndex() stays report-driven.
test(lifecycle): the biography lane — a store's whole life, refereed
tests/lifecycle/: an independent shadow model referees every read after
every chapter (founding, a working day, clean restart, crash, repair,
second life). Chapters 1-3 green. Chapters 4-6 assert the true contract and
are marked .fails as a release-blocking finding (the kill-matrix
convention): after a crash + adopt reopen the metadata index computes its
'catchup' watermark verdict and nothing consumes it — find() serves the
pre-crash index while canonical and counts recover. The catchup wiring is
the cure; a passing .fails will force the marker's removal. The lane runs
in the integration gate (config + coverage guard).
2026-08-24 12:45:51 -07:00
; ( internals . index as unknown as { isReady ? : ( ) = > boolean } ) . isReady = ( ) = > false
2026-06-30 13:37:23 -07:00
// No isMigrating → not deferring.
feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
The read gate stops consulting the unnamed isReady() boolean: every provider
may expose healthReport() (sync, O(1), composed from exact ledgers —
HealthReport with a monotonic generation, per-invariant source
ledger|deep|unledgered, missing {count, sample}), and one readiness
authority (assessProviderHealth) derives the verdict. Unledgered families
are UNKNOWN — never healthy, never broken; a report that throws is a loud
not-ready, never a shrug. Reads at the four index choke points refuse with
the typed NotReady errors, narrated once per (provider, generation) — a
read NEVER starts a store walk:
- the first-read lazy build retires (open builds instead, regardless of
size — the ≥10k deferral and the "lazy loading on first query" branch go;
disableAutoRebuild is re-meant honestly in its docs);
- the verify*Live read-path rebuild triggers retire (refuse-or-serve);
- the read-time consistency probe that could launch a dark rebuild from an
ordinary find() retires;
- repairIndex({ rebuild: ['metadata'|'graph'|'vector'] | 'all' }) is the
one explicit door: rebuilds the named leg unconditionally and reports
rebuilt per family; bare repairIndex() stays report-driven.
test(lifecycle): the biography lane — a store's whole life, refereed
tests/lifecycle/: an independent shadow model referees every read after
every chapter (founding, a working day, clean restart, crash, repair,
second life). Chapters 1-3 green. Chapters 4-6 assert the true contract and
are marked .fails as a release-blocking finding (the kill-matrix
convention): after a crash + adopt reopen the metadata index computes its
'catchup' watermark verdict and nothing consumes it — find() serves the
pre-crash index while canonical and counts recover. The catchup wiring is
the cure; a passing .fails will force the marker's removal. The lane runs
in the integration gate (config + coverage guard).
2026-08-24 12:45:51 -07:00
expect ( ( ) = > internals . ensureIndexesLoaded ( ) ) . toThrow ( VectorIndexNotReadyError )
// Still never rebuilds — the gate refuses loudly instead.
expect ( rebuildSpy ) . toHaveBeenCalledTimes ( 0 )
2026-06-30 13:37:23 -07:00
} )
// --- 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.
2026-08-03 15:53:13 -07:00
// Epoch 3: the namespace-law key split (bare user keys · literal
// 'system.<field>' scalars, 2026-08-03) — every brain rebuilds onto the
// frozen keys at first open. (Epoch 2 same day: `level` indexability.)
expect ( EXPECTED_INDEX_EPOCH ) . toBe ( 3 )
2026-06-30 13:37:23 -07:00
expect ( CURRENT_DATA_FORMAT ) . toBe ( '8.0' )
} )
} )