fix(health): one contract for a throwing probe — heal is none, serving is not withheld; repair report gains missing/rebuilt/reason
A validateInvariants() that threw was re-synthesized by the host's catch as
heal:'rebuild' with serving:false — a rebuild lever one transient exception
away, while the native provider's own composer reports the same event as
heal:'none'. Two components disagreeing on what a thrown check means is how
a flaky probe becomes an outage. Both now agree: the report is named
('validate-invariants-threw'), loud (healthy:false, the error in detail),
unverified — and it never buys a rebuild and never withholds serving; the
provider's serving verdict is the provider's to compose, not inferred from a
probe that failed to run. The synthesized report also carries the provider's
name instead of 'unknown'.
RepairFamilyReport gains `missing: {count, sample}` (an exact count plus a
capped id sample — a verdict, not a dump), `rebuilt` (a full generational
rebuild ran, as opposed to an incremental heal) and `reason`, aligning the
receipt's shape with the provider-side health report.
Pinned in tests/unit/validate-invariants-delegation.test.ts.
This commit is contained in:
parent
7c8c8be30c
commit
116550eb16
3 changed files with 54 additions and 3 deletions
|
|
@ -14221,16 +14221,29 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
const report = await fn.call(provider)
|
const report = await fn.call(provider)
|
||||||
if (report && Array.isArray(report.invariants)) reports.push(report)
|
if (report && Array.isArray(report.invariants)) reports.push(report)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// ONE CONTRACT FOR A THROWING PROBE, both engines: a probe that throws
|
||||||
|
// is `heal: 'none'` with the error in `detail` — flakiness can never
|
||||||
|
// buy a rebuild, and a thrown check never changes `serving` (the
|
||||||
|
// provider's serving verdict is composed by the provider, not inferred
|
||||||
|
// from a probe that failed to run). This catch used to synthesize
|
||||||
|
// `heal: 'rebuild'` — the read-triggered dark-rebuild lever one
|
||||||
|
// transient exception away — while the native composer said 'none';
|
||||||
|
// two components disagreeing on what a throw means is how a flaky
|
||||||
|
// probe became an outage. `healthy: false` stays: an unrunnable probe
|
||||||
|
// is a named, loud, unverified state, never a clean bill.
|
||||||
|
const name = typeof (provider as { name?: string })?.name === 'string'
|
||||||
|
? (provider as { name: string }).name
|
||||||
|
: 'unknown'
|
||||||
reports.push({
|
reports.push({
|
||||||
provider: 'unknown',
|
provider: name,
|
||||||
healthy: false,
|
healthy: false,
|
||||||
serving: false,
|
serving: true,
|
||||||
invariants: [
|
invariants: [
|
||||||
{
|
{
|
||||||
name: 'validate-invariants-threw',
|
name: 'validate-invariants-threw',
|
||||||
holds: false,
|
holds: false,
|
||||||
detail: `validateInvariants() threw (contract violation — it must never throw): ${(err as Error).message}`,
|
detail: `validateInvariants() threw (contract violation — it must never throw): ${(err as Error).message}`,
|
||||||
heal: 'rebuild'
|
heal: 'none'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
checkedAt: Date.now(),
|
checkedAt: Date.now(),
|
||||||
|
|
|
||||||
|
|
@ -1200,10 +1200,23 @@ export interface RelateManyParams<T = any> {
|
||||||
*/
|
*/
|
||||||
export interface RepairFamilyReport {
|
export interface RepairFamilyReport {
|
||||||
family: string
|
family: string
|
||||||
|
/** The family was actually examined (false = skipped; see `skipped`/`reason`). */
|
||||||
checked: boolean
|
checked: boolean
|
||||||
|
/** Items re-posted / corrected in place — the incremental heal count. */
|
||||||
healed: number
|
healed: number
|
||||||
|
/**
|
||||||
|
* What the check found missing or divergent, when it can name it: an exact
|
||||||
|
* count plus a capped sample of ids (never the whole list — a report is a
|
||||||
|
* verdict, not a dump). Absent when the family has nothing to name.
|
||||||
|
*/
|
||||||
|
missing?: { count: number; sample: string[] }
|
||||||
|
/** A full generational rebuild of this family ran (as opposed to an incremental heal). */
|
||||||
|
rebuilt?: boolean
|
||||||
detail?: string
|
detail?: string
|
||||||
|
/** Why the family was not checked (`checked: false`). */
|
||||||
skipped?: string
|
skipped?: string
|
||||||
|
/** Why the outcome is what it is when neither `detail` nor `skipped` says it. */
|
||||||
|
reason?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The full receipt returned by repairIndex(). */
|
/** The full receipt returned by repairIndex(). */
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,31 @@ describe('validateIndexConsistency delegates to provider validateInvariants() (P
|
||||||
delete brain.index.validateInvariants
|
delete brain.index.validateInvariants
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('ONE CONTRACT FOR A THROWING PROBE: heal is none (flakiness never buys a rebuild) and serving is not withheld', async () => {
|
||||||
|
// The probe that fails to RUN must never be read as "the index is broken,
|
||||||
|
// rebuild it" — that synthesized heal:'rebuild' was the dark-rebuild lever
|
||||||
|
// one transient exception away, and the native composer already said
|
||||||
|
// 'none' for the same event. Both engines now agree: named, loud,
|
||||||
|
// unverified — and never a rebuild, never a withheld serve.
|
||||||
|
brain.index.validateInvariants = async () => { throw new Error('transient: mmap window busy') }
|
||||||
|
const v = await brain.validateIndexConsistency()
|
||||||
|
const thrown = v.providers?.find((p: ProviderInvariantReport) =>
|
||||||
|
p.invariants.some((i) => i.name === 'validate-invariants-threw')
|
||||||
|
)
|
||||||
|
expect(thrown).toBeDefined()
|
||||||
|
expect(thrown!.healthy).toBe(false)
|
||||||
|
expect(thrown!.serving).toBe(true)
|
||||||
|
const inv = thrown!.invariants.find((i) => i.name === 'validate-invariants-threw')!
|
||||||
|
expect(inv.holds).toBe(false)
|
||||||
|
expect(inv.heal).toBe('none')
|
||||||
|
expect(inv.detail).toMatch(/transient: mmap window busy/)
|
||||||
|
// No provider report in the set recommends a rebuild for this event.
|
||||||
|
expect(
|
||||||
|
v.providers!.flatMap((p: ProviderInvariantReport) => p.invariants).some((i) => i.heal === 'rebuild')
|
||||||
|
).toBe(false)
|
||||||
|
delete brain.index.validateInvariants
|
||||||
|
})
|
||||||
|
|
||||||
it('providers without validateInvariants() are omitted (JS baseline unchanged)', async () => {
|
it('providers without validateInvariants() are omitted (JS baseline unchanged)', async () => {
|
||||||
const v = await brain.validateIndexConsistency()
|
const v = await brain.validateIndexConsistency()
|
||||||
expect(v.providers).toBeUndefined()
|
expect(v.providers).toBeUndefined()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue