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
102
src/plugin.ts
102
src/plugin.ts
|
|
@ -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-new→verify→swap 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
|
||||
|
|
|
|||
Reference in a new issue