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).
This commit is contained in:
parent
a8b5ca0c8f
commit
f8f64780b1
19 changed files with 2160 additions and 652 deletions
|
|
@ -3,10 +3,14 @@
|
|||
* reported cold `find({ where })` returning a silent `[]` on a freshly-opened
|
||||
* brain (a native metadata index that reports data but has not loaded its field
|
||||
* postings). This guard, the field-index counterpart of verifyGraphAdjacencyLive,
|
||||
* probes a known persisted value on the first filtered find(): if the index does
|
||||
* not serve it, brainy rebuilds and re-probes, and raises a loud
|
||||
* MetadataIndexNotReadyError only if the rebuild still can't serve — never a
|
||||
* silent empty result that misrepresents existing data.
|
||||
* probes a known persisted value on the first filtered find().
|
||||
*
|
||||
* 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). When the probe
|
||||
* cannot serve the known value it raises a loud MetadataIndexNotReadyError
|
||||
* IMMEDIATELY, with no rebuild attempt in between — never a silent empty
|
||||
* result that misrepresents existing data.
|
||||
*
|
||||
* The 8.0 JS index cold-loads correctly, so we simulate the cold native failure
|
||||
* mode by intercepting the provider's getIdsForFilter/rebuild.
|
||||
|
|
@ -42,37 +46,19 @@ describe('Metadata cold-read guard (#venue silent-[])', () => {
|
|||
mi.rebuild = origRebuild
|
||||
})
|
||||
|
||||
it('cold index: verifyMetadataLive self-heals via rebuild — find({where}) is correct, NOT silent []', async () => {
|
||||
it('cold index: verifyMetadataLive REFUSES immediately — find({where}) throws MetadataIndexNotReadyError, NEVER a silent [], and NEVER a rebuild attempt', async () => {
|
||||
const mi = brain.metadataIndex
|
||||
const origGetIds = mi.getIdsForFilter.bind(mi)
|
||||
let rebuilds = 0
|
||||
const origRebuild = mi.rebuild.bind(mi)
|
||||
let cold = true
|
||||
brain._metadataVerified = false // re-arm the one-shot for this scenario
|
||||
mi.getIdsForFilter = async (...a: any[]) => (cold ? [] : origGetIds(...a))
|
||||
mi.rebuild = async () => {
|
||||
await origRebuild()
|
||||
cold = false // the rebuild warms the postings
|
||||
}
|
||||
try {
|
||||
const res = await brain.find({ where: { status: 'active' }, limit: 100 })
|
||||
expect(res.length).toBe(1) // self-healed — the known entity is returned
|
||||
} finally {
|
||||
mi.getIdsForFilter = origGetIds
|
||||
mi.rebuild = origRebuild
|
||||
}
|
||||
})
|
||||
|
||||
it('unrecoverably cold index: find({where}) throws MetadataIndexNotReadyError — never a silent []', async () => {
|
||||
const mi = brain.metadataIndex
|
||||
const origGetIds = mi.getIdsForFilter.bind(mi)
|
||||
const origRebuild = mi.rebuild.bind(mi)
|
||||
brain._metadataVerified = false
|
||||
mi.getIdsForFilter = async () => [] // always cold; rebuild can't fix it
|
||||
mi.rebuild = async () => {}
|
||||
mi.getIdsForFilter = async () => [] // cold: the known value never resolves
|
||||
mi.rebuild = async () => { rebuilds++; return origRebuild() }
|
||||
try {
|
||||
await expect(brain.find({ where: { status: 'active' }, limit: 100 })).rejects.toBeInstanceOf(
|
||||
MetadataIndexNotReadyError
|
||||
)
|
||||
expect(rebuilds).toBe(0) // the guard never rebuilds from a read — it refuses loudly instead
|
||||
} finally {
|
||||
mi.getIdsForFilter = origGetIds
|
||||
mi.rebuild = origRebuild
|
||||
|
|
|
|||
Reference in a new issue