/** * @module graph/graphAudit * @description Read-only graph-truth audit — the graph sibling of `repairIndex()`'s * diagnosis half. Verifies three layers against each other without mutating anything: * * 1. CANONICAL verb records (the storage walk — the source of truth) * 2. the RELATIONSHIP READ PATH (`related()` with all visibility tiers — exactly * what application reads like a VFS `readdir` consult) * 3. ENTITY ENDPOINTS (does each verb's source/target still exist?) * * and classifies every discrepancy into the three failure families production * incidents have shown: * * - `missingFromReads` — a canonical verb record the read path does NOT return * for its source: PRESENT BUT INVISIBLE (adjacency/membership staleness). * - `danglingEndpoints` — a canonical verb whose endpoint entity is gone: * the SCAR class (write-path loss / partial delete). * - `readOnlyVerbIds` — the read path returns an edge with NO canonical * record: GHOST edges (stale index entries). * * `visibilityHiddenCount` is reported separately: an internal/system edge that is * indexed and present but hidden from DEFAULT reads is working as designed — the * audit reads with all tiers included so design-hiding is never misclassified as * index loss. * * Full counts are always exact; only the example LISTS are capped (`maxExamples`) * — a capped report says so via `truncatedExamples`, never silently. */ import { prodLog } from '../utils/logger.js' /** One discrepant relationship, identified fully enough to inspect by hand. */ export interface GraphAuditDiscrepancy { verbId: string from: string to: string type: string } export interface GraphAuditReport { /** True iff every discrepancy count is zero — `related()` returns canonical truth. */ coherent: boolean verbsInCanonical: number entitiesInCanonical: number /** Distinct source entities whose read path was actually consulted (coverage honesty). */ sourcesChecked: number /** PRESENT BUT INVISIBLE: canonical records the read path omits. */ missingFromReadsCount: number missingFromReads: GraphAuditDiscrepancy[] /** SCAR CLASS: canonical verbs with a missing endpoint entity. */ danglingEndpointsCount: number danglingEndpoints: Array /** GHOST EDGES: read-path verb ids with no canonical record. */ readOnlyCount: number readOnlyVerbIds: string[] /** Canonical verbs hidden from DEFAULT reads by design (internal/system visibility). */ visibilityHiddenCount: number /** Example lists above were capped at maxExamples; counts remain exact. */ truncatedExamples: boolean durationMs: number } /** A canonical verb record, as the audit needs it. */ export interface AuditVerbRecord { id: string type: string sourceId: string targetId: string visibility?: string } /** The seams the audit runs over — injected so the walk is testable in isolation. */ export interface GraphAuditDeps { /** Stream every canonical entity id (id-only; no per-entity reads needed). */ eachNounId(consume: (id: string) => void): Promise /** Stream every canonical verb record. */ eachVerb(consume: (verb: AuditVerbRecord) => void): Promise /** * The END-TO-END relationship read for one source, ALL visibility tiers * included — must be the same path application reads consult. */ readRelationsFrom(sourceId: string): Promise> } export interface GraphAuditOptions { /** Cap on entries per example list (counts stay exact). Default 100. */ maxExamples?: number } export async function runGraphAudit( deps: GraphAuditDeps, options: GraphAuditOptions = {} ): Promise { const maxExamples = options.maxExamples ?? 100 const started = Date.now() // 1. Canonical entity ids — endpoint existence oracle. const entityIds = new Set() await deps.eachNounId((id) => entityIds.add(id)) // 2. Canonical verb walk: group by source, check endpoints, note visibility. const canonicalVerbIds = new Set() const bySource = new Map() let verbsInCanonical = 0 let visibilityHiddenCount = 0 let danglingEndpointsCount = 0 const danglingEndpoints: GraphAuditReport['danglingEndpoints'] = [] await deps.eachVerb((verb) => { verbsInCanonical++ canonicalVerbIds.add(verb.id) const list = bySource.get(verb.sourceId) if (list) list.push(verb) else bySource.set(verb.sourceId, [verb]) if (verb.visibility === 'internal' || verb.visibility === 'system') { visibilityHiddenCount++ } const fromMissing = !entityIds.has(verb.sourceId) const toMissing = !entityIds.has(verb.targetId) if (fromMissing || toMissing) { danglingEndpointsCount++ if (danglingEndpoints.length < maxExamples) { danglingEndpoints.push({ verbId: verb.id, from: verb.sourceId, to: verb.targetId, type: verb.type, missingEnd: fromMissing && toMissing ? 'both' : fromMissing ? 'from' : 'to' }) } } }) // 3. Per-source read-path comparison. A verb must be returned by the read // path of ITS OWN source — the exact consult a readdir/traversal makes. let missingFromReadsCount = 0 const missingFromReads: GraphAuditDiscrepancy[] = [] let readOnlyCount = 0 const readOnlyVerbIds: string[] = [] const readOnlySeen = new Set() for (const [sourceId, verbs] of bySource) { const readIds = new Set((await deps.readRelationsFrom(sourceId)).map((r) => r.id)) for (const verb of verbs) { if (!readIds.has(verb.id)) { missingFromReadsCount++ if (missingFromReads.length < maxExamples) { missingFromReads.push({ verbId: verb.id, from: verb.sourceId, to: verb.targetId, type: verb.type }) } } } for (const readId of readIds) { if (!canonicalVerbIds.has(readId) && !readOnlySeen.has(readId)) { readOnlySeen.add(readId) readOnlyCount++ if (readOnlyVerbIds.length < maxExamples) { readOnlyVerbIds.push(readId) } } } } const coherent = missingFromReadsCount === 0 && danglingEndpointsCount === 0 && readOnlyCount === 0 const report: GraphAuditReport = { coherent, verbsInCanonical, entitiesInCanonical: entityIds.size, sourcesChecked: bySource.size, missingFromReadsCount, missingFromReads, danglingEndpointsCount, danglingEndpoints, readOnlyCount, readOnlyVerbIds, visibilityHiddenCount, truncatedExamples: missingFromReadsCount > missingFromReads.length || danglingEndpointsCount > danglingEndpoints.length || readOnlyCount > readOnlyVerbIds.length, durationMs: Date.now() - started } if (coherent) { prodLog.info( `[GraphAudit] coherent: ${verbsInCanonical} verbs across ${bySource.size} sources — ` + `the read path returns canonical truth (${report.durationMs}ms)` ) } else { prodLog.warn( `[GraphAudit] INCOHERENT: ${missingFromReadsCount} present-but-invisible, ` + `${danglingEndpointsCount} dangling-endpoint, ${readOnlyCount} ghost ` + `(of ${verbsInCanonical} canonical verbs, ${bySource.size} sources, ` + `${visibilityHiddenCount} visibility-hidden by design) — ${report.durationMs}ms` ) } return report }