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

@ -171,6 +171,66 @@ export interface ProviderInvariantReport {
durationMs: number
}
/**
* @description Where a {@link LedgerInvariantResult} verdict came from:
* - `'ledger'` decided from an exact, durable ledger (a real count, not a sample).
* - `'deep'` decided by a full/expensive scan (the `validateInvariants()` diagnostic path only).
* - `'unledgered'` this family has no ledger yet; the verdict is UNKNOWN, never healthy and never broken.
*/
export type InvariantSource = 'ledger' | 'deep' | 'unledgered'
/**
* @description One invariant verdict inside a {@link HealthReport}. Extends
* {@link InvariantResult} with the provenance of the verdict ({@link InvariantSource})
* and, for a failing set-membership invariant, an exact count plus a capped sample
* of the diverging ids a VERDICT, never a dump. `sample` MUST be capped at 16 ids;
* `count` is the exact number even when `sample` is truncated.
*/
export interface LedgerInvariantResult extends InvariantResult {
/** Provenance of this verdict — see {@link InvariantSource}. */
source: InvariantSource
/** Exact count of diverging/missing items plus a capped (≤16 ids) sample. Present only on a failing set-membership invariant. */
missing?: { count: number; sample: string[] }
}
/**
* @description The NAMED, SYNCHRONOUS, O(1) health report a provider exposes via
* {@link MetadataIndexProvider.healthReport} / {@link GraphIndexProvider.healthReport} /
* {@link VectorIndexProvider.healthReport}. This is the read gate's ONLY source of
* truth for "can I serve right now" it replaces sampled self-probes and the
* unnamed `isReady()` latch with an exact, ledger-derived verdict.
*
* Derivation laws (a provider MUST honor these; brainy's read gate assumes them):
* - `healthy` = every VERIFIED invariant in {@link invariants} holds. An invariant
* whose family is named in {@link unledgered} is NEVER counted toward `healthy`
* either way it is unknown, not passing.
* - `serving` = no verified invariant in {@link invariants} FAILS with `heal: 'rebuild'`.
* A failure with `heal: 'repair'` or `heal: 'none'` is degraded-but-serving
* `serving` stays `true`. Only a `'rebuild'`-grade failure makes `serving` `false`.
* - `validateInvariants()` remains the async DEEP diagnostic (full scans allowed,
* `source: 'deep'` results); `healthReport()` MUST be synchronous, O(1) from
* exact ledgers/counters, and MUST NOT throw for a well-formed provider a
* provider that cannot produce a safe verdict reports it as a failing invariant,
* it does not throw (a throw is read by the gate as a CONTRACT VIOLATION, not as
* "unknown").
*/
export interface HealthReport extends ProviderInvariantReport {
/**
* Monotonic per provider: bumps on every ledger mutation and every rebuild
* boundary. Consumers (the read gate's narration dedup, external callers) may
* cache a verdict per generation.
*/
generation: number
/** Each checked invariant, with provenance — see {@link LedgerInvariantResult}. */
invariants: LedgerInvariantResult[]
/**
* Families with no ledger yet. NAMED here so an operator can see what is not
* yet tracked NEVER counted as healthy (they are not verified) and NEVER
* counted as broken (there is nothing to fail).
*/
unledgered: string[]
}
/**
* @description A provider's self-report of its own outstanding background
* maintenance work (compaction, deferred writes, a build-newverifyswap in
@ -266,6 +326,20 @@ export interface MetadataIndexProvider {
*/
validateInvariants?(): Promise<ProviderInvariantReport>
/**
* @description OPTIONAL. The named, SYNCHRONOUS, O(1) health verdict this
* provider derives from its own exact ledgers see {@link HealthReport} for
* the full derivation laws. MUST NOT perform I/O and MUST NOT throw for a
* well-formed provider (brainy treats a throw as a CONTRACT VIOLATION, never
* as "unknown"). When present, brainy's read gate (`assessProviderHealth()`)
* reads THIS instead of `isReady()` / size heuristics: `serving` decides
* whether reads may proceed; a `false` refuses the read loudly rather than
* triggering a rebuild. Absent the gate falls back to `isReady?()` / the
* size heuristic (this train's JS built-in providers stay on that interim
* path).
*/
healthReport?(): HealthReport
/**
* @description OPTIONAL. A native provider returns true from the moment its
* `init()` detects a large epoch-drift until its background
@ -462,6 +536,20 @@ export interface GraphIndexProvider {
*/
validateInvariants?(): Promise<ProviderInvariantReport>
/**
* @description OPTIONAL. The named, SYNCHRONOUS, O(1) health verdict this
* provider derives from its own exact ledgers see {@link HealthReport} for
* the full derivation laws. MUST NOT perform I/O and MUST NOT throw for a
* well-formed provider (brainy treats a throw as a CONTRACT VIOLATION, never
* as "unknown"). When present, brainy's read gate (`assessProviderHealth()`)
* reads THIS instead of `isReady()` / size heuristics: `serving` decides
* whether reads may proceed; a `false` refuses the read loudly rather than
* triggering a rebuild. Absent the gate falls back to `isReady?()` / the
* size heuristic (this train's JS built-in providers stay on that interim
* path).
*/
healthReport?(): HealthReport
/**
* @description OPTIONAL eager cold-load. Called once during brain init AFTER
* the metadata provider's `init()` (so the id-mapper is hydrated; a native int
@ -1225,6 +1313,20 @@ export interface VectorIndexProvider {
*/
validateInvariants?(): Promise<ProviderInvariantReport>
/**
* @description OPTIONAL. The named, SYNCHRONOUS, O(1) health verdict this
* provider derives from its own exact ledgers see {@link HealthReport} for
* the full derivation laws. MUST NOT perform I/O and MUST NOT throw for a
* well-formed provider (brainy treats a throw as a CONTRACT VIOLATION, never
* as "unknown"). When present, brainy's read gate (`assessProviderHealth()`)
* reads THIS instead of `isReady()` / size heuristics: `serving` decides
* whether reads may proceed; a `false` refuses the read loudly rather than
* triggering a rebuild. Absent the gate falls back to `isReady?()` / the
* size heuristic (this train's JS built-in providers stay on that interim
* path).
*/
healthReport?(): HealthReport
/**
* @description OPTIONAL. A native provider returns true from the moment its
* `init()` detects a large epoch-drift until its background