/** * Live schema source for the memory-write / memory-update validator. * * Three sources, one decision rule: * * liveLabels — `db.labels()` snapshot from Neo4j (refreshed every 60s * by SchemaCache from platform/lib/graph-mcp). * declaredLabels — labels declared in `platform/neo4j/schema.cypher` * (constraint + index FOR (alias:Label) forms). * markdownLabels — labels with documented required-property groups in * `platform/plugins/memory/references/schema-*.md`. * * recognised = liveLabels ∪ declaredLabels ← gates label existence * markdown documented? → run required-property check, else skip * * Why a per-process cache rather than a shared one. The graph-mcp proxy and * the memory plugin run as separate stdio MCP servers spawned by Claude Code * (each with its own Neo4j driver). "In-process" brief means * "no per-decision tool round-trip" — *not* a shared cache instance, which * would require IPC. Two SchemaCache instances polling the same Neo4j every * 60s costs roughly two `db.labels()` calls per minute and avoids the IPC * surface entirely. * * Why drift runs from the cache's emit hook. SchemaCache uses single-flight * refresh — concurrent `refresh()` calls share an in-flight promise. A * parallel timer could race against an in-progress refresh and read a stale * snapshot during the comparison. The cache's own `emit()` callback fires * synchronously after a successful refresh swap, so tapping it guarantees * the snapshot we read is the one the refresh just produced. * * Bootstrap posture. Fresh installs have no nodes yet, so `db.labels()` * returns []. The validator must still accept the very first * `LocalBusiness` write because that label is *declared* in schema.cypher * even when no node carries it. The recognised-set union resolves this * directly; no fail-open branch needed for that case. * * db.labels() declared in cypher recognised? * ───────── ────────────────── ─────────── * [] [LocalBusiness] LocalBusiness ✓ (bootstrap) * [LocalBusiness] [LocalBusiness] LocalBusiness ✓ (steady state) * [Person] [LocalBusiness] Person + LocalBusiness ✓ * [] [] {} reject all (Neo4j * unreachable AND * schema.cypher * unreadable — both * logged loudly) */ import { SchemaCache, neo4jSchemaFetcher, type SchemaFetcher } from "../../../../../lib/graph-mcp/dist/schema-cache.js"; export type DriftKind = "live-not-declared" | "declared-not-live" | "markdown-orphan"; export interface DriftEntry { kind: DriftKind; token: string; } export interface LiveSchemaSource { /** live ∪ declared — used for label existence */ recognisedLabels(): Set; /** Just `db.labels()` — for drift comparison and observability */ liveLabels(): Set; /** Just schema.cypher constraint+index declarations */ declaredLabels(): Set; /** Whether the live snapshot has resolved at least once */ liveReady(): boolean; } interface BuildLiveSchemaSourceOptions { schemaCypherPath: string; /** * Markdown-documented label set (the renamed `LoadedSchema.markdownLabels`). * Used only for boot/refresh drift comparison — not for the per-write * decision path. */ markdownLabels: Iterable; fetcher: SchemaFetcher; /** Override default 60s refresh in tests. */ refreshIntervalMs?: number; /** Test-injectable log emitter. Default: process.stderr. */ emit?: (line: string) => void; } export interface LiveSchemaRuntime { source: LiveSchemaSource; cache: SchemaCache; /** * Resolves once the boot refresh has completed (success or failure). * Callers that want drift logged at boot should `await runtime.ready`. * Validators do not block on this — they fail-open against an empty live * snapshot, which (combined with the declared set) still accepts the * bootstrap labels on a fresh install. */ ready: Promise; } /** * Build the live schema source plus its backing cache. * * The caller (memory MCP `index.ts`) owns lifecycle: it must `await * runtime.ready` (or fire-and-forget) and call `runtime.cache.stop()` on * shutdown. Tests pass a stub fetcher and a synthetic interval. */ export declare function buildLiveSchemaSource(options: BuildLiveSchemaSourceOptions): LiveSchemaRuntime; /** Re-export the fetcher factory so index.ts can wire one up cleanly. */ export { neo4jSchemaFetcher }; interface DriftCheckInput { live: ReadonlySet; declared: ReadonlySet; markdown: ReadonlySet; emit: (line: string) => void; } export declare function runDriftCheck(input: DriftCheckInput): DriftEntry[]; /** * Resolve the absolute path to `platform/neo4j/schema.cypher`. * * Layout (compiled and source paths are siblings, so the offset is the same): * platform/plugins/memory/mcp/{src,dist}/lib/live-schema-source.{ts,js} * platform/neo4j/schema.cypher * * From `import.meta.dirname` (mcp/{src,dist}/lib/) the relative path is * `../../../../../neo4j/schema.cypher` — 5 hops to platform/, then down. * `PLATFORM_ROOT` overrides for production where the layout may differ. */ export declare function defaultSchemaCypherPath(): string; //# sourceMappingURL=live-schema-source.d.ts.map