feat: brain.auditGraph() — read-only graph-truth audit
- New public API: walks every canonical relationship record, queries the same read path applications use (related() with all visibility tiers included), and classifies every discrepancy into its failure family: missingFromReads (records the read path omits — stale adjacency), danglingEndpoints (endpoint entity gone — the partial-delete scar class), readOnlyVerbIds (read-path edges with no canonical record — ghosts). Design-hidden internal/system edges are counted separately so intentional hiding is never misclassified as loss. Counts exact; example lists capped with an explicit truncatedExamples flag; loud narration on incoherence; mutates nothing. - Classification core lives in src/graph/graphAudit.ts behind injected seams (canonical walks + the end-to-end read) so the discrepancy logic is testable in isolation; brainy wires the seams to storage walks and related(). - getNounIdsWithPagination now refuses an undecodable resume cursor loudly — the third and final pagination walk brought under the 8.5.2 cursor contract. - Types exported: GraphAuditReport, GraphAuditDiscrepancy. Docs: the inspection guide gains the audit -> repair -> audit verification ritual.
This commit is contained in:
parent
07144ead86
commit
2a03fae0e2
7 changed files with 522 additions and 0 deletions
|
|
@ -46,6 +46,7 @@ import {
|
|||
pageRank,
|
||||
MinHeap
|
||||
} from './graph/analyticsFallback.js'
|
||||
import { runGraphAudit, type GraphAuditReport } from './graph/graphAudit.js'
|
||||
import { createPipeline } from './streaming/pipeline.js'
|
||||
import { configureLogger, LogLevel, prodLog } from './utils/logger.js'
|
||||
import { setGlobalCache } from './utils/unifiedCache.js'
|
||||
|
|
@ -15489,6 +15490,88 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only graph-truth audit — proves (or disproves) that relationship
|
||||
* reads return canonical truth on THIS brain, and classifies every
|
||||
* discrepancy into its failure family:
|
||||
*
|
||||
* - `missingFromReads` — canonical verb records the read path omits
|
||||
* (PRESENT BUT INVISIBLE: adjacency/membership staleness)
|
||||
* - `danglingEndpoints` — verbs whose endpoint entity is gone (SCAR class)
|
||||
* - `readOnlyVerbIds` — read-path edges with no canonical record (GHOSTS)
|
||||
*
|
||||
* Design-hidden edges (internal/system visibility) are counted separately —
|
||||
* the audit reads with every visibility tier included, so intentional
|
||||
* hiding is never misclassified as index loss. Mutates nothing; safe on a
|
||||
* live brain (cost: one canonical walk + one indexed read per source).
|
||||
* Run it after any engine upgrade, restore, or migration; a `coherent`
|
||||
* report is the verified statement that `related()`/`readdir` can be
|
||||
* trusted. If it reports discrepancies, `repairIndex()` is the sanctioned
|
||||
* heal — re-run the audit afterwards to prove the repair.
|
||||
*
|
||||
* @param options.maxExamples - Cap per example list (counts stay exact). Default 100.
|
||||
* @returns The full audit report; also narrated via logs (loud on incoherence).
|
||||
* @since 8.6.0
|
||||
*/
|
||||
async auditGraph(options: { maxExamples?: number } = {}): Promise<GraphAuditReport> {
|
||||
await this.ensureInitialized({ needs: ['graph'] })
|
||||
|
||||
const PAGE = 1000
|
||||
return runGraphAudit(
|
||||
{
|
||||
eachNounId: async (consume) => {
|
||||
let cursor: string | undefined
|
||||
let offset = 0
|
||||
for (;;) {
|
||||
const page = await (this.storage as unknown as {
|
||||
getNounIdsWithPagination(o: {
|
||||
limit: number
|
||||
offset?: number
|
||||
cursor?: string
|
||||
}): Promise<{ ids: string[]; hasMore: boolean; nextCursor?: string }>
|
||||
}).getNounIdsWithPagination(
|
||||
cursor ? { limit: PAGE, cursor } : { limit: PAGE, offset }
|
||||
)
|
||||
for (const id of page.ids) consume(id)
|
||||
if (!page.hasMore || page.ids.length === 0) break
|
||||
if (page.nextCursor) cursor = page.nextCursor
|
||||
else offset += page.ids.length
|
||||
}
|
||||
},
|
||||
eachVerb: async (consume) => {
|
||||
let cursor: string | undefined
|
||||
let offset = 0
|
||||
for (;;) {
|
||||
const page = await this.storage.getVerbs({
|
||||
pagination: cursor ? { limit: PAGE, cursor } : { limit: PAGE, offset }
|
||||
})
|
||||
for (const verb of page.items) {
|
||||
const v = verb as unknown as Record<string, unknown>
|
||||
consume({
|
||||
id: String(v.id),
|
||||
type: String(v.verb ?? 'unknown'),
|
||||
sourceId: String(v.sourceId),
|
||||
targetId: String(v.targetId),
|
||||
visibility: typeof v.visibility === 'string' ? v.visibility : undefined
|
||||
})
|
||||
}
|
||||
if (!page.hasMore || page.items.length === 0) break
|
||||
if (page.nextCursor) cursor = page.nextCursor
|
||||
else offset += page.items.length
|
||||
}
|
||||
},
|
||||
readRelationsFrom: async (sourceId) =>
|
||||
this.related({
|
||||
from: sourceId,
|
||||
includeInternal: true,
|
||||
includeSystem: true,
|
||||
limit: 100000
|
||||
})
|
||||
},
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
async repairIndex(): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue