feat(repair): a heal:'repair' verdict routes to the provider's own incremental repair()
repairIndex() acted only on heal:'rebuild' — an invariant asking for the
INCREMENTAL heal (re-post exactly what the ledger names, O(missing), never
a store-sized rebuild) did nothing on brainy's side. A failing 'repair'
verdict now routes to the provider's feature-detected repair(); the
post-heal RE-READ of the report decides success (the acceptance meta-pin's
law — run the named heal once, re-read, nothing may still fail the same
way), and a repair that does not converge is recorded with the escalation
named: repairIndex({ rebuild: [family] }).
This commit is contained in:
parent
ddd5e71928
commit
553e0d97ae
2 changed files with 83 additions and 1 deletions
|
|
@ -17549,10 +17549,50 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
} else {
|
} else {
|
||||||
await p.rebuild()
|
await p.rebuild()
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
report.invariants.some((i) => !i.holds && i.heal === 'repair') &&
|
||||||
|
typeof (provider as { repair?: () => Promise<unknown> }).repair === 'function'
|
||||||
|
) {
|
||||||
|
// INCREMENTAL HEAL ROUTING (ADR-008 D4): a failing verdict whose heal
|
||||||
|
// is 'repair' routes to the provider's own repair() — O(missing),
|
||||||
|
// re-posting exactly what its ledger names, never a store-sized
|
||||||
|
// rebuild. The return shape is the provider's own; the RE-READ of the
|
||||||
|
// report is what decides success (the acceptance meta-pin's law: run
|
||||||
|
// the named heal once, re-read, nothing may still fail the same way).
|
||||||
|
const failingRepairs = report.invariants
|
||||||
|
.filter((i) => !i.holds && i.heal === 'repair')
|
||||||
|
.map((i) => i.name)
|
||||||
|
prodLog.warn(
|
||||||
|
`[Brainy] repairIndex(): provider '${report.provider}' asks for an incremental ` +
|
||||||
|
`repair (${failingRepairs.join(', ')}) — running its own repair().`
|
||||||
|
)
|
||||||
|
await (provider as { repair: () => Promise<unknown> }).repair()
|
||||||
|
let cleared = false
|
||||||
|
let after: ProviderInvariantReport | null = null
|
||||||
|
try {
|
||||||
|
after = await p.validateInvariants()
|
||||||
|
cleared = !after.invariants.some(
|
||||||
|
(i) => !i.holds && i.heal === 'repair' && failingRepairs.includes(i.name)
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// The post-heal re-read failing is itself reportable, never a crash.
|
||||||
|
}
|
||||||
|
record(`provider:${report.provider}`, {
|
||||||
|
checked: true,
|
||||||
|
healed: cleared ? failingRepairs.length : 0,
|
||||||
|
detail: cleared
|
||||||
|
? `incremental repair cleared: ${failingRepairs.join(', ')}`
|
||||||
|
: `repair() ran but the re-read still fails (${
|
||||||
|
after
|
||||||
|
? after.invariants.filter((i) => !i.holds).map((i) => `${i.name}→${i.heal}`).join(', ')
|
||||||
|
: 're-read threw'
|
||||||
|
}) — escalate to repairIndex({ rebuild: ['${familyName}'] })`,
|
||||||
|
reason: cleared ? undefined : 'repair did not converge'
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
record(`provider:${report.provider}`, {
|
record(`provider:${report.provider}`, {
|
||||||
checked: true, healed: 0,
|
checked: true, healed: 0,
|
||||||
detail: `unhealthy without a rebuild verdict (failing: ${report.invariants.filter((i) => !i.holds).map((i) => `${i.name}→${i.heal}`).join(', ')})`
|
detail: `unhealthy without a routable verdict (failing: ${report.invariants.filter((i) => !i.holds).map((i) => `${i.name}→${i.heal}`).join(', ')})`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,4 +68,46 @@ describe('repairIndex per-family receipt', () => {
|
||||||
expect(orphans!.healed, 'the ghost was pruned and receipted').toBeGreaterThan(0)
|
expect(orphans!.healed, 'the ghost was pruned and receipted').toBeGreaterThan(0)
|
||||||
expect(report.healedTotal).toBeGreaterThan(0)
|
expect(report.healedTotal).toBeGreaterThan(0)
|
||||||
}, 120000)
|
}, 120000)
|
||||||
|
|
||||||
|
|
||||||
|
it("a heal:'repair' verdict routes to the provider's own repair(), and the re-read decides", async () => {
|
||||||
|
// A fake provider report: one failing invariant asking for the INCREMENTAL
|
||||||
|
// heal. repairIndex must call repair() (never rebuild()) and count the heal
|
||||||
|
// only when the post-repair re-read clears the same verdict.
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'brainy-repair-route-'))
|
||||||
|
const brain: any = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false, silent: true })
|
||||||
|
await brain.init()
|
||||||
|
brains.push(brain)
|
||||||
|
let repairCalls = 0
|
||||||
|
let rebuildCalls = 0
|
||||||
|
let healed = false
|
||||||
|
const failing = {
|
||||||
|
provider: 'vector', healthy: false, serving: true,
|
||||||
|
invariants: [{ name: 'node-coverage', holds: false, detail: 'short 3', heal: 'repair' as const }],
|
||||||
|
checkedAt: 1, durationMs: 1
|
||||||
|
}
|
||||||
|
const clean = {
|
||||||
|
provider: 'vector', healthy: true, serving: true,
|
||||||
|
invariants: [{ name: 'node-coverage', holds: true, detail: 'ok', heal: 'none' as const }],
|
||||||
|
checkedAt: 2, durationMs: 1
|
||||||
|
}
|
||||||
|
;(brain.index as any).validateInvariants = async () => (healed ? clean : failing)
|
||||||
|
;(brain.index as any).repair = async () => { repairCalls++; healed = true; return { repaired: 3 } }
|
||||||
|
const origRebuild = (brain.index as any).rebuild
|
||||||
|
;(brain.index as any).rebuild = async () => { rebuildCalls++ }
|
||||||
|
try {
|
||||||
|
const report = await brain.repairIndex()
|
||||||
|
const row = report.families.find((f: any) => f.family === 'provider:vector')
|
||||||
|
expect(row, 'the provider family is in the receipt').toBeDefined()
|
||||||
|
expect(repairCalls, 'repair() ran exactly once').toBe(1)
|
||||||
|
expect(rebuildCalls, "a heal:'repair' verdict never runs rebuild()").toBe(0)
|
||||||
|
expect(row!.healed, 'the cleared verdict counts as healed').toBe(1)
|
||||||
|
expect(String(row!.detail)).toMatch(/incremental repair cleared: node-coverage/)
|
||||||
|
} finally {
|
||||||
|
delete (brain.index as any).validateInvariants
|
||||||
|
delete (brain.index as any).repair
|
||||||
|
;(brain.index as any).rebuild = origRebuild
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue