feat(repair): repairIndex returns the per-family receipt and narrates its summary
Some checks failed
CI / Node 22 (push) Has been cancelled
CI / Node 24 (push) Has been cancelled
CI / Integration + conformance (Node 22) (push) Has been cancelled
CI / Bun (latest) (push) Has been cancelled

repairIndex() now returns a RepairReport: one row per repair family
(orphaned containers, count rollups, VFS containment, metadata corruption,
write quarantine, each provider's invariant pass, degraded-read state) with
checked / healed counts and an explicit skip reason for anything not run —
no silent rows. A summary line narrates families checked and heals applied.

This is the receipts half of the graph-trust program's ask: a repair that
cannot show its work per store is a repair nobody can audit. Pinned: a
healthy store yields a complete zero-heal receipt with every family
accounted; a manufactured pre-8.3.1 ghost container appears in the receipt
as a counted heal. Additive: void-callers are unaffected.
This commit is contained in:
David Snelling 2026-08-20 11:51:29 -07:00
parent 40e7119b85
commit 8d45f964e9
4 changed files with 168 additions and 7 deletions

View file

@ -144,7 +144,9 @@ import {
ScoreExplanation,
FillSubtypeRule,
FillSubtypeRules,
FillSubtypesResult
FillSubtypesResult,
RepairReport,
RepairFamilyReport
} from './types/brainy.types.js'
import { NounType, VerbType, TypeUtils } from './types/graphTypes.js'
import {
@ -17088,8 +17090,13 @@ export class Brainy<T = any> implements BrainyInterface<T> {
)
}
async repairIndex(): Promise<void> {
async repairIndex(): Promise<RepairReport> {
await this.ensureInitialized()
const startedAt = Date.now()
const families: RepairFamilyReport[] = []
const record = (family: string, entry: Omit<RepairFamilyReport, 'family'>): void => {
families.push({ family, ...entry })
}
// Prune orphaned canonical containers left by the pre-8.3.1 partial-delete
// defect: a delete that removed the metadata (content) leg but left the
@ -17105,13 +17112,23 @@ export class Brainy<T = any> implements BrainyInterface<T> {
}
if (typeof pruner.pruneOrphanedEntities === 'function') {
const orphans = await pruner.pruneOrphanedEntities()
if (orphans.nouns.length + orphans.verbs.length > 0) {
const pruned = orphans.nouns.length + orphans.verbs.length
record('orphaned-containers', {
checked: true,
healed: pruned,
...(pruned > 0
? { detail: `${orphans.nouns.length} noun + ${orphans.verbs.length} verb container(s) pruned` }
: {})
})
if (pruned > 0) {
prodLog.warn(
`[Brainy] repairIndex() pruned ${orphans.nouns.length} orphaned noun + ` +
`${orphans.verbs.length} orphaned verb container(s) left by a pre-8.3.1 ` +
`partial delete.`
)
}
} else {
record('orphaned-containers', { checked: false, healed: 0, skipped: 'storage has no container model' })
}
// SANCTIONED RECOUNT — unconditional, not gated on orphans found: the
// persisted counters can be inflated over perfectly clean shelves (deletes
@ -17122,6 +17139,14 @@ export class Brainy<T = any> implements BrainyInterface<T> {
// canonical walk and persists them.
await pruner.rebuildTypeCounts?.()
await pruner.rebuildSubtypeCounts?.()
record('count-rollups', {
checked: typeof pruner.rebuildTypeCounts === 'function',
healed: 0,
detail: typeof pruner.rebuildTypeCounts === 'function'
? 'recomputed from one canonical walk (unconditional)'
: undefined,
...(typeof pruner.rebuildTypeCounts !== 'function' ? { skipped: 'storage has no count rollups' } : {})
})
// The recount changed the rollup truth — re-stamp the entity tree so the
// stamp's invariants match the healed counters (repair leaves a coherent
@ -17135,6 +17160,13 @@ export class Brainy<T = any> implements BrainyInterface<T> {
// containment edges are touched. Loud per repair.
if (this._vfsInitialized && this._vfs) {
const containment = await this._vfs.repairContainment()
record('vfs-containment', {
checked: true,
healed: containment.removed + containment.restored,
...(containment.removed + containment.restored > 0
? { detail: `${containment.removed} stale edge(s) removed, ${containment.restored} restored` }
: {})
})
if (containment.removed + containment.restored > 0) {
prodLog.warn(
`[Brainy] repairIndex() reconciled VFS containment: removed ${containment.removed} ` +
@ -17143,13 +17175,19 @@ export class Brainy<T = any> implements BrainyInterface<T> {
}
}
if (!this._vfsInitialized || !this._vfs) {
record('vfs-containment', { checked: false, healed: 0, skipped: 'VFS not initialized' })
}
await this.metadataIndex.detectAndRepairCorruption()
record('metadata-corruption', { checked: true, healed: 0, detail: 'detect-and-repair pass ran (see its own narration for repairs)' })
// Lift a failed-rollback write-quarantine: force a full rebuild so the
// derived indexes are provably reconciled with canonical, then clear the
// flag so writes resume.
if (this.storeInconsistency) {
await this.rebuildIndexesIfNeeded(true)
const cleared = this.storeInconsistency
record('write-quarantine', { checked: true, healed: 1, detail: `lifted (${cleared.records.length} record(s) reconciled)` })
this.storeInconsistency = null
prodLog.warn(
`[Brainy] repairIndex() reconciled the store and LIFTED the write-quarantine ` +
@ -17166,20 +17204,38 @@ export class Brainy<T = any> implements BrainyInterface<T> {
validateInvariants?: () => Promise<ProviderInvariantReport>
rebuild?: () => Promise<void>
} | null
if (!p || typeof p.validateInvariants !== 'function' || typeof p.rebuild !== 'function') continue
if (!p || typeof p.validateInvariants !== 'function' || typeof p.rebuild !== 'function') {
record(`provider:${(provider as { constructor?: { name?: string } })?.constructor?.name ?? 'unknown'}`, {
checked: false, healed: 0, skipped: 'no validateInvariants/rebuild contract'
})
continue
}
let report: ProviderInvariantReport
try {
report = await p.validateInvariants()
} catch {
} catch (err) {
record(`provider:unknown`, { checked: false, healed: 0, skipped: `validateInvariants threw: ${(err as Error).message}` })
continue // a throwing validateInvariants is surfaced by validateIndexConsistency; skip repair here
}
if (report.healthy) continue
if (report.healthy) {
record(`provider:${report.provider}`, { checked: true, healed: 0 })
continue
}
if (report.invariants.some((i) => !i.holds && i.heal === 'rebuild')) {
record(`provider:${report.provider}`, {
checked: true, healed: 1,
detail: `rebuilt from canonical (failing: ${report.invariants.filter((i) => !i.holds).map((i) => i.name).join(', ')})`
})
prodLog.warn(
`[Brainy] repairIndex(): provider '${report.provider}' has a failing invariant ` +
`requiring a rebuild — reconciling its derived state from canonical.`
)
await p.rebuild()
} else {
record(`provider:${report.provider}`, {
checked: true, healed: 0,
detail: `unhealthy without a rebuild verdict (failing: ${report.invariants.filter((i) => !i.holds).map((i) => `${i.name}${i.heal}`).join(', ')})`
})
}
}
// detectAndRepairCorruption() above rebuilt the derived indexes from
@ -17190,7 +17246,18 @@ export class Brainy<T = any> implements BrainyInterface<T> {
this._indexDegradedIds.clear()
this._indexRebuildFailed = null
this._degradedReadWarned = false
record('degraded-read-state', { checked: true, healed: 1, detail: 'degraded ids cleared, read-path warning re-armed' })
}
const healedTotal = families.reduce((n, f) => n + f.healed, 0)
const report: RepairReport = { families, healedTotal, durationMs: Date.now() - startedAt }
prodLog.warn(
`[Brainy] repairIndex complete in ${report.durationMs}ms — ` +
`${families.filter((f) => f.checked).length}/${families.length} families checked, ` +
`${healedTotal} heal(s): ` +
families.map((f) => `${f.family}=${f.checked ? f.healed : 'skipped'}`).join(', ')
)
return report
}
/**