feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door
All checks were successful
CI / Node 22 (push) Successful in 12m19s
CI / Node 24 (push) Successful in 12m16s
CI / Integration + conformance (Node 22) (push) Successful in 18m41s
CI / Bun (latest) (push) Successful in 12m20s

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).
This commit is contained in:
David Snelling 2026-08-24 12:45:51 -07:00
parent a8b5ca0c8f
commit f8f64780b1
19 changed files with 2160 additions and 652 deletions

View file

@ -3,9 +3,14 @@
* @description Pattern-A / Finding 1: a pure semantic find({ query }) has no
* filter, so verifyMetadataLive never fires nothing guarded the vector index.
* A cold native vector index that loaded its COUNT but not its serving structure
* returned a silent []. verifyVectorLive() closes that: honest isReady() first,
* else a known-vector self-match probe; self-heal (rebuild) or throw
* VectorIndexNotReadyError never a silent empty result.
* returned a silent []. verifyVectorLive() closes that: the health-report/isReady()
* authority first, else a known-vector self-match probe.
*
* RE-POINTED to the health-gate law: the guard NEVER rebuilds and NEVER walks
* the store from a read a read-path rebuild is exactly the dark-rebuild
* failure mode the law retires (open() alone owns building). A not-serving
* signal (from either strategy) THROWS VectorIndexNotReadyError immediately,
* with no rebuild attempt in between never a silent empty result.
*/
import { describe, it, expect, beforeEach } from 'vitest'
import { Brainy, NounType, VectorIndexNotReadyError } from '../../src/index.js'
@ -34,50 +39,37 @@ describe('Vector cold-read guard (verifyVectorLive) — silent-[] on cold semant
vi.rebuild = origRebuild
})
it('cold index: verifyVectorLive self-heals via rebuild — semantic find is correct, NOT silent []', async () => {
const vi = brain.index
const origSearch = vi.search.bind(vi)
const origRebuild = vi.rebuild.bind(vi)
let cold = true
brain._vectorVerified = false
// size()>0 (count present) but search returns nothing until a rebuild warms it.
vi.search = async (...a: any[]) => (cold ? [] : origSearch(...a))
vi.rebuild = async (...a: any[]) => { await origRebuild(...a); cold = false }
try {
const res = await brain.find({ query: 'x', searchMode: 'semantic', limit: 100 })
expect(res.length).toBeGreaterThan(0) // self-healed
} finally {
vi.search = origSearch; vi.rebuild = origRebuild
}
})
it('unrecoverably cold index: semantic find throws VectorIndexNotReadyError', async () => {
it('cold index (no isReady()): verifyVectorLive REFUSES immediately — throws VectorIndexNotReadyError, NEVER rebuilds', async () => {
const vi = brain.index
const origSearch = vi.search.bind(vi)
let rebuilds = 0
const origRebuild = vi.rebuild.bind(vi)
brain._vectorVerified = false
vi.search = async () => [] // always cold; rebuild can't fix it
vi.rebuild = async () => {}
// size()>0 (count present) but search never returns a hit for the known vector.
vi.search = async () => []
vi.rebuild = async (...a: any[]) => { rebuilds++; return origRebuild(...a) }
try {
await expect(
brain.find({ query: 'x', searchMode: 'semantic', limit: 100 })
).rejects.toBeInstanceOf(VectorIndexNotReadyError)
expect(rebuilds).toBe(0) // the guard never rebuilds from a read — it refuses loudly instead
} finally {
vi.search = origSearch; vi.rebuild = origRebuild
}
})
it('native provider reporting isReady()===false rebuilds, then serves', async () => {
it('native provider reporting isReady()===false THROWS immediately — never rebuilds', async () => {
const vi = brain.index
let rebuilds = 0
const origRebuild = vi.rebuild.bind(vi)
let ready = false
brain._vectorVerified = false
vi.isReady = () => ready
vi.rebuild = async (...a: any[]) => { await origRebuild(...a); ready = true }
vi.isReady = () => false
vi.rebuild = async (...a: any[]) => { rebuilds++; return origRebuild(...a) }
try {
const res = await brain.find({ query: 'x', searchMode: 'semantic', limit: 100 })
expect(ready).toBe(true) // rebuild ran because isReady() was false
expect(res).toBeDefined()
await expect(
brain.find({ query: 'x', searchMode: 'semantic', limit: 100 })
).rejects.toBeInstanceOf(VectorIndexNotReadyError)
expect(rebuilds).toBe(0) // a not-ready report throws immediately — it is never a rebuild trigger
} finally {
delete vi.isReady; vi.rebuild = origRebuild
}