feat(repair): repairIndex returns the per-family receipt and narrates its summary
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:
parent
40e7119b85
commit
8d45f964e9
4 changed files with 168 additions and 7 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue