@remnic/coding-graph 9.6.23 → 9.6.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -19
- package/dist/{chunk-CPYJACC5.js → chunk-ABDBWCXU.js} +146 -8
- package/dist/chunk-ABDBWCXU.js.map +1 -0
- package/dist/{chunk-5I2DBHOQ.js → chunk-I4R6GAAA.js} +2 -2
- package/dist/cypher/query-parser.js +2 -2
- package/dist/graph-store.d.ts +47 -0
- package/dist/graph-store.js +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +318 -26
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/edge-provenance-scope.test.ts +457 -0
- package/src/engine/emit.ts +55 -8
- package/src/engine/extractors.ts +45 -11
- package/src/graph-store.ts +265 -9
- package/src/heuristic-resolution.test.ts +596 -0
- package/src/heuristic-resolution.ts +395 -0
- package/src/lsp/resolution.ts +52 -11
- package/src/lsp-reconciliation.test.ts +145 -0
- package/src/reindex.test.ts +57 -0
- package/src/reindex.ts +18 -6
- package/dist/chunk-CPYJACC5.js.map +0 -1
- /package/dist/{chunk-5I2DBHOQ.js.map → chunk-I4R6GAAA.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/graph-store.ts"],"sourcesContent":["/**\n * Coding-graph write pipeline — two-pass single-batch single-transaction\n * delete + reinsert per file. PR1 scope (issue #1552):\n *\n * - Node ids are sha256 over SORTED key material (qualified name, file\n * path, label) — rule 23/38; the same hash MUST be computed identically\n * at ingest and lookup time.\n * - File contents are NEVER stored — only spans + content hashes\n * (privacy + DB size; rule 11). `get_code_snippet` is a read-side\n * concern that lands in PR2.\n * - DB handles live on the GraphStore instance, keyed per instance —\n * never module scope (rule 11).\n * - Writes are serialized per DB with a rejection-recovering queue\n * (rule 40). A second concurrent `upsertFileBatch` call waits on a\n * FIFO tail then runs; if it rejects (timeout / abort), the queue\n * drains so the next call can proceed.\n * - One DB per namespace path passed in by the caller — this PR does\n * NOT add namespace resolution; rule 42 keeps it that way until PR2\n * wires the namespace layer.\n *\n * Two-pass ingestion (per `upsertFileBatch`):\n * 1. Every file in the batch is upserted (file row + node row per\n * symbol). FTS5 is kept in lockstep via explicit DELETE/INSERT on\n * the contentless `nodes_fts` table (the write pipeline is the\n * single source of FTS truth — no auto-triggers).\n * 2. Edges are resolved against the FULL batch's node map (already in\n * the DB after pass 1) so cross-file edges are order-independent.\n * Stale edges for files in this batch are deleted first so changed\n * confidence/provenance values overwrite prior rows.\n *\n * Tagged failure shapes (rule 34): the open + write paths return a\n * discriminated union. Success: `{ok:true, results: UpsertResult[]}`. Failure:\n * `{ok:false, code:\"db_locked\"|\"db_corrupt\"}` — no message is exposed\n * to callers because `error.message` from better-sqlite3 frequently\n * contains absolute filesystem paths and stack snippets that should\n * never reach agents or HTTP surfaces (rule 11). The store logs the\n * underlying error internally and returns the code only.\n */\nimport { createHash } from \"node:crypto\";\nimport { mkdir, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport {\n openBetterSqlite3,\n type BetterSqlite3Database,\n} from \"@remnic/core/runtime/better-sqlite\";\n\nimport {\n applyCodingGraphSchema,\n ftsRowidForNodeId,\n isEdgeProvenance,\n readSchemaVersion,\n type EdgeProvenance,\n} from \"./graph-schema.js\";\n\nimport { expectRow, expectRows } from \"./row-types.js\";\n\n// Re-export the core IR contract types so existing imports from\n// `./graph-store.js` still resolve. The store no longer redefines these;\n// it derives from @remnic/core's contract so PR2 callers can pass\n// `ParseResult.ir` directly without field-name translation or casts\n// (chatgpt-codex-connector P2: 'Derive store FileIR from the core parser\n// contract'). Core owns the canonical types in\n// packages/remnic-core/src/coding/coding-graph-types.ts; this package\n// implements against them.\nexport type {\n FileIR,\n SymbolIR,\n ImportIR,\n ExportIR,\n CallSiteIR,\n RouteIR,\n CodingGraphLanguage,\n} from \"@remnic/core/coding/coding-graph-types\";\n\nimport type {\n FileIR,\n SymbolIR,\n ExportIR,\n RouteIR,\n CodingGraphLanguage,\n} from \"@remnic/core/coding/coding-graph-types\";\n\n/**\n * Half-open byte span `[startByte, endByte)` — matches @remnic/core's\n * inline span type. Kept as a named alias for API consumers that import\n * `ByteSpan` from the store subpath (issue #1551 / rule 35).\n */\nexport type ByteSpan = { readonly startByte: number; readonly endByte: number };\n\n/**\n * Symbol kind union — matches @remnic/core's `SymbolIR[\"kind\"]` exactly\n * (core does not export this as a named type).\n */\nexport type SymbolKind =\n | \"function\"\n | \"class\"\n | \"method\"\n | \"interface\"\n | \"enum\"\n | \"type\"\n | \"module\";\n\n/**\n * Store-specific edge — references nodes by `qualifiedName` so the store\n * can resolve them against the same batch's symbol set plus the on-disk\n * node table. PR1 only carries CALLS-style edges; PR2 adds the rest of\n * #1552's edge types.\n *\n * Optional `srcNodeId` / `dstNodeId` (issue #1677) carry the content-\n * derived node id (the same canonical hash form the store uses as\n * `nodes.id`, see `nodeIdFor`). When present, the standalone\n * `upsertEdges` path resolves the endpoint by `nodes.id` (unique) instead\n * of by qualified name, so a SIMILAR_TO edge between two symbols that\n * share a qualified name across files is persisted rather than dropped as\n * ambiguous. The qname-keyed file-batch path and the existing\n * `ambiguous … drops edges` behavior are unchanged. Only populated by\n * callers that originate edges from node-id-keyed pairs (the semantic\n * SIMILAR_TO pipeline); structural/trace edges keep the qname path.\n */\nexport interface EdgeIR {\n /** Qualified name of the source node (caller / definition site). */\n srcQualifiedName: string;\n /** Qualified name of the destination node (callee / type used). */\n dstQualifiedName: string;\n type: string;\n confidence: number;\n provenance: EdgeProvenance;\n /**\n * Optional content-derived source node id (`nodes.id`). When present on\n * a standalone-edge upsert, the store resolves the endpoint by id\n * (unambiguous) instead of falling back to qualified-name resolution.\n */\n readonly srcNodeId?: string;\n /** Optional content-derived destination node id — see {@link EdgeIR.srcNodeId}. */\n readonly dstNodeId?: string;\n /**\n * Repo-relative, extension-stripped path the dst must live in (issue\n * #1894 review): derived from a relative import's module specifier. A\n * hinted edge resolves ONLY among nodes whose file path matches the\n * hint (`<hint>`, `<hint>.<ext>`, `<hint>/index.<ext>`, or\n * `<hint>/__init__.<ext>`) — never via\n * the global bare-name fallback — so `import { foo } from \"./missing\"`\n * can never bind an unrelated same-named symbol elsewhere in the repo.\n */\n readonly dstPathHint?: string;\n /**\n * Language of the importing file (issue #1894 round 13): constrains the\n * hinted dst's file extension to that language's module-resolution set\n * so a polyglot repo cannot cross-bind a JS import to a same-named .py\n * file.\n */\n readonly dstImporterLanguage?: string;\n}\n\n/**\n * Store input — the subset of @remnic/core's `FileIR` the store reads,\n * plus the store-specific `edges` extension. A core `FileIR` (from\n * `ParseResult.ir`) is structurally assignable here: all required fields\n * (path, language, contentHash, symbols, imports, exports, callSites,\n * routes) match by name and readonly-ness. PR2 callers pass\n * `{ ...parseResult.ir, edges }` (or the bare IR when edges are absent)\n * with zero casts or field-name translation.\n *\n * PR2 adds optional `exports` and `routes` consumption: when present,\n * the write pipeline marks matching nodes in `node_attributes` so the\n * `deadCode()` query can exclude them via the\n * {@link DEAD_CODE_EXCLUSION} constant. Both fields are optional because\n * a PR1-era caller (or a JSON-IR caller that strips them) still ingests\n * cleanly — the dead-code query simply sees no exclusion flags.\n */\nexport interface StoreFileIR {\n readonly path: string;\n readonly language: CodingGraphLanguage;\n readonly contentHash: string;\n readonly symbols: readonly SymbolIR[];\n /** Store-specific edges derived from the IR by the caller. */\n readonly edges?: readonly EdgeIR[];\n /**\n * When present, the stale-edge delete in `upsertFileEdges` is scoped to\n * edges whose provenance is in this list: prior src-owned edges of OTHER\n * provenances survive un-asserted (issue #1891). The reindex pipeline\n * asserts `[\"heuristic\"]` because a fresh parse says nothing about\n * `trace`/`lsp` edges; deleting them on every re-ingest would destroy\n * state the parse never contradicted (rule 25). Absent = legacy\n * behavior: every stale src-owned edge is deleted.\n */\n readonly assertedEdgeProvenances?: readonly EdgeProvenance[];\n /**\n * Per-file export list (mirrors core FileIR.exports). When present,\n * the write pipeline marks every node in this file whose `name`\n * matches an ExportIR.name as `is_exported=1` in `node_attributes`.\n * Name-matching is the conventional pattern: a parser that emits a\n * `export const foo` declaration also emits a SymbolIR named `foo`\n * (or omits it if foo is a non-symbol like a plain variable); the\n * dead-code query then excludes surviving exported symbols.\n */\n readonly exports?: readonly ExportIR[];\n /**\n * Per-file HTTP route declarations (mirrors core FileIR.routes). When\n * present, the write pipeline marks the node whose `qualifiedName`\n * equals `route.handlerQualifiedName` as `is_route_handler=1` in\n * `node_attributes`. Route handlers are reachable from HTTP traffic\n * regardless of whether any other indexed node CALLS them.\n */\n readonly routes?: readonly RouteIR[];\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Result shapes — tagged failures (rule 34).\n// ──────────────────────────────────────────────────────────────────────────\n\nexport type GraphStoreFailureCode = \"db_locked\" | \"db_corrupt\" | \"db_error\" | \"store_closed\";\n\nexport interface GraphStoreFailure {\n ok: false;\n code: GraphStoreFailureCode;\n}\n\nexport interface UpsertResult {\n path: string;\n fileId: number;\n nodeCount: number;\n edgeCount: number;\n /**\n * Dangling edges observed while deleting the file's prior subgraph\n * (cross-file edges whose `dst` belonged to a node owned by this file).\n * Per the PR1 dangling-edge policy in {@link graph-schema}, they are\n * DROPPED, not kept with a marker. Surfaced here so callers can log\n * the loss (rule 11, 40).\n */\n droppedDanglingEdges: number;\n}\n\nexport interface UpsertSuccess {\n ok: true;\n results: UpsertResult[];\n}\n\nexport type UpsertBatchResult = UpsertSuccess | GraphStoreFailure;\n\n/**\n * Result of {@link GraphStore.upsertEdges} — a standalone-edge write used by\n * the codegraph ingest_traces surface (issue #1554). `persisted` counts\n * edges actually inserted/updated; `skipped` counts edges whose src or dst\n * did not resolve to exactly one node (dangling-edge policy).\n */\nexport interface UpsertEdgesSuccess {\n ok: true;\n persisted: number;\n skipped: number;\n}\nexport type UpsertEdgesResult = UpsertEdgesSuccess | GraphStoreFailure;\n\n// ──────────────────────────────────────────────────────────────────────────\n// PR2 read primitives — query / result types (issue #1552 steps 4–5).\n// Tagged failures share the {@link GraphStoreFailureCode} set so callers\n// can use one switch over every store surface.\n// ──────────────────────────────────────────────────────────────────────────\n\n/** Direction of traversal relative to the edge's src→dst orientation. */\nexport type TraverseDirection = \"outgoing\" | \"incoming\" | \"both\";\n\n/**\n * Iterative frontier BFS over the edges table. Cycle-safe via a JS\n * visited set keyed by node id; predictable memory regardless of graph\n * shape (recursive CTEs are the documented fallback if benchmarks ever\n * justify them — issue #1552 design section).\n */\nexport interface TraverseQuery {\n /**\n * Start node. Accepts either a node id or a qualified name — the\n * store resolves a qualified name to its deterministic id via the\n * same `(qualifiedName, filePath, label)` identity used at ingest\n * time. When the qualified name is ambiguous (declared in more than\n * one file), the query is rejected with `code: \"ambiguous_start\"`\n * so the caller can pass an explicit node id instead.\n */\n start: string;\n /** Default `\"outgoing\"`. */\n direction?: TraverseDirection;\n /**\n * Edge types to follow (e.g. `[\"CALLS\", \"USES_TYPE\"]`). When omitted\n * or empty, every edge type in the table is followed. Unknown edge\n * types simply contribute no rows — the query is not rejected\n * because the schema places no CHECK constraint on `edges.type`.\n */\n edgeTypes?: readonly string[];\n /**\n * Maximum BFS depth. Half-open: a node at depth == maxDepth IS\n * included; a node at depth maxDepth+1 is NOT (rule 35). The start\n * node itself sits at depth 0 and is always included in the result\n * set when it exists. A maxDepth of 0 returns just the start node.\n * MUST be a non-negative integer — invalid values are rejected with\n * `code: \"invalid_query\"` rather than silently clamped (rule 51).\n */\n maxDepth: number;\n}\n\nexport interface TraverseHit {\n nodeId: string;\n qualifiedName: string;\n name: string;\n label: string;\n /** Repo-relative file path of the node (joined from files.path). */\n filePath: string;\n /** BFS depth from the start node (start = 0). */\n depth: number;\n}\n\nexport type TraverseResult =\n | { ok: true; hits: TraverseHit[] }\n | ({ ok: false } & GraphStoreFailure)\n | { ok: false; code: \"unknown_start\" | \"ambiguous_start\" | \"invalid_query\" };\n\n/**\n * Default cap on the number of concrete paths {@link GraphStore.traversePaths}\n * enumerates before stopping and flagging `truncated`. Bounds the worst-case\n * exponential blowup of relationship-simple path enumeration on dense\n * subgraphs (issue #1650). Callers may override per-query via\n * {@link TraversePathsQuery.maxPaths}.\n */\nexport const DEFAULT_TRAVERSE_PATHS_MAX = 10_000;\n\n/**\n * Hard upper bound on {@link TraversePathsQuery.maxHops}. The DFS recurses\n * once per hop; an unbounded depth (e.g. a Cypher `*15000`) would overflow\n * the call stack before `maxPaths` could stop it. 1000 is ~100x any\n * realistic code-graph depth and recurses safely (chatgpt-codex-connector\n * P2: 'Avoid recursive DFS for deep bounded paths').\n */\nexport const MAX_TRAVERSE_PATHS_HOPS = 1000;\n\n/**\n * Path-enumerating traversal query (issue #1650). Mirrors {@link TraverseQuery}\n * but yields CONCRETE paths rather than BFS-shortest-depth reachability, so an\n * exact `*N` (N > 1) hop count is honored for nodes reachable at both a shorter\n * and a length-N path.\n */\nexport interface TraversePathsQuery {\n /** Start node id or qualified name (same resolution rules as {@link TraverseQuery.start}). */\n start: string;\n /** Default `\"outgoing\"`. */\n direction?: TraverseDirection;\n /** Edge types to follow; omitted/empty means every type. */\n edgeTypes?: readonly string[];\n /**\n * Inclusive upper bound on enumerated path LENGTH (hop count). MUST be a\n * non-negative integer. A `maxHops` of 0 yields no paths (every enumerated\n * path has length >= 1); callers that need the length-0 trivial path add it\n * themselves.\n */\n maxHops: number;\n /**\n * Inclusive LOWER bound on EMITTED path length (hop count). Defaults\n * to 1. The DFS still EXPLORES shorter prefixes to reach longer paths,\n * but only EMITS (and counts toward {@link maxPaths}) paths whose length\n * is in `[minHops, maxHops]` -- so an exact `*N` cap is not consumed by\n * the shorter prefixes (cursor Bugbot: 'Path cap ignores hop minimum').\n * MUST be a positive integer (>= 1) when present.\n */\n minHops?: number;\n /**\n * Safety cap on total enumerated paths. Defaults to\n * {@link DEFAULT_TRAVERSE_PATHS_MAX}. When the cap is reached, enumeration\n * STOPS and the result carries `truncated: true` so callers can detect that\n * the result is incomplete (e.g. to narrow the query or raise the cap).\n */\n maxPaths?: number;\n}\n\n/**\n * One enumerated path. The endpoint node is fully resolved; the full node-id\n * sequence lets callers reconstruct the path (issue #1650 acceptance).\n */\nexport interface TraversePathHit {\n nodeId: string;\n qualifiedName: string;\n name: string;\n label: string;\n filePath: string;\n /** Length of this path in hops (>= 1). */\n length: number;\n /** Full path as node ids, start-first (`length + 1` entries). */\n nodeIds: string[];\n /**\n * Edge type per hop, parallel to {@link nodeIds} (`length` entries). Two\n * distinct relationships can connect the same node pair with different\n * types (the edges table is UNIQUE on `(src, dst, type)`); exposing the\n * type per hop lets callers distinguish those otherwise-identical-node\n * paths (chatgpt-codex-connector P2: 'Include edge identity in path\n * hits').\n */\n edgeTypes: string[];\n /**\n * Per-hop edge endpoints, parallel to {@link nodeIds} (`length` entries).\n * Under `direction: \"both\"` antiparallel same-type edges (A->B and B->A)\n * yield distinct relationship-simple paths that share nodeIds + edgeTypes;\n * the src/dst per hop disambiguates which edge was traversed and in which\n * direction (chatgpt-codex-connector P2: 'Include edge endpoints in path\n * hits').\n */\n edgeEndpoints: Array<{ src: string; dst: string }>;\n}\n\nexport type TraversePathsResult =\n | { ok: true; hits: TraversePathHit[]; truncated: boolean }\n | ({ ok: false } & GraphStoreFailure)\n | { ok: false; code: \"unknown_start\" | \"ambiguous_start\" | \"invalid_query\" };\n\n/**\n * Structured node search. All filters are AND-combined; every filter\n * is optional so the bare query `{}` returns the whole graph (capped\n * by `limit`). Patterns use SQLite `LIKE` semantics — `%` matches any\n * run, `_` matches one character — applied case-insensitively via\n * `LIKE ... COLLATE NOCASE`. Patterns are parameter-bound, never\n * string-interpolated, so a `%`/`_` in user input cannot inject SQL.\n */\nexport interface SearchQuery {\n /** Filter by node label (the symbol kind, e.g. `\"function\"`). */\n label?: string;\n /** LIKE pattern on `nodes.name` (case-insensitive). */\n namePattern?: string;\n /** LIKE pattern on `files.path` (case-insensitive). */\n filePattern?: string;\n /**\n * Inclusive lower bound on total degree (in + out edge count).\n * Combined with {@link degreeMax} for a half-open? — no, inclusive\n * on both ends by convention since degree is an integer count, not\n * a span (rule 35 covers byte/time spans, not integer ranges).\n */\n degreeMin?: number;\n /** Inclusive upper bound on total degree. */\n degreeMax?: number;\n /**\n * Cap on returned rows. Default 100; clamped to [0, 1000]. A\n * `limit: 0` returns an empty `hits` array (rule 27 — guard the\n * slice/LIMIT against the zero case).\n */\n limit?: number;\n}\n\nexport interface SearchHit {\n nodeId: string;\n qualifiedName: string;\n name: string;\n label: string;\n filePath: string;\n /** Total in + out edge count for this node. */\n degree: number;\n}\n\nexport type SearchResult =\n | { ok: true; hits: SearchHit[] }\n | ({ ok: false } & GraphStoreFailure)\n | { ok: false; code: \"invalid_query\" };\n\n/** Aggregate counts over the whole graph — single round-trip. */\nexport interface SchemaStats {\n files: number;\n nodes: number;\n edges: number;\n /** Node count grouped by `label` (symbol kind). */\n nodesByLabel: Record<string, number>;\n /** Edge count grouped by `type`. */\n edgesByType: Record<string, number>;\n}\n\nexport type SchemaStatsResult =\n | { ok: true; stats: SchemaStats }\n | ({ ok: false } & GraphStoreFailure);\n\nexport interface DeadCodeHit {\n nodeId: string;\n qualifiedName: string;\n name: string;\n label: string;\n filePath: string;\n}\n\nexport type DeadCodeResult =\n | { ok: true; hits: DeadCodeHit[] }\n | ({ ok: false } & GraphStoreFailure);\n\n/**\n * Read a symbol's source span from disk. The store NEVER persists file\n * contents (privacy + DB size — issue #1552 design); `snippetFor`\n * resolves the node's `files.path` against {@link GraphStoreOptions.repoRoot}\n * and slices `[span_start, span_end)` from the on-disk bytes.\n */\nexport interface SnippetQuery {\n /**\n * Qualified name to resolve. Optional when `nodeId` is supplied — the\n * guard requires at least one of the two.\n */\n qualifiedName?: string;\n /**\n * Optional repo root override. When set, the snippet is read from this\n * root instead of the root captured at GraphStore.open() time, so a\n * caller that supplies its own repoRoot (e.g. semanticQuery) hydrates\n * snippets even when the store was opened without one (chatgpt-codex-\n * connector + cursor: 'Snippet hydration ignores query repoRoot').\n */\n repoRoot?: string;\n /**\n * Optional deterministic node id. When set, the lookup resolves by\n * `nodes.id` (unique) instead of `qualified_name`, so a hit whose\n * qualified name is duplicated across files still hydrates the exact\n * node's snippet instead of failing with `ambiguous_name`\n * (chatgpt-codex-connector P2: 'Hydrate snippets by node id as well').\n */\n nodeId?: string;\n /**\n * Optional lines of context to include before and after the span\n * (default 0 — exact span only). Context is line-aligned: the slice\n * expands to the nearest line boundary at each end.\n */\n contextLines?: number;\n}\n\nexport interface SnippetSuccess {\n ok: true;\n qualifiedName: string;\n filePath: string;\n /** Absolute path the bytes were read from (`repoRoot/files.path`). */\n absolutePath: string;\n startByte: number;\n endByte: number;\n /** The decoded source slice (UTF-8). */\n text: string;\n lang: string;\n}\n\nexport type SnippetFailureCode =\n | \"not_found\"\n | \"ambiguous_name\"\n | \"repo_root_unset\"\n | \"read_failed\"\n | \"invalid_query\"\n | \"store_closed\"\n // DB-level failures surface from the node-lookup catch path so the\n // typed contract matches every code classifyReadError can return\n // (cursor Bugbot: 'snippetFor omits store failure codes').\n | \"db_locked\"\n | \"db_corrupt\"\n | \"db_error\";\n\nexport type SnippetResult = SnippetSuccess | { ok: false; code: SnippetFailureCode };\n\n// ──────────────────────────────────────────────────────────────────────────\n// KV / list read primitives — tagged failures (rule 22). readMeta,\n// readFileHashes, and readCoChanges previously caught every error and\n// returned the empty value (null / new Map() / []), making a SQLITE_BUSY\n// indistinguishable from \"key absent\" / \"empty index\" / \"no co-change\n// edges\". The reindex executor's prune + head-advance decisions depend on\n// readFileHashes, so conflating error with empty could skip pruning while\n// advancing head, or prune against a falsely-empty set. These result types\n// force callers to handle the two cases distinctly (cursor Bugbot HIGH:\n// 'readFileHashes conflates error with empty'; 'readCoChanges swallows\n// store errors'; 'readMeta conflates absent key with db failure').\n// ──────────────────────────────────────────────────────────────────────────\n\n/** Result of readMeta — `{ ok: true; value: null }` is a genuinely absent key;\n * a tagged failure is a backend error (rule 22). */\nexport type ReadMetaResult =\n | { ok: true; value: string | null }\n | ({ ok: false } & GraphStoreFailure);\n\n/** Result of readFileHashes — `{ ok: true; hashes: <empty> }` is an empty\n * index; a tagged failure is a backend error (rule 22). */\nexport type ReadFileHashesResult =\n | { ok: true; hashes: Map<string, string> }\n | ({ ok: false } & GraphStoreFailure);\n\n/** A co-change edge row returned by readCoChanges. */\nexport interface ReadCoChangeEdge {\n readonly fileA: string;\n readonly fileB: string;\n readonly support: number;\n readonly confidence: number;\n}\n\n/** Result of readCoChanges — `{ ok: true; edges: [] }` means no edges\n * recorded; a tagged failure is a backend error (rule 22). */\nexport type ReadCoChangesResult =\n | { ok: true; edges: readonly ReadCoChangeEdge[] }\n | ({ ok: false } & GraphStoreFailure);\n\n// ──────────────────────────────────────────────────────────────────────────\n// PR2 dead-code exclusion — explicit named constant (rule 53 analog).\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * The single source of truth for what `deadCode()` EXCLUDES from the\n * candidate set. Anything matched by these patterns or flags is treated\n * as a non-dead surface even when it has zero inbound call/usage edges.\n *\n * This constant exists so the exclusion criteria are NAMED, DOCUMENTED,\n * and auditable in one place — not scattered across ad-hoc `WHERE`\n * clauses (rule 53 analog). Adding a new exclusion category means\n * extending this constant plus the matching `node_attributes` column;\n * the query then picks both up automatically.\n *\n * Categories:\n * - {@link INBOUND_USAGE_EDGE_TYPES} — an inbound edge of any of these\n * types disqualifies a node from being dead.\n * - {@link TEST_PATH_PATTERNS} — a node whose `files.path` matches is\n * in a test file; tests can call into private code without the\n * production graph seeing the edge.\n * - {@link ENTRY_POINT_PATH_PATTERNS} — process entry points (index,\n * main, cli, bin/); these are reachable from outside the graph.\n * - {@link EXCLUDED_ATTRIBUTE_FLAGS} — per-node flags stored in\n * `node_attributes` (set at write time from FileIR.exports /\n * FileIR.routes); `is_exported` and `is_route_handler`.\n */\nexport const DEAD_CODE_EXCLUSION = {\n /**\n * Edge types that — when pointing INTO a node — count as \"this node\n * is used\". Mirrors the issue's `CALLS/USAGE` wording plus the four\n * call-flavored edge types in the wider coding-graph vocabulary.\n */\n INBOUND_USAGE_EDGE_TYPES: [\n \"CALLS\",\n \"USES_TYPE\",\n \"ASYNC_CALLS\",\n \"HTTP_CALLS\",\n \"DATA_FLOWS\",\n ] as const,\n /**\n * File-path regexes identifying test files. Matched against\n * `files.path` (repo-relative, forward slashes).\n */\n TEST_PATH_PATTERNS: [\n /\\.test\\.[cm]?[tj]sx?$/,\n /\\.spec\\.[cm]?[tj]sx?$/,\n /(^|\\/)__tests__\\//,\n /(^|\\/)__mocks__\\//,\n /(^|\\/)tests?\\//,\n /(^|\\/)test\\//,\n ] as const,\n /**\n * File-path regexes identifying entry points (reachable from\n * outside the indexed code). Matched against `files.path`. Kept\n * deliberately narrow — `server.ts` / `app.ts` are intentionally\n * NOT treated as entry points because they are common module\n * names that may also contain dead helpers. The conservative\n * direction is to report a symbol as dead rather than hide it.\n */\n ENTRY_POINT_PATH_PATTERNS: [\n /(^|\\/)index\\.[cm]?[tj]sx?$/,\n /(^|\\/)main\\.[cm]?[tj]sx?$/,\n /(^|\\/)cli\\.[cm]?[tj]sx?$/,\n /(^|\\/)bin\\//,\n /(^|\\/)src\\/bin\\//,\n ] as const,\n /**\n * Columns on `node_attributes` whose value being `1` excludes the\n * node. Names mirror the schema so a future column add is a one-line\n * constant extension + a query clause (no scattered edits).\n */\n EXCLUDED_ATTRIBUTE_FLAGS: [\"is_exported\", \"is_route_handler\"] as const,\n} as const;\n\n/**\n * @returns true iff `filePath` matches any pattern in\n * {@link DEAD_CODE_EXCLUSION.TEST_PATH_PATTERNS} or\n * {@link DEAD_CODE_EXCLUSION.ENTRY_POINT_PATH_PATTERNS}.\n */\nfunction isExcludedByPath(filePath: string): boolean {\n for (const re of DEAD_CODE_EXCLUSION.TEST_PATH_PATTERNS) {\n if (re.test(filePath)) return true;\n }\n for (const re of DEAD_CODE_EXCLUSION.ENTRY_POINT_PATH_PATTERNS) {\n if (re.test(filePath)) return true;\n }\n return false;\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Internal: SQL variable-limit chunking (PR1 pattern — keep under 32766).\n// ──────────────────────────────────────────────────────────────────────────\n\n/** SQLite variable bind limit for the bundled better-sqlite3 native build. */\nconst SQLITE_VARIABLE_LIMIT = 32_766;\n\n/**\n * Run a parameterized `IN (?, ?, …)` query in chunks small enough to\n * stay under SQLite's variable bind limit. The caller provides the\n * statement prefix/suffix with a single `%PH%` placeholder where the\n * `?,?,…` list goes; this helper substitutes the chunked placeholders\n * and runs `.run(...)` per chunk, aggregating the returned rows.\n *\n * Mirrors the chunking pattern PR1 already uses inside\n * `pruneFileNodes` (the FTS rowid deletes) and `upsertFileEdges` (the\n * stale-edge tuple deletes). The reviews already hardened this class\n * against `too many SQL variables` failures.\n */\nfunction chunkedInQuery(\n db: BetterSqlite3Database,\n sqlTemplate: string,\n params: readonly (string | number)[],\n): unknown[] {\n const out: unknown[] = [];\n if (params.length === 0) return out;\n for (let i = 0; i < params.length; i += SQLITE_VARIABLE_LIMIT) {\n const chunk = params.slice(i, i + SQLITE_VARIABLE_LIMIT);\n const placeholders = chunk.map(() => \"?\").join(\", \");\n const sql = sqlTemplate.replace(\"%PH%\", placeholders);\n const rows = db.prepare(sql).all(...chunk);\n if (Array.isArray(rows)) {\n for (const r of rows) out.push(r);\n }\n }\n return out;\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Internal: write-queue (rule 40).\n// ──────────────────────────────────────────────────────────────────────────\n\n/**\n * Per-instance FIFO. A second call to `upsertFileBatch` enqueues and the\n * previous call's promise is awaited before the new one runs. `schedule()`\n * returns immediately with a promise — callers can `await` it or fire-and-\n * forget. A throwing handler propagates the rejection AND drains the queue\n * so the next enqueued call doesn't deadlock (rejection-recovering).\n */\nclass WriteQueue {\n private tail: Promise<unknown> = Promise.resolve();\n\n schedule<T>(run: () => Promise<T>): Promise<T> {\n const next = this.tail.then(run, run);\n // Swallow the tail's settlement for callers waiting on `next` only.\n // The actual rejection still surfaces from `next` itself.\n this.tail = next.catch(() => undefined);\n return next as Promise<T>;\n }\n\n /** Test seam: wait until the queue has drained. */\n async drain(): Promise<void> {\n await this.tail;\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Store.\n// ──────────────────────────────────────────────────────────────────────────\n\nexport interface GraphStoreOptions {\n /** Absolute path to the SQLite file. The caller resolves the namespace. */\n dbPath: string;\n /**\n * Optional absolute path to the repo root. When set, `snippetFor()`\n * resolves a node's repo-relative `files.path` against this root to\n * read its source span from disk. When unset, `snippetFor()` returns\n * `code: \"repo_root_unset\"` for every call. The store NEVER persists\n * file contents (privacy + DB size — issue #1552 design); this is\n * the only path the read-side uses.\n */\n repoRoot?: string;\n}\n\n/**\n * One DB per instance. The store does NOT mutate its path or close the\n * handle until {@link close} is called explicitly (rule 11).\n */\nexport class GraphStore {\n private readonly db: BetterSqlite3Database;\n private readonly queue = new WriteQueue();\n private readonly repoRoot: string | undefined;\n private closed = false;\n private closing = false;\n /**\n * True once close() has begun (closing) or completed (closed). Public so\n * callers that hold a GraphStore reference can return the documented\n * 'store_closed' degradation code instead of treating a closed store as\n * an empty graph (cursor Bugbot: 'Closed store reports success'). The\n * read primitives already short-circuit on this internally; this getter\n * lets the semantic entry points do the same BEFORE calling a read that\n * would return [].\n */\n get isClosed(): boolean {\n return this.closed || this.closing;\n }\n // Shared drain-and-close promise so a second close() called while the\n // first is still draining awaits the same completion instead of\n // resolving early (chatgpt-codex-connector P2: 'Wait for an\n // in-progress close').\n private closePromise: Promise<void> | undefined;\n\n private constructor(db: BetterSqlite3Database, repoRoot: string | undefined) {\n this.db = db;\n // Validate at open() so the failure mode is a thrown, name-specific\n // error at construction — never a silent `code: \"repo_root_unset\"`\n // cascade on the first snippetFor() call after a long ingest. The\n // caller may still pass `undefined` (the PR1 default); they just\n // cannot pass a relative path that would silently slice the wrong\n // file (rule 11 — no path assembly at call sites).\n this.repoRoot = repoRoot;\n }\n\n /**\n * Open a store at the given dbPath. Creates parent directories and\n * applies the schema (idempotent — also handles upgrade). The dbPath\n * does no namespace resolution.\n */\n static async open(options: GraphStoreOptions): Promise<GraphStore> {\n const { dbPath, repoRoot } = options;\n if (!path.isAbsolute(dbPath)) {\n throw new Error(\n `graph-store: dbPath must be absolute; received ${JSON.stringify(dbPath)}`,\n );\n }\n // Validate repoRoot up-front (rule 11). When provided it MUST be\n // absolute — a relative repoRoot would silently resolve against\n // the process CWD and `snippetFor()` would slice the wrong file\n // (or a non-existent one) without a clear failure shape. The\n // PR1 baseline keeps `repoRoot` optional so existing callers that\n // do not need snippets continue to open() with just `{ dbPath }`.\n if (repoRoot !== undefined && !path.isAbsolute(repoRoot)) {\n throw new Error(\n `graph-store: repoRoot must be absolute when provided; received ${JSON.stringify(repoRoot)}`,\n );\n }\n await mkdir(path.dirname(dbPath), { recursive: true });\n const db = openBetterSqlite3(dbPath);\n // Pragmas verbatim from packages/remnic-core/src/lcm/schema.ts — the\n // shared in-repo pattern. Do not tune per-store (rule 23).\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"busy_timeout = 5000\");\n db.pragma(\"synchronous = NORMAL\");\n // SQLite defaults to foreign_keys=OFF per-connection. The graph's\n // `edges` table relies on `ON DELETE CASCADE` from `nodes(id)` to\n // drop owned edges when a file's prior nodes are pruned; without\n // this pragma the cascade silently no-ops and edges accumulate as\n // orphans (cursor + codex review). PR2's `node_attributes` table\n // also relies on this cascade so attribute rows die with their node.\n db.pragma(\"foreign_keys = ON\");\n applyCodingGraphSchema(db);\n return new GraphStore(db, repoRoot);\n }\n\n /**\n * The current schema_version row. Test seam — never expires, never\n * cached so migrations land without a restart.\n */\n schemaVersion(): number {\n return readSchemaVersion(this.db);\n }\n\n /**\n * Ingest a batch of IR files atomically. One transaction wraps every\n * file's delete + insert; if any file throws, the whole batch rolls\n * back (rule 34 — never partial-write a coding graph).\n *\n * Re-ingesting the same IR is a no-op once the rows are written\n * (idempotency — node ids are deterministic so the second pass collides\n * on PRIMARY KEY).\n *\n * Two-pass ordering: pass 1 upserts every file's nodes (so FTS stays\n * in sync and cross-file edge targets exist by the time pass 2 runs),\n * pass 2 resolves edges against the full batch's node map and deletes\n * prior edges owned by these files so changed confidence/provenance\n * values overwrite (chatgpt-codex-connector P1 + cursor medium + PR1\n * design anchor in graph-schema).\n *\n * Tagging:\n * - `{ok:true, results}` — every file's counts.\n * - `{ok:false, code:\"db_locked\"}` — busy_timeout elapsed; caller may\n * retry. NOT a thrown error so the agent can degrade gracefully.\n * - `{ok:false, code:\"db_corrupt\"}` — SQLite reported\n * `database disk image is malformed`; the caller must surface and\n * stop trusting this DB.\n */\n async upsertFileBatch(\n files: StoreFileIR[],\n /**\n * Optional paths to delete in the SAME transaction as the upsert\n * (issue #1553 — the reindex executor prunes deleted files atomically\n * with the changed-files upsert so a mid-batch failure cannot leave\n * the graph with committed deletions but no re-ingested replacements).\n * Cascades to nodes + edges + node_attributes via the schema's\n * `ON DELETE CASCADE`. Empty/omitted = no deletions.\n */\n deletePaths: readonly string[] = [],\n ): Promise<UpsertBatchResult> {\n if (this.closed || this.closing) {\n return {\n ok: false,\n code: \"store_closed\",\n };\n }\n return this.queue.schedule(() => this.runUpsert(files, deletePaths));\n }\n\n /**\n * Upsert standalone edges whose endpoints are resolved from the FULL\n * database (not just a per-file batch). Used by the codegraph\n * ingest_traces surface (issue #1554) to persist runtime HTTP_CALLS\n * observations as edges with `provenance: \"trace\"` — upgrading\n * confidence on existing edges and inserting new ones.\n *\n * Endpoint resolution: when an edge carries `srcNodeId` / `dstNodeId`\n * (issue #1677 — the SIMILAR_TO pipeline populates them from\n * content-derived node ids), the endpoint is resolved by `nodes.id`\n * (unique primary key), so an edge between two symbols that share a\n * qualified name across files is persisted rather than dropped as\n * ambiguous. Edges WITHOUT node ids fall back to qualified_name\n * resolution via the global `resolveNodeId` (unambiguous single-match\n * policy). Edges whose endpoints do not resolve (missing node id row OR\n * an ambiguous/dangling qualified name) are skipped (and counted in\n * `skipped`) rather than attached to the wrong node — the dangling-edge\n * policy from `upsertFileBatch` applies.\n *\n * Serialized on the store's write queue like `upsertFileBatch` so a\n * concurrent file-batch upsert and a trace upsert cannot interleave\n * (rule 40).\n */\n async upsertEdges(\n edges: readonly EdgeIR[],\n ): Promise<UpsertEdgesResult> {\n if (this.closed || this.closing) {\n return { ok: false, code: \"store_closed\" };\n }\n return this.queue.schedule(() => this.runUpsertEdges(edges));\n }\n\n /**\n * Retire stale LSP-provenance edges for a file (issue #1895).\n *\n * The LSP resolution pass re-derives edges from the CURRENT source on each\n * run. After writing the new `lsp` edges for a file, this method deletes\n * prior `lsp`-provenance edges owned by that file's nodes whose\n * `(src, dst, type)` key is NOT in the asserted set. This is the LSP\n * layer's side of the provenance-lifecycle contract: each layer owns its\n * own stale-edge retirement (#1894 established that reindex's heuristic\n * scope never touches `lsp` rows).\n *\n * Heuristic, trace, and semantic edges are never touched.\n *\n * @returns the number of retired edges.\n */\n reconcileLspEdges(\n filePath: string,\n assertedEdges: ReadonlyArray<{\n srcQualifiedName: string;\n dstQualifiedName: string;\n type: string;\n }>,\n ): number {\n if (this.closed) return 0;\n try {\n // Resolve the file and its nodes.\n const fileRow = expectRow<{ id: number }>(\n this.db.prepare(\"SELECT id FROM files WHERE path = ?\").get(filePath),\n [\"id\"],\n );\n if (!fileRow) return 0;\n const nodes = expectRows<{ id: string; qualified_name: string }>(\n this.db\n .prepare(\"SELECT id, qualified_name FROM nodes WHERE file_id = ?\")\n .all(fileRow.id),\n [\"id\", \"qualified_name\"],\n );\n if (nodes.length === 0) return 0;\n\n // Build srcQualifiedName → nodeId for this file's nodes. Only\n // include names that appear exactly once (same conservative\n // ambiguity policy as upsertFileEdges — cursor review on #1914).\n const nameCount = new Map<string, number>();\n for (const n of nodes) nameCount.set(n.qualified_name, (nameCount.get(n.qualified_name) ?? 0) + 1);\n const srcMap = new Map<string, string>();\n for (const n of nodes) {\n if (nameCount.get(n.qualified_name) === 1) srcMap.set(n.qualified_name, n.id);\n }\n const nodeIds = nodes.map((n) => n.id);\n\n // Build the asserted key set (resolved to node IDs).\n const assertedKeys = new Set<string>();\n for (const e of assertedEdges) {\n const srcId = srcMap.get(e.srcQualifiedName);\n if (!srcId) continue;\n const dstId = resolveNodeId(e.dstQualifiedName, new Map(), this.db);\n if (!dstId) continue;\n assertedKeys.add(`${srcId}\\u0000${dstId}\\u0000${e.type}`);\n }\n\n // Find prior lsp edges owned by this file's nodes.\n const placeholders = nodeIds.map(() => \"?\").join(\", \");\n const priorEdges = expectRows<{ src: string; dst: string; type: string }>(\n this.db\n .prepare(\n `SELECT src, dst, type FROM edges\n WHERE src IN (${placeholders}) AND provenance = 'lsp'`,\n )\n .all(...nodeIds),\n [\"src\", \"dst\", \"type\"],\n );\n\n // Delete those NOT in the asserted set.\n let deleted = 0;\n const toDelete: Array<[string, string, string]> = [];\n for (const e of priorEdges) {\n const key = `${e.src}\\u0000${e.dst}\\u0000${e.type}`;\n if (!assertedKeys.has(key)) toDelete.push([e.src, e.dst, e.type]);\n }\n if (toDelete.length > 0) {\n const SQLITE_VARIABLE_LIMIT = 32_766;\n const PARAMS_PER_TUPLE = 3;\n const MAX_TUPLES = Math.floor(SQLITE_VARIABLE_LIMIT / PARAMS_PER_TUPLE);\n for (let i = 0; i < toDelete.length; i += MAX_TUPLES) {\n const chunk = toDelete.slice(i, i + MAX_TUPLES);\n const ph = chunk.map(() => \"(?, ?, ?)\").join(\", \");\n const r = this.db\n .prepare(`DELETE FROM edges WHERE (src, dst, type) IN (${ph})`)\n .run(...chunk.flat());\n deleted += r.changes;\n }\n }\n return deleted;\n } catch (error) {\n logWriteFailure(error);\n return 0;\n }\n }\n\n /** Wait for pending writes to drain — test seam. */\n async drain(): Promise<void> {\n await this.queue.drain();\n }\n // ──────────────────────────────────────────────────────────────────────\n // PR3 (issue #1553): meta-table + file-management methods for the\n // incremental reindex pipeline.\n // ──────────────────────────────────────────────────────────────────────\n\n /**\n * Read a value from the `meta` table. Returns `null` when the key is\n * absent. Synchronous (like the other read primitives) so the reindex\n * planner can read `last_indexed_head` without an await.\n */\n readMeta(key: string): ReadMetaResult {\n if (this.closed) return { ok: false, code: \"store_closed\" };\n try {\n const row = expectRow<{ value: string }>(\n this.db.prepare(\"SELECT value FROM meta WHERE key = ?\").get(key),\n [\"value\"],\n );\n return { ok: true, value: row ? row.value : null };\n } catch (error) {\n logWriteFailure(error);\n return classifyReadError(error);\n }\n }\n\n /**\n * Write a key/value pair to the `meta` table. Synchronous — runs in its\n * own implicit transaction. The reindex executor calls this AFTER\n * `upsertFileBatch` resolves (rule 25: head/state updates only after\n * the data transaction commits). A crash between the two leaves the old\n * head, and the next run re-ingests idempotently (deterministic node ids).\n */\n writeMeta(key: string, value: string): void {\n if (this.closed) return;\n this.db\n .prepare(\"INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)\")\n .run(key, value);\n }\n\n /**\n * Read every file row's path → content_hash. Used by hash_scan mode\n * to detect content drift without a reachable base commit (issue #1553).\n */\n readFileHashes(): ReadFileHashesResult {\n if (this.closed) return { ok: false, code: \"store_closed\" };\n try {\n const rows = expectRows<{ path: string; content_hash: string }>(\n this.db.prepare(\"SELECT path, content_hash FROM files\").all(),\n [\"path\", \"content_hash\"],\n );\n const out = new Map<string, string>();\n for (const r of rows) out.set(r.path, r.content_hash);\n return { ok: true, hashes: out };\n } catch (error) {\n logWriteFailure(error);\n return classifyReadError(error);\n }\n }\n\n /**\n * Drop file rows by path, cascading to their nodes + edges +\n * node_attributes (the schema's `ON DELETE CASCADE` from `files(id)`\n * handles the cascade — `foreign_keys = ON` is set in `open()`).\n * Used by the reindex executor to prune deleted files.\n *\n * Paths are chunked under the SQLite variable limit (rule 23 pattern).\n */\n async dropFiles(paths: readonly string[]): Promise<void> {\n if (this.closed || this.closing || paths.length === 0) return;\n await this.queue.schedule(async () => {\n this.runChunkedDelete(\n \"DELETE FROM files WHERE path IN (%PH%)\",\n paths,\n );\n });\n }\n\n /**\n * Chunk a parameterized DELETE-with-IN-list under SQLite's variable\n * bind limit. Mirrors the chunking pattern used by `runChunkedUpdate`\n * and the stale-edge deletes.\n */\n private runChunkedDelete(sqlTemplate: string, params: readonly string[]): void {\n if (params.length === 0) return;\n for (let i = 0; i < params.length; i += SQLITE_VARIABLE_LIMIT) {\n const chunk = params.slice(i, i + SQLITE_VARIABLE_LIMIT);\n const placeholders = chunk.map(() => \"?\").join(\", \");\n this.db.prepare(sqlTemplate.replace(\"%PH%\", placeholders)).run(...chunk);\n }\n }\n /**\n * PR3 (issue #1553): upsert co-change edges into the `co_changes`\n * table. Clears existing edges then inserts the new set in one\n * transaction (idempotent — re-running on unchanged history produces\n * the same table). Serialized through the write queue.\n */\n /**\n * PR3 (issue #1553): upsert co-change edges into the `co_changes`\n * table. Clears existing edges then inserts the new set in one\n * transaction (idempotent — re-running on unchanged history produces\n * the same table). Serialized through the write queue.\n *\n * Returns `{ ok: false, code: \"store_closed\" }` when the store is\n * closed/closing so the caller does NOT believe mining succeeded\n * while nothing was persisted (cursor Bugbot: 'Co-change store\n * reports false success').\n */\n async upsertCoChanges(edges: readonly {\n readonly fileA: string;\n readonly fileB: string;\n readonly support: number;\n readonly confidence: number;\n }[]): Promise<\n | { ok: true }\n | { ok: false; code: \"store_closed\" }\n | { ok: false; code: \"db_error\" }\n > {\n if (this.closed || this.closing) {\n return { ok: false, code: \"store_closed\" };\n }\n try {\n await this.queue.schedule(async () => {\n const tx = this.db.transaction(() => {\n this.db.exec(\"DELETE FROM co_changes\");\n const insert = this.db.prepare(\n `INSERT INTO co_changes (file_a, file_b, support, confidence)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(file_a, file_b) DO UPDATE SET\n support = excluded.support,\n confidence = excluded.confidence`,\n );\n for (const e of edges) {\n insert.run(e.fileA, e.fileB, e.support, e.confidence);\n }\n });\n tx();\n });\n return { ok: true };\n } catch (error) {\n // A locked/corrupt DB would otherwise throw out of the queued\n // callback and crash the caller even though the public type only\n // advertises tagged failures. Surface a tagged db_error instead\n // (chatgpt-codex-connector: 'Return a tagged co-change store\n // failure').\n logWriteFailure(error);\n return { ok: false, code: \"db_error\" };\n }\n }\n\n /**\n * PR3 (issue #1553): read co-change edges for a file. Returns edges\n * where the file is either `file_a` or `file_b`. Synchronous read.\n */\n readCoChanges(filePath: string): ReadCoChangesResult {\n if (this.closed) return { ok: false, code: \"store_closed\" };\n try {\n const rows = expectRows<{\n file_a: string;\n file_b: string;\n support: number;\n confidence: number;\n }>(\n this.db\n .prepare(\n `SELECT file_a, file_b, support, confidence\n FROM co_changes\n WHERE file_a = ? OR file_b = ?\n ORDER BY confidence DESC, file_a ASC, file_b ASC`,\n )\n .all(filePath, filePath),\n [\"file_a\", \"file_b\", \"support\", \"confidence\"],\n );\n return {\n ok: true,\n edges: rows.map((r) => ({\n fileA: r.file_a,\n fileB: r.file_b,\n support: r.support,\n confidence: r.confidence,\n })),\n };\n } catch (error) {\n logWriteFailure(error);\n return classifyReadError(error);\n }\n }\n\n /**\n * Close the SQLite handle after draining the write queue. A batch\n * that has already been scheduled on the queue would otherwise run\n * against a closed DB and surface as `db_corrupt` — the caller\n * would stop trusting the store for unrelated reasons. Drain first,\n * then close (cursor Bugbot #09be5784).\n */\n async close(): Promise<void> {\n if (this.closed) return;\n // A concurrent close() is already draining. Return the shared\n // promise so this caller's `await store.close()` actually waits\n // for the drain to finish and the SQLite handle to close — the\n // pre-fix early `return` resolved immediately, so a caller that\n // treats close() as a flush barrier could delete/reopen the DB\n // while writes were still in flight (chatgpt-codex-connector P2:\n // 'Wait for an in-progress close').\n if (this.closing) return this.closePromise;\n // Block NEW writes before draining so a concurrent upsertFileBatch\n // cannot schedule a write that runs after this drain's await captured\n // the old tail. Without this flag, close() drains the queue snapshot,\n // closes the handle, and the late-scheduled write hits a closed DB\n // (chatgpt-codex-connector P2: 'Block new writes before draining').\n this.closing = true;\n this.closePromise = this.finishClose();\n return this.closePromise;\n }\n\n /** Drain queued writes then close the SQLite handle exactly once. */\n private async finishClose(): Promise<void> {\n await this.queue.drain();\n this.closed = true;\n this.db.close();\n }\n\n // ────────────── private ──────────────\n\n private async runUpsert(\n files: StoreFileIR[],\n deletePaths: readonly string[] = [],\n ): Promise<UpsertBatchResult> {\n // Guard: duplicate paths in one batch silently corrupt the edge\n // pass — pass 2 deletes the first entry's edges when the second\n // entry's edge pass runs against the same file row. Fail loud so\n // the caller fixes the input (cursor Bugbot: 'Duplicate paths\n // corrupt edge pass').\n const seenPaths = new Set<string>();\n for (const ir of files) {\n // Canonical-path check BEFORE the duplicate check: a caller that\n // passes the same repo file as `./src/a.ts` in one ingest and\n // `src/a.ts` in another (or uses backslashes / an absolute path)\n // would persist two distinct files rows + node-id hashes and\n // leave duplicate/stale symbols the later canonical ingest\n // cannot match or prune. The FileIR contract requires\n // repo-relative forward-slash paths; reject the violation at the\n // store boundary rather than silently normalizing\n // (chatgpt-codex-connector P2: 'Reject non-canonical file paths\n // before persisting').\n assertCanonicalFilePath(ir.path);\n if (seenPaths.has(ir.path)) {\n throw new Error(\n `graph-store: duplicate path '${ir.path}' in batch — each FileIR must have a unique path`,\n );\n }\n seenPaths.add(ir.path);\n // `symbols` is a REQUIRED FileIR contract field (non-optional\n // `readonly symbols: readonly SymbolIR[]`). Runtime null can\n // still arrive via JSON deserialization or a malformed parser\n // result; without this guard the `?? []` fallback made a\n // missing/null field indistinguishable from an explicit empty\n // array, so the prune step silently wiped every existing\n // node/edge for the path while the batch returned ok. Reject\n // the contract violation instead of clearing the file\n // (chatgpt-codex-connector P2: 'Reject missing symbols instead\n // of pruning the file').\n const symbolsField = ir.symbols as unknown;\n if (!Array.isArray(symbolsField)) {\n throw new Error(\n `graph-store: file '${ir.path}' symbols must be an array (FileIR contract requires it); received ${\n symbolsField === null ? \"null\" : typeof symbolsField\n } — refusing to ingest to avoid wiping existing nodes`,\n );\n }\n // Span check: a malformed parser or JSON caller can emit\n // startByte > endByte (or non-integer / negative spans); the\n // values are bound directly into span_start/span_end and PR2\n // snippet/search consumers will trust them as half-open byte\n // offsets. Reject before insertion so bad IR cannot corrupt\n // graph metadata (chatgpt-codex-connector P2: 'Reject invalid\n // symbol spans before storing nodes').\n for (const sym of symbolsField) {\n assertValidSymbolSpan(sym, ir.path);\n }\n // Attribute arrays: `exports` and `routes` are optional, but\n // when present MUST be arrays. A malformed non-array (e.g. a\n // JSON caller passing `exports: \"publicApi\"`) is iterable as\n // characters whose entries have no `.name`, so the per-flag\n // rebuild in upsertFileAttributes would compute an empty set\n // and then WIPE every is_exported / is_route_handler flag for\n // the file — turning a single bad re-ingest into silent\n // dead-code misclassification. Reject at the boundary like the\n // symbols check above (chatgpt-codex-connector P2: 'Validate\n // attribute arrays before clearing flags').\n if (ir.exports != null) {\n if (!Array.isArray(ir.exports)) {\n throw new Error(\n `graph-store: file '${ir.path}' exports must be an array when present; received ${\n ir.exports === null ? \"null\" : typeof ir.exports\n } — refusing to ingest to avoid wiping existing flags`,\n );\n }\n // Each entry must carry a non-empty string `name`; a malformed\n // entry (e.g. `{ name: 42 }`) is silently skipped by the\n // per-flag rebuild, so it contributes nothing while the wipe\n // still clears every is_exported flag. Reject the whole batch\n // like the symbols check (chatgpt-codex-connector P2: 'Reject\n // malformed attribute entries before clearing flags').\n for (const ex of ir.exports) {\n if (!ex || typeof ex.name !== \"string\" || ex.name.length === 0) {\n throw new Error(\n `graph-store: file '${ir.path}' has a malformed export entry — expected { name: string (non-empty) }; refusing to ingest to avoid wiping existing flags`,\n );\n }\n }\n }\n if (ir.routes != null) {\n if (!Array.isArray(ir.routes)) {\n throw new Error(\n `graph-store: file '${ir.path}' routes must be an array when present; received ${\n ir.routes === null ? \"null\" : typeof ir.routes\n } — refusing to ingest to avoid wiping existing flags`,\n );\n }\n for (const r of ir.routes) {\n if (\n !r ||\n typeof r.handlerQualifiedName !== \"string\" ||\n r.handlerQualifiedName.length === 0\n ) {\n throw new Error(\n `graph-store: file '${ir.path}' has a malformed route entry — expected { handlerQualifiedName: string (non-empty) }; refusing to ingest to avoid wiping existing flags`,\n );\n }\n }\n }\n }\n try {\n const results: UpsertResult[] = [];\n\n // Single transaction for the whole batch — atomic, faster than\n // per-file BEGIN/COMMIT, and rule 34 mandates \"never partial-write\n // a coding graph\". Two passes: pass 1 upserts every file's nodes\n // (so FTS stays in sync and cross-file edge targets exist by the\n // time pass 2 runs), pass 2 resolves edges against the full\n // batch's node map and deletes prior edges owned by these files\n // so changed confidence/provenance values overwrite. The two\n // passes together make the write pipeline order-independent for\n // cross-file edges (chatgpt-codex-connector P1/P2).\n const tx = this.db.transaction((irs: StoreFileIR[]) => {\n // Pass 0 (issue #1553): prune deleted-file rows in the SAME\n // transaction as the upsert so a failure rolls both back\n // atomically (cursor Bugbot: 'Deletes commit before ingest\n // fails'). Cascades to nodes + edges + node_attributes.\n if (deletePaths.length > 0) {\n for (let i = 0; i < deletePaths.length; i += SQLITE_VARIABLE_LIMIT) {\n const chunk = deletePaths.slice(i, i + SQLITE_VARIABLE_LIMIT);\n const placeholders = chunk.map(() => \"?\").join(\", \");\n this.db\n .prepare(\"DELETE FROM files WHERE path IN (%PH%)\".replace(\"%PH%\", placeholders))\n .run(...chunk);\n }\n }\n // Pass 1a: upsert every file's nodes and collect the per-file\n // prune sets WITHOUT deleting yet. `upsertFileNodes` returns\n // the result plus the node ids it wants to prune; the actual\n // prune (and the dangling-edge count that gates it) is deferred\n // to pass 1b so all files in the batch share one batch-wide\n // view of what is being pruned.\n const pending: { result: UpsertResult; prunedNodeIds: string[] }[] = [];\n for (const ir of irs) {\n const { result, prunedNodeIds } = this.upsertFileNodes(ir);\n pending.push({ result, prunedNodeIds });\n results.push(result);\n }\n // Pass 1b: count + delete dangling edges per file. The src\n // exclusion uses the BATCH-WIDE pruned set, not just this\n // file's, so an edge whose both ends are pruned in different\n // files is never reported as \"dangling\" — it is\n // cascade-deleted, and the reported loss no longer depends on\n // which file the loop visits first\n // (chatgpt-codex-connector P2: 'Count dangling edges against\n // the whole batch').\n const batchPrunedIds: string[] = [];\n for (const { prunedNodeIds } of pending) {\n for (const id of prunedNodeIds) batchPrunedIds.push(id);\n }\n for (const { result, prunedNodeIds } of pending) {\n this.pruneFileNodes(result, prunedNodeIds, batchPrunedIds);\n }\n // Pass 2: every file's edges. Resolves against the full DB\n // (which already contains every node from this batch plus\n // every node from prior batches).\n for (let i = 0; i < irs.length; i += 1) {\n const ir = irs[i]!;\n const result = results[i]!;\n this.upsertFileEdges(ir, result);\n }\n // Pass 3 (PR2): every file's node_attributes rows\n // (`is_exported`, `is_route_handler`). Derived from the IR's\n // optional `exports` and `routes` arrays. Runs after the prune\n // so attribute rows for nodes that survived into this batch\n // are written against the final node set. Cascade-delete on\n // `nodes(id)` already cleaned up rows for pruned nodes during\n // pass 1b; this pass only inserts new / updates existing rows\n // for surviving nodes.\n for (let i = 0; i < irs.length; i += 1) {\n const ir = irs[i]!;\n const result = results[i]!;\n this.upsertFileAttributes(ir, result);\n }\n });\n tx(files);\n return { ok: true, results };\n } catch (error) {\n logWriteFailure(error);\n return classifyError(error);\n }\n }\n\n /**\n * Standalone-edge upsert body (runs under the write queue). Resolves\n * both endpoints from the full DB via the unambiguous single-match\n * `resolveNodeId` fallback, then upserts each edge with the same\n * ON CONFLICT(src,dst,type) policy as the file-batch path. Edges whose\n * src or dst do not resolve to exactly one node are skipped (counted\n * in `skipped`) per the dangling-edge policy.\n */\n private async runUpsertEdges(\n edges: readonly EdgeIR[],\n ): Promise<UpsertEdgesResult> {\n const emptyBatch: Map<string, string> = new Map();\n const insertEdge = this.db.prepare(\n `INSERT INTO edges (src, dst, type, confidence, provenance)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(src, dst, type) DO UPDATE SET\n confidence = excluded.confidence,\n provenance = excluded.provenance`,\n );\n // node-id resolution (issue #1677). `nodes.id` is the PRIMARY KEY, so\n // this is a unique, unambiguous lookup — a SIMILAR_TO edge between two\n // same-qualified-name symbols resolves here instead of being dropped by\n // the ambiguous qualified-name fallback. The row's qualified_name is\n // returned so the caller's (src|dst)QualifiedName can be matched against\n // it: a stale or mismatched id+qname pair is skipped (counted in\n // `skipped`) rather than silently writing an edge from the wrong node\n // (chatgpt-codex-connector P2 — id/qname consistency at the boundary).\n const nodeById = this.db.prepare(\n \"SELECT qualified_name FROM nodes WHERE id = ? LIMIT 1\",\n );\n try {\n let persisted = 0;\n let skipped = 0;\n this.db.transaction(() => {\n for (const edge of edges) {\n if (!isEdgeProvenance(edge.provenance)) {\n throw new Error(\n `graph-store: edge has invalid provenance ${JSON.stringify(edge.provenance)}`,\n );\n }\n if (\n !Number.isFinite(edge.confidence) ||\n edge.confidence < 0 ||\n edge.confidence > 1\n ) {\n throw new Error(\n `graph-store: edge confidence ${edge.confidence} is out of range [0, 1] for edge ${edge.srcQualifiedName} → ${edge.dstQualifiedName}`,\n );\n }\n // Prefer the content-derived node id when the caller supplied one\n // (issue #1677). Only fall through to qualified-name resolution\n // when no id is present, preserving the existing trace/HTTP_CALLS\n // path verbatim. When an id IS supplied, its row's qualified_name\n // MUST match the edge's (src|dst)QualifiedName — a mismatched pair\n // (stale body map, custom integration) is skipped like a dangling\n // edge instead of corrupting the graph (chatgpt-codex-connector P2).\n const srcId = edge.srcNodeId\n ? resolveByNodeId(nodeById, edge.srcNodeId, edge.srcQualifiedName)\n : resolveNodeId(edge.srcQualifiedName, emptyBatch, this.db);\n const dstId = edge.dstNodeId\n ? resolveByNodeId(nodeById, edge.dstNodeId, edge.dstQualifiedName)\n : resolveNodeId(edge.dstQualifiedName, emptyBatch, this.db);\n if (!srcId || !dstId) {\n skipped += 1;\n continue;\n }\n const r = insertEdge.run(srcId, dstId, edge.type, edge.confidence, edge.provenance);\n persisted += r.changes;\n }\n })();\n return { ok: true, persisted, skipped };\n } catch (error) {\n logWriteFailure(error);\n return classifyError(error);\n }\n }\n\n /**\n * Pass 1a: upsert the file row and every symbol node, refreshing the\n * contentless `nodes_fts` index in lockstep, and compute the set of\n * stale node ids this file wants to prune (deterministic id, NOT\n * qualified_name, so a kind change gets a new id and the OLD row is\n * deleted). The prune itself — and the dangling-edge count that\n * gates it — is deferred to {@link pruneFileNodes} so the whole batch\n * shares one batch-wide view of what is being pruned before any\n * cascade runs.\n */\n private upsertFileNodes(ir: StoreFileIR): {\n result: UpsertResult;\n prunedNodeIds: string[];\n } {\n // Upsert the file row first; the nodes table references files(id).\n const upsertFile = this.db.prepare(\n `INSERT INTO files (path, lang, content_hash)\n VALUES (?, ?, ?)\n ON CONFLICT(path) DO UPDATE SET\n lang = excluded.lang,\n content_hash = excluded.content_hash\n RETURNING id`,\n );\n const fileRow = expectRow<{ id: number }>(\n upsertFile.get(ir.path, ir.language, ir.contentHash),\n [\"id\"],\n );\n if (!fileRow) {\n throw new Error(\n `graph-store: INSERT INTO files RETURNING id returned no row for path=${ir.path}`,\n );\n }\n const fileId = fileRow.id;\n\n // Build the seen id set FIRST (every symbol → its deterministic id)\n // so the prune step is order-stable and never deletes an id we\n // are about to (re)insert. Determinism is non-negotiable — see\n // nodeIdFor for the canonical form.\n const seenNodeIds = new Set<string>();\n const symbolByNodeId = new Map<string, SymbolIR>();\n for (const sym of ir.symbols) {\n const id = nodeIdFor({\n qualifiedName: sym.qualifiedName,\n filePath: ir.path,\n label: sym.kind,\n });\n seenNodeIds.add(id);\n symbolByNodeId.set(id, sym);\n }\n\n // Snapshot the prior nodes owned by this file so we can (a) skip\n // true no-op UPSERTs to keep `changes` honest, and (b) count\n // dangling edges the prune step will cascade.\n const existingNodes = expectRows<{\n id: string;\n label: string;\n name: string;\n qualified_name: string;\n file_id: number;\n span_start: number;\n span_end: number;\n lang: string;\n }>(\n this.db\n .prepare(\n `SELECT id, label, name, qualified_name, file_id,\n span_start, span_end, lang\n FROM nodes WHERE file_id = ?`,\n )\n .all(fileId),\n [\"id\", \"label\", \"name\", \"qualified_name\", \"file_id\", \"span_start\", \"span_end\", \"lang\"],\n );\n const existingById = new Map(existingNodes.map((n) => [n.id, n]));\n\n const insertNode = this.db.prepare(\n `INSERT INTO nodes (\n id, label, name, qualified_name,\n file_id, span_start, span_end, lang\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n label = excluded.label,\n name = excluded.name,\n qualified_name = excluded.qualified_name,\n file_id = excluded.file_id,\n span_start = excluded.span_start,\n span_end = excluded.span_end,\n lang = excluded.lang`,\n );\n const insertFts = this.db.prepare(\n `INSERT INTO nodes_fts (rowid, name, qualified_name) VALUES (?, ?, ?)`,\n );\n const deleteFtsByRowid = this.db.prepare(\n `DELETE FROM nodes_fts WHERE rowid = ?`,\n );\n // fts_index: maps FTS rowid → node id so PR2's search can JOIN\n // hits back to `nodes`. Contentless FTS5 does NOT store column\n // values, so the `id UNINDEXED` column reads NULL on every\n // MATCH — this table is the only reverse-mapping the read path\n // has (chatgpt-codex-connector P2: 'Preserve a node key for\n // FTS hits'). UNIQUE(node_id) lets the upsert use INSERT OR\n // REPLACE so a same-node re-upsert (the common no-op path)\n // keeps a single mapping row.\n const upsertFtsIndex = this.db.prepare(\n `INSERT INTO fts_index (fts_rowid, node_id) VALUES (?, ?)\n ON CONFLICT(node_id) DO UPDATE SET\n fts_rowid = excluded.fts_rowid`,\n );\n const deleteFtsIndexByRowid = this.db.prepare(\n `DELETE FROM fts_index WHERE fts_rowid = ?`,\n );\n let nodeCount = 0;\n for (const [id, sym] of symbolByNodeId) {\n const prior = existingById.get(id);\n if (\n prior &&\n prior.label === sym.kind &&\n prior.name === sym.name &&\n prior.qualified_name === sym.qualifiedName &&\n prior.span_start === sym.span.startByte &&\n prior.span_end === sym.span.endByte &&\n prior.lang === ir.language\n ) {\n // Truly a no-op — the row already matches the IR. Skip the\n // INSERT/UPDATE entirely so `changes` stays 0.\n continue;\n }\n // Drop any prior FTS row for this id before the new INSERT.\n // Contentless FTS5 (`content=''`) does NOT store UNINDEXED\n // column values, so the only reliable key is the deterministic\n // rowid we derive from the node id hash (chatgpt-codex-connector\n // P2: `WHERE id = ?` matches zero rows in contentless mode).\n const ftsRowid = ftsRowidForNodeId(id);\n deleteFtsByRowid.run(ftsRowid);\n // Mirror the delete into the FTS → node id reverse map so a\n // re-upsert does not collide on the UNIQUE(fts_rowid) PK when\n // the same rowid previously pointed at a different node id\n // (chatgpt-codex-connector P2: 'Preserve a node key for\n // FTS hits').\n deleteFtsIndexByRowid.run(ftsRowid);\n insertNode.run(\n id,\n sym.kind,\n sym.name,\n sym.qualifiedName,\n fileId,\n sym.span.startByte,\n sym.span.endByte,\n ir.language,\n );\n insertFts.run(ftsRowid, sym.name, sym.qualifiedName);\n upsertFtsIndex.run(ftsRowid, id);\n nodeCount += 1;\n }\n\n // Prune by id (NOT qualified_name). A symbol whose kind changed\n // keeps the same qualifiedName but has a new node id; the old\n // id's row must be deleted to keep the file's symbol set honest.\n // We do this AFTER the upserts so any same-id re-upsert above is\n // preserved. The actual delete (and the dangling-edge count) is\n // deferred to pruneFileNodes so the batch can share one batch-wide\n // view of every pruned node before any cascade runs.\n const prunedNodeIds = existingNodes\n .map((n) => n.id)\n .filter((id) => !seenNodeIds.has(id));\n\n return {\n result: {\n path: ir.path,\n fileId,\n nodeCount,\n edgeCount: 0,\n droppedDanglingEdges: 0,\n },\n prunedNodeIds,\n };\n }\n\n /**\n * Pass 1b: count the dangling edges this file's prune will drop and\n * perform the cascade delete + FTS cleanup. A dangling edge is one\n * whose dst is pruned by THIS file but whose src survives — and\n * \"survives\" is judged against the BATCH-WIDE pruned set, so an edge\n * whose both ends are pruned (possibly in different files) is\n * cascade-deleted and never reported as dangling. This makes the\n * reported loss independent of the order files are visited in\n * (chatgpt-codex-connector P2: 'Count dangling edges against the\n * whole batch').\n */\n private pruneFileNodes(\n result: UpsertResult,\n prunedNodeIds: readonly string[],\n batchPrunedIds: readonly string[],\n ): void {\n if (prunedNodeIds.length === 0) {\n result.droppedDanglingEdges = 0;\n return;\n }\n // Two temp tables keep the IN / NOT IN queries under SQLite's\n // ~32766 variable bind limit for large prune sets (cursor Bugbot:\n // 'Prune path exceeds SQL variable limit'). Each insert binds 1\n // param; the subquery-based count/delete bind zero.\n this.db.exec(\n \"CREATE TEMP TABLE IF NOT EXISTS _pruned_ids (id TEXT NOT NULL PRIMARY KEY)\",\n );\n this.db.exec(\n \"CREATE TEMP TABLE IF NOT EXISTS _batch_pruned_ids (id TEXT NOT NULL PRIMARY KEY)\",\n );\n const clearPruned = this.db.prepare(\"DELETE FROM _pruned_ids\");\n const clearBatch = this.db.prepare(\"DELETE FROM _batch_pruned_ids\");\n const insertPruned = this.db.prepare(\n \"INSERT OR IGNORE INTO _pruned_ids (id) VALUES (?)\",\n );\n const insertBatch = this.db.prepare(\n \"INSERT OR IGNORE INTO _batch_pruned_ids (id) VALUES (?)\",\n );\n clearPruned.run();\n clearBatch.run();\n const fillTemp = this.db.transaction(\n (rows: { table: string; ids: readonly string[] }[]) => {\n for (const { table, ids } of rows) {\n const stmt =\n table === \"_pruned_ids\"\n ? insertPruned\n : insertBatch;\n for (const id of ids) stmt.run(id);\n }\n },\n );\n fillTemp([\n { table: \"_pruned_ids\", ids: prunedNodeIds },\n { table: \"_batch_pruned_ids\", ids: batchPrunedIds },\n ]);\n // Count dangling edges BEFORE the cascade: dst is a node pruned by\n // THIS file AND src is NOT pruned anywhere in the batch (edges\n // between two batch-pruned nodes are cascade-deleted, not\n // \"dangling\", and must not be attributed to either file).\n const dangling = expectRow<{ c: number }>(\n this.db\n .prepare(\n `SELECT COUNT(*) AS c FROM edges\n WHERE dst IN (SELECT id FROM _pruned_ids)\n AND src NOT IN (SELECT id FROM _batch_pruned_ids)`,\n )\n .get(),\n [\"c\"],\n );\n result.droppedDanglingEdges = dangling?.c ?? 0;\n // DELETE stale nodes — ON DELETE CASCADE on edges drops every\n // edge whose src or dst is pruned (FK pragma set in open()).\n this.db.exec(\"DELETE FROM nodes WHERE id IN (SELECT id FROM _pruned_ids)\");\n clearPruned.run();\n clearBatch.run();\n // FTS + fts_index cleanup: rowids are derived in JS (not SQL),\n // so chunk the IN list to stay under the bind limit.\n const SQLITE_VAR_LIMIT = 32_766;\n const ftsRowids = prunedNodeIds.map(ftsRowidForNodeId);\n for (let i = 0; i < ftsRowids.length; i += SQLITE_VAR_LIMIT) {\n const chunk = ftsRowids.slice(i, i + SQLITE_VAR_LIMIT);\n const ph = chunk.map(() => \"?\").join(\", \");\n this.db.prepare(`DELETE FROM nodes_fts WHERE rowid IN (${ph})`).run(...chunk);\n this.db.prepare(`DELETE FROM fts_index WHERE fts_rowid IN (${ph})`).run(...chunk);\n }\n }\n\n /**\n * Pass 2: re-insert edges for one file. Runs AFTER every file's\n * nodes are in place (the full batch is committed to nodes) so\n * cross-file edges resolve regardless of input order. Stale edges\n * for nodes owned by this file are deleted first so a changed\n * `confidence` or `provenance` actually overwrites the prior row\n * (chatgpt-codex-connector P1: ON CONFLICT DO NOTHING silently\n * kept stale edges across re-ingests).\n */\n private upsertFileEdges(ir: StoreFileIR, result: UpsertResult): void {\n // If edges are not provided (undefined or null — e.g. from JSON\n // deserialization), preserve prior edges rather than treating a\n // missing field as an empty assertion set. A bare core\n // ParseResult.ir (which has no edges field) re-upsert must NOT\n // wipe previously stored edges. An explicit empty array [] DOES\n // assert \"no edges\" and deletes all prior src-owned edges\n // (cursor Bugbot: 'Omitted edges field wipes stored edges' /\n // 'Null edges wipe stored edges').\n if (ir.edges == null) {\n return;\n }\n // Build the set of edges the IR is asserting for this file. The\n // SRC of each edge MUST belong to this file (it is resolved from\n // the per-file `qualifiedNameToId` map only — no DB fallback);\n // a FileIR that asserts an edge whose src is absent from this\n // file but present elsewhere is malformed and the edge is dropped\n // so it cannot be silently cross-owned. The DST may be cross-file\n // and uses the full-DB fallback (chatgpt-codex-connector P2:\n // 'Require edge sources to belong to the ingested file'). Edges\n // whose dst cannot resolve are also dropped — the caller is\n // responsible for the batch's canonical file set (rule 40).\n const qualifiedNameToId = new Map<string, string>();\n const ownSymbols = expectRows<{ id: string; qualified_name: string }>(\n this.db\n .prepare(\"SELECT id, qualified_name FROM nodes WHERE file_id = ?\")\n .all(result.fileId),\n [\"id\", \"qualified_name\"],\n );\n // Count qualified_name occurrences so ambiguous names are excluded.\n // Node identity is (qualifiedName, filePath, label) — two symbols in\n // the same file CAN share a qualified_name (e.g. a TS type + value\n // both named Foo, with different labels → different node ids). A\n // qualified_name-only map would silently keep just one; instead,\n // ambiguous names are left out so edges to them resolve to undefined\n // and are dropped — matching the DST conservative-drop policy\n // (chatgpt-codex-connector P2: 'Reject ambiguous local qualified names').\n const qnameCounts = new Map<string, number>();\n for (const row of ownSymbols) {\n qnameCounts.set(row.qualified_name, (qnameCounts.get(row.qualified_name) ?? 0) + 1);\n }\n for (const row of ownSymbols) {\n if ((qnameCounts.get(row.qualified_name) ?? 0) === 1) {\n qualifiedNameToId.set(row.qualified_name, row.id);\n }\n }\n\n const assertedKeys = new Set<string>();\n // Re-resolve each edge in the IR to its deterministic key so the\n // delete below only drops edges that are NOT being re-asserted.\n // Doing this BEFORE the delete is critical: deleting first then\n // checking the no-op skip leaves cross-file edges (whose src is\n // owned here but whose dst lives elsewhere) orphaned when the\n // IR re-asserts them — the prior-edge snapshot matches, the\n // insert is skipped, and the row is gone (cursor Bugbot #6a78cd0a).\n const seenKeys: string[] = [];\n // Map each resolved key to its first edge so the insertion pass\n // can look up metadata in O(1) instead of rescanning ir.edges\n // and re-running resolveNodeId (with DB lookups) per key\n // (chatgpt-codex-connector P2: 'Preserve resolved edge metadata\n // instead of rescanning'). First-edge-wins dedupe policy\n // (cursor Bugbot #28876d4c) is preserved by only setting on\n // first occurrence.\n const keyToEdge = new Map<string, EdgeIR>();\n for (const edge of ir.edges ?? []) {\n // Reject malformed edges up-front (rule 51: surface what is wrong).\n if (!isEdgeProvenance(edge.provenance)) {\n throw new Error(\n `graph-store: edge has invalid provenance ${JSON.stringify(edge.provenance)}`,\n );\n }\n if (\n !Number.isFinite(edge.confidence) ||\n edge.confidence < 0 ||\n edge.confidence > 1\n ) {\n throw new Error(\n `graph-store: edge confidence ${edge.confidence} is out of range [0, 1] for edge ${edge.srcQualifiedName} → ${edge.dstQualifiedName}`,\n );\n }\n // SRC must be a symbol in THIS file — resolve from the per-file\n // map only. A FileIR whose edge src lives in another file is\n // malformed; dropping it prevents cross-owned edges that survive\n // re-ingest (chatgpt-codex-connector P2).\n const srcId = qualifiedNameToId.get(edge.srcQualifiedName);\n if (!srcId) continue;\n // DST may be cross-file. A path-hinted edge (relative import,\n // issue #1894 review) resolves ONLY within its declared target\n // file — never via the global bare-name fallback; unhinted edges\n // keep the batch-map + full-DB fallback.\n const dstId = edge.dstPathHint\n ? resolveNodeIdWithPathHint(\n edge.dstQualifiedName,\n edge.dstPathHint,\n this.db,\n edge.dstImporterLanguage,\n )\n : resolveNodeId(edge.dstQualifiedName, qualifiedNameToId, this.db);\n if (!dstId) continue;\n const key = `${srcId}\\u0000${dstId}\\u0000${edge.type}`;\n assertedKeys.add(key);\n seenKeys.push(key);\n if (!keyToEdge.has(key)) {\n keyToEdge.set(key, edge);\n }\n }\n\n // Pre-fetch the prior edges owned by this file (src in this file's\n // nodes) so we can (a) skip the no-op re-upsert when confidence +\n // provenance match exactly, and (b) compute the stale-edge delete\n // set: prior src-owned edges that are NOT in the current IR's\n // asserted keys.\n const priorEdges = expectRows<{\n src: string;\n dst: string;\n type: string;\n confidence: number;\n provenance: string;\n }>(\n this.db\n .prepare(\n \"SELECT src, dst, type, confidence, provenance FROM edges WHERE src IN (SELECT id FROM nodes WHERE file_id = ?)\",\n )\n .all(result.fileId),\n [\"src\", \"dst\", \"type\", \"confidence\", \"provenance\"],\n );\n const priorByKey = new Map<string, { confidence: number; provenance: string }>();\n const staleSrcDstTypes: Array<{ src: string; dst: string; type: string }> = [];\n // Provenance scoping (issue #1891): when the IR declares which\n // provenances it asserts, stale edges of OTHER provenances are not\n // this ingest's to delete — a fresh parse contradicts only its own\n // derivation class (rule 25). Absent = legacy delete-all-stale.\n // An EMPTY scope array is treated as absent (cursor review round 9):\n // \"[]\" would otherwise be truthy, protect every prior edge from\n // deletion AND from updates, and silently disable cleanup — an\n // assertion that scopes nothing scopes nothing.\n const scoped =\n ir.assertedEdgeProvenances && ir.assertedEdgeProvenances.length > 0\n ? ir.assertedEdgeProvenances\n : undefined;\n for (const p of priorEdges) {\n const key = `${p.src}\\u0000${p.dst}\\u0000${p.type}`;\n priorByKey.set(key, { confidence: p.confidence, provenance: p.provenance });\n if (assertedKeys.has(key)) continue;\n if (scoped && !(scoped as readonly string[]).includes(p.provenance)) continue;\n staleSrcDstTypes.push({ src: p.src, dst: p.dst, type: p.type });\n }\n\n // Delete only the stale src-owned edges (prior-but-not-asserted).\n // This preserves any cross-file edge that the current IR\n // re-asserts, even when its dst lives in a file NOT in this\n // batch — the row survives the delete and the no-op skip below\n // keeps `changes` honest.\n //\n // Chunk the deletes: each tuple binds 3 parameters and SQLite\n // enforces a variable limit (32766 in the bundled build). An\n // unbounded IN list would throw `too many SQL variables` for a\n // file with >10,922 stale edges, rolling back the whole batch\n // (chatgpt-codex-connector P2: 'Chunk stale-edge deletes before\n // binding them').\n const SQLITE_VARIABLE_LIMIT = 32_766;\n const PARAMS_PER_TUPLE = 3;\n const MAX_TUPLES_PER_CHUNK = Math.floor(SQLITE_VARIABLE_LIMIT / PARAMS_PER_TUPLE);\n for (let i = 0; i < staleSrcDstTypes.length; i += MAX_TUPLES_PER_CHUNK) {\n const chunk = staleSrcDstTypes.slice(i, i + MAX_TUPLES_PER_CHUNK);\n const placeholders = chunk.map(() => \"(?, ?, ?)\").join(\", \");\n this.db\n .prepare(`DELETE FROM edges WHERE (src, dst, type) IN (${placeholders})`)\n .run(...chunk.flatMap((e) => [e.src, e.dst, e.type]));\n }\n\n const insertEdge = this.db.prepare(\n `INSERT INTO edges (src, dst, type, confidence, provenance)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(src, dst, type) DO UPDATE SET\n confidence = excluded.confidence,\n provenance = excluded.provenance`,\n );\n let edgeCount = 0;\n // Dedupe keys: a FileIR with two edges sharing `(src, dst, type)`\n // (but differing confidence/provenance) is malformed input. First-edge\n // metadata wins; later duplicates are skipped so they cannot inflate\n // edgeCount via redundant re-upserts (cursor Bugbot #28876d4c).\n const processedKeys = new Set<string>();\n for (const key of seenKeys) {\n if (processedKeys.has(key)) continue;\n processedKeys.add(key);\n const edge = keyToEdge.get(key);\n if (!edge) continue;\n const parts = key.split(\"\\u0000\");\n const srcId = parts[0]!;\n const dstId = parts[1]!;\n const prior = priorByKey.get(key);\n if (\n prior &&\n prior.confidence === edge.confidence &&\n prior.provenance === edge.provenance\n ) {\n // Identical to the prior row — no INSERT/UPDATE needed, so\n // `changes` stays 0 and the idempotency contract holds. The\n // row still exists because the delete above only removed\n // stale keys.\n continue;\n }\n // Two update-path protections under provenance scoping (issue\n // #1891 + #1894 review rounds):\n // 1. a prior row whose provenance is OUTSIDE the asserted scope\n // is not this assertion's to modify (defense in depth — in\n // practice cross-provenance key collisions are heuristic/lsp\n // only, handled by 2);\n // 2. an lsp row is a strictly stronger derivation of the SAME\n // source-derived edge: re-asserting the heuristic key keeps\n // the row alive (it is not stale) but must never downgrade it.\n // Retirement of lsp rows happens through the stale-delete when\n // the call disappears from the parse — lsp IS in the reindex\n // assertion scope precisely so vanished calls retire their\n // upgraded rows too.\n if (\n prior &&\n scoped &&\n (!(scoped as readonly string[]).includes(prior.provenance) ||\n (prior.provenance === \"lsp\" && edge.provenance === \"heuristic\"))\n ) {\n continue;\n }\n const r = insertEdge.run(srcId, dstId, edge.type, edge.confidence, edge.provenance);\n edgeCount += r.changes;\n }\n result.edgeCount = edgeCount;\n }\n\n /**\n * Pass 3 (PR2): upsert `node_attributes` rows for this file's\n * surviving nodes, derived from the IR's optional `exports` and\n * `routes` arrays. Per-field preservation semantics (mirrors the\n * edges pass, generalized to two independent flags):\n * - `exports == null` (omitted) → preserve existing `is_exported`\n * flags untouched (PR1-era IR has no exports field). The\n * `is_route_handler` flag is rebuilt independently from\n * `routes` — the two columns do NOT interact.\n * - `exports === []` (explicit empty) → wipe the file's\n * `is_exported` flags (the caller is asserting \"this file\n * exports nothing\").\n * - same rule for `routes` / `is_route_handler`.\n *\n * A symbol is `is_exported=1` when its `name` matches an entry in\n * `ir.exports` (multiple symbols with the same name in one file all\n * get the flag — the dead-code query treats this conservatively,\n * never silently picking one). A symbol is `is_route_handler=1`\n * when its `qualifiedName` equals a route's `handlerQualifiedName`.\n *\n * Implementation: per-flag UPDATE, not a delete-then-insert (the\n * original PR2 implementation wiped both flags whenever either field\n * was present, so a re-ingest with only `exports` silently dropped\n * `is_route_handler` — cursor Bugbot + chatgpt-codex-connector P2).\n * The two flags live in the same row keyed by node_id; INSERT OR\n * IGNORE ensures a row exists, then UPDATE-per-flag changes only\n * the column the IR is asserting.\n */\n private upsertFileAttributes(ir: StoreFileIR, result: UpsertResult): void {\n // Both fields omitted → nothing to assert. Preserve every flag\n // (PR1 baseline). Avoids touching the table at all so truly-no-op\n // re-ingests stay zero-cost.\n if (ir.exports == null && ir.routes == null) {\n return;\n }\n\n const ownNodes = expectRows<{ id: string; name: string; qualified_name: string }>(\n this.db\n .prepare(\"SELECT id, name, qualified_name FROM nodes WHERE file_id = ?\")\n .all(result.fileId),\n [\"id\", \"name\", \"qualified_name\"],\n );\n const ownNodeIds = ownNodes.map((n) => n.id);\n if (ownNodeIds.length === 0) {\n return;\n }\n\n // Per-flag rebuild. The pattern is identical for each flag:\n // 1. Ensure every node in this file has an attributes row\n // (default 0,0). INSERT OR IGNORE keeps any existing row.\n // 2. If the IR field for this flag is present, wipe the column\n // for this file's nodes (so removed flags clear), then set\n // the column for nodes in the new set.\n // 3. If the IR field is omitted, leave the column untouched.\n const ensureRow = this.db.prepare(\n `INSERT OR IGNORE INTO node_attributes (node_id, is_exported, is_route_handler)\n VALUES (?, 0, 0)`,\n );\n for (const id of ownNodeIds) ensureRow.run(id);\n\n if (ir.exports != null) {\n const exportNames = new Set<string>();\n for (const ex of ir.exports) {\n if (ex && typeof ex.name === \"string\" && ex.name.length > 0) {\n exportNames.add(ex.name);\n }\n }\n const newExportedIds = new Set<string>();\n for (const n of ownNodes) {\n if (exportNames.has(n.name)) newExportedIds.add(n.id);\n }\n // Wipe is_exported for this file's nodes, chunked under the\n // SQLite variable limit. The other column is untouched.\n this.runChunkedUpdate(\n `UPDATE node_attributes SET is_exported = 0 WHERE node_id IN (%PH%)`,\n ownNodeIds,\n );\n // Set the flag for the new exported set.\n const setExported = this.db.prepare(\n `UPDATE node_attributes SET is_exported = 1 WHERE node_id = ?`,\n );\n for (const id of newExportedIds) setExported.run(id);\n }\n\n if (ir.routes != null) {\n const handlerQNames = new Set<string>();\n for (const r of ir.routes) {\n if (r && typeof r.handlerQualifiedName === \"string\" && r.handlerQualifiedName.length > 0) {\n handlerQNames.add(r.handlerQualifiedName);\n }\n }\n const newRouteIds = new Set<string>();\n for (const n of ownNodes) {\n if (handlerQNames.has(n.qualified_name)) newRouteIds.add(n.id);\n }\n this.runChunkedUpdate(\n `UPDATE node_attributes SET is_route_handler = 0 WHERE node_id IN (%PH%)`,\n ownNodeIds,\n );\n const setRoute = this.db.prepare(\n `UPDATE node_attributes SET is_route_handler = 1 WHERE node_id = ?`,\n );\n for (const id of newRouteIds) setRoute.run(id);\n }\n }\n\n /**\n * Chunk a parameterized UPDATE-with-IN-list under SQLite's variable\n * bind limit. The SQL template uses `%PH%` as a placeholder for the\n * `?,?,…` list. Mirrors the chunking pattern PR1 uses for deletes.\n */\n private runChunkedUpdate(sqlTemplate: string, params: readonly string[]): void {\n if (params.length === 0) return;\n for (let i = 0; i < params.length; i += SQLITE_VARIABLE_LIMIT) {\n const chunk = params.slice(i, i + SQLITE_VARIABLE_LIMIT);\n const placeholders = chunk.map(() => \"?\").join(\", \");\n this.db.prepare(sqlTemplate.replace(\"%PH%\", placeholders)).run(...chunk);\n }\n }\n\n // ──────────────────────────────────────────────────────────────────────\n // PR2 read primitives (issue #1552 steps 4–5).\n // ──────────────────────────────────────────────────────────────────────\n\n /**\n * Iterative frontier BFS over the edges table. Cycle-safe via a JS\n * visited set keyed by node id; depth-capped by {@link TraverseQuery.maxDepth}\n * (half-open — depth==maxDepth is INCLUDED, maxDepth+1 is NOT — rule 35).\n * The start node is always included at depth 0 when it exists.\n *\n * Reads the edges table via a single prepared statement per\n * direction; the frontier expands level-by-level so memory is\n * bounded by the visited set's size, not the recursion depth.\n */\n traverse(query: TraverseQuery): TraverseResult {\n if (this.closed) return { ok: false, code: \"store_closed\" };\n // Guard the query object before any dereference: a null/undefined\n // payload (e.g. malformed JSON forwarded at an MCP boundary) would\n // throw on `query.maxDepth` below instead of returning the tagged\n // invalid_query the read contract advertises\n // (chatgpt-codex-connector P2: 'Validate read query objects before\n // dereferencing').\n if (query == null || typeof query !== \"object\") {\n return { ok: false, code: \"invalid_query\" };\n }\n // Validate maxDepth up-front (rule 51: surface what's wrong).\n if (\n typeof query.maxDepth !== \"number\" ||\n !Number.isInteger(query.maxDepth) ||\n query.maxDepth < 0\n ) {\n return {\n ok: false,\n code: \"invalid_query\",\n };\n }\n // Validate direction against the allowed set (rule 51 +\n // chatgpt-codex-connector P2: 'Reject invalid traversal directions\n // explicitly'). Default ONLY on `undefined` — a `null` from a\n // JSON/tool caller is a malformed value, not an absent one, so the\n // `??` operator (which treats null as nullish) would silently turn\n // it into \"outgoing\" and mask the bad input. Reject null explicitly\n // (chatgpt-codex-connector P2: 'Reject null traversal directions').\n const direction: TraverseDirection =\n query.direction === undefined ? \"outgoing\" : query.direction;\n if (\n direction !== \"outgoing\" &&\n direction !== \"incoming\" &&\n direction !== \"both\"\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n // Validate edgeTypes, if present, is an array of strings (rule 51 +\n // chatgpt-codex-connector P2: 'Validate edgeTypes before building\n // traversal SQL'). A malformed value like a bare string \"CALLS\"\n // would otherwise throw at .map() instead of returning the\n // tagged invalid_query failure the contract advertises.\n if (\n query.edgeTypes !== undefined &&\n (!Array.isArray(query.edgeTypes) ||\n !query.edgeTypes.every((e) => typeof e === \"string\"))\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n // Validate `start` is a non-empty string before it reaches the\n // SQLite bind (rule 51 + chatgpt-codex-connector P2: 'Validate\n // traverse start before binding it'). A JS/JSON caller passing an\n // object/array survives the regex `.test()` coercion but then\n // throws a non-SQLite TypeError at bind time; surface that as the\n // precise `invalid_query` rather than letting it fall through to\n // the generic db_error catch-all.\n if (\n typeof query.start !== \"string\" ||\n query.start.length === 0\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n // Wrap DB operations in try/catch so lock/corrupt errors return a\n // tagged failure instead of throwing (cursor Bugbot: 'SQLite errors\n // escape read APIs'). Same shape as schemaStats/deadCode.\n try {\n // Resolve the start node. If `start` is a 64-char lowercase hex\n // string (the nodeIdFor sha256 format), resolve ONLY by id — this\n // is the unambiguous path a caller uses after seeing an ambiguous\n // qualified_name rejection. Otherwise resolve ONLY by qualified_name.\n // Splitting the two paths (cursor Bugbot: 'Traverse start conflates id\n // and name') prevents a mistyped id from silently matching an\n // unrelated qualified_name, and prevents a unique id from being\n // reported as ambiguous just because some other node shares the id\n // string as a qualified_name.\n let startId: string;\n const isNodeId = /^[0-9a-f]{64}$/.test(query.start);\n const rows = expectRows<{ id: string }>(\n this.db\n .prepare(\n isNodeId\n ? \"SELECT id FROM nodes WHERE id = ?\"\n : \"SELECT id FROM nodes WHERE qualified_name = ?\",\n )\n .all(query.start),\n [\"id\"],\n );\n if (rows.length === 0) {\n return { ok: false, code: \"unknown_start\" };\n }\n if (rows.length > 1) {\n // Multiple rows mean the start resolved to more than one node —\n // either the same id matching twice (impossible — PRIMARY KEY)\n // or a qualified_name declared in multiple files. Reject so the\n // caller passes an explicit node id.\n return { ok: false, code: \"ambiguous_start\" };\n }\n startId = rows[0]!.id;\n\n // BFS. Edge-type filter is bound into the prepared statement via\n // IN (?, ?, ...) — empty list means \"all types\" (no WHERE clause\n // on type). The edge-type list is small (≤ ~25) so chunking is\n // unnecessary here, but we still parameterize so user input\n // cannot inject SQL.\n const edgeTypes = query.edgeTypes ?? [];\n const typeClause =\n edgeTypes.length > 0\n ? `AND type IN (${edgeTypes.map(() => \"?\").join(\", \")})`\n : \"\";\n const outgoingStmt = this.db.prepare(\n `SELECT dst AS neighbor, src AS via_id FROM edges WHERE src = ? ${typeClause}`,\n );\n const incomingStmt = this.db.prepare(\n `SELECT src AS neighbor, dst AS via_id FROM edges WHERE dst = ? ${typeClause}`,\n );\n\n const visited = new Set<string>([startId]);\n const hits: TraverseHit[] = [];\n const startRow = expectRow<{\n id: string;\n qualified_name: string;\n name: string;\n label: string;\n file_path: string;\n }>(\n this.db\n .prepare(\n \"SELECT n.id, n.qualified_name, n.name, n.label, f.path AS file_path FROM nodes n JOIN files f ON n.file_id = f.id WHERE n.id = ?\",\n )\n .get(startId),\n [\"id\", \"qualified_name\", \"name\", \"label\", \"file_path\"],\n );\n if (!startRow) {\n // Race: the node vanished between the resolve and the read.\n // Treat as unknown rather than crash.\n return { ok: false, code: \"unknown_start\" };\n }\n hits.push({\n nodeId: startRow.id,\n qualifiedName: startRow.qualified_name,\n name: startRow.name,\n label: startRow.label,\n filePath: startRow.file_path,\n depth: 0,\n });\n\n // maxDepth === 0 → just the start node (half-open: depth 0 is\n // included, depth 1 is not).\n if (query.maxDepth === 0) {\n return { ok: true, hits };\n }\n\n let frontier: string[] = [startId];\n for (let depth = 1; depth <= query.maxDepth; depth += 1) {\n const nextFrontier: string[] = [];\n for (const nodeId of frontier) {\n const params = [nodeId, ...edgeTypes];\n const outRows =\n direction === \"outgoing\" || direction === \"both\"\n ? expectRows<{ neighbor: string }>(\n outgoingStmt.all(...params),\n [\"neighbor\"],\n )\n : [];\n const inRows =\n direction === \"incoming\" || direction === \"both\"\n ? expectRows<{ neighbor: string }>(\n incomingStmt.all(...params),\n [\"neighbor\"],\n )\n : [];\n for (const r of [...outRows, ...inRows]) {\n const neighbor = r.neighbor;\n // Cycle safety: a node already in `visited` is not re-added.\n // This also handles self-edges (src == dst): the start is in\n // visited, so a self-loop on it never re-expands the frontier.\n if (visited.has(neighbor)) continue;\n visited.add(neighbor);\n nextFrontier.push(neighbor);\n const hitRow = expectRow<{\n id: string;\n qualified_name: string;\n name: string;\n label: string;\n file_path: string;\n }>(\n this.db\n .prepare(\n \"SELECT n.id, n.qualified_name, n.name, n.label, f.path AS file_path FROM nodes n JOIN files f ON n.file_id = f.id WHERE n.id = ?\",\n )\n .get(neighbor),\n [\"id\", \"qualified_name\", \"name\", \"label\", \"file_path\"],\n );\n if (hitRow) {\n hits.push({\n nodeId: hitRow.id,\n qualifiedName: hitRow.qualified_name,\n name: hitRow.name,\n label: hitRow.label,\n filePath: hitRow.file_path,\n depth,\n });\n }\n }\n }\n if (nextFrontier.length === 0) break;\n frontier = nextFrontier;\n }\n return { ok: true, hits };\n } catch (error) {\n logWriteFailure(error);\n return classifyReadError(error) as TraverseResult;\n }\n }\n /**\n * Path-enumerating traversal (issue #1650). Unlike {@link traverse}'s BFS —\n * which visits each node ONCE at its shortest-path depth and so cannot honor\n * an exact `*N` (N > 1) hop count for nodes reachable at both a shorter and a\n * length-N path — this primitive enumerates concrete relationship-simple\n * paths from the start, yielding one hit per distinct (path, endpoint) pair\n * up to {@link TraversePathsQuery.maxHops}.\n *\n * Cycle safety uses RELATIONSHIP UNIQUENESS (the real Cypher rule): a single\n * path never traverses the same edge twice, keyed by the edge's canonical\n * `(src, dst, type)` identity. A node MAY recur in a path via distinct edges\n * (e.g. A->B->A over two different edges) — that is correct Cypher behavior.\n * The {@link TraversePathsQuery.maxHops} cap bounds each path's length;\n * {@link TraversePathsQuery.maxPaths} bounds the total enumerated count so a\n * dense subgraph cannot blow enumeration up exponentially without notice\n * (when hit, enumeration stops and the result carries `truncated: true`).\n *\n * Every yielded path has length >= 1 (at least one edge). A length-0 \"path\"\n * (the trivial start->start) is NOT enumerated; callers that need the start\n * node for a `*0..N` bound add it themselves.\n */\n traversePaths(query: TraversePathsQuery): TraversePathsResult {\n if (this.closed) return { ok: false, code: \"store_closed\" };\n // Guard the query object before any dereference (mirrors traverse).\n if (query == null || typeof query !== \"object\") {\n return { ok: false, code: \"invalid_query\" };\n }\n if (\n typeof query.maxHops !== \"number\" ||\n !Number.isInteger(query.maxHops) ||\n query.maxHops < 0\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n // Reject depths that would overflow the recursive DFS before maxPaths\n // can bind it (chatgpt-codex-connector P2: 'Avoid recursive DFS for deep\n // bounded paths').\n if (query.maxHops > MAX_TRAVERSE_PATHS_HOPS) {\n return { ok: false, code: \"invalid_query\" };\n }\n const direction: TraverseDirection =\n query.direction === undefined ? \"outgoing\" : query.direction;\n if (\n direction !== \"outgoing\" &&\n direction !== \"incoming\" &&\n direction !== \"both\"\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n if (\n query.edgeTypes !== undefined &&\n (!Array.isArray(query.edgeTypes) ||\n !query.edgeTypes.every((e) => typeof e === \"string\"))\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n if (typeof query.start !== \"string\" || query.start.length === 0) {\n return { ok: false, code: \"invalid_query\" };\n }\n // minHops: validate only when explicitly provided; default 1. MUST be a\n // positive integer -- the primitive never emits length-0 paths.\n const minHops = query.minHops === undefined ? 1 : query.minHops;\n if (\n typeof minHops !== \"number\" ||\n !Number.isInteger(minHops) ||\n minHops < 1\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n // maxPaths: reject a malformed EXPLICIT value rather than silently\n // defaulting (rule 51 -- surface what's wrong). Only `undefined` defaults\n // (chatgpt-codex-connector P2: 'Reject malformed maxPaths instead of\n // defaulting').\n let maxPaths: number;\n if (query.maxPaths === undefined) {\n maxPaths = DEFAULT_TRAVERSE_PATHS_MAX;\n } else if (\n typeof query.maxPaths !== \"number\" ||\n !Number.isInteger(query.maxPaths) ||\n query.maxPaths < 0\n ) {\n return { ok: false, code: \"invalid_query\" };\n } else {\n maxPaths = query.maxPaths;\n }\n\n try {\n // Resolve the start node — same split id/qualified_name policy as\n // traverse (cursor Bugbot: 'Traverse start conflates id and name').\n const isNodeId = /^[0-9a-f]{64}$/.test(query.start);\n const rows = expectRows<{ id: string }>(\n this.db\n .prepare(\n isNodeId\n ? \"SELECT id FROM nodes WHERE id = ?\"\n : \"SELECT id FROM nodes WHERE qualified_name = ?\",\n )\n .all(query.start),\n [\"id\"],\n );\n if (rows.length === 0) return { ok: false, code: \"unknown_start\" };\n if (rows.length > 1) return { ok: false, code: \"ambiguous_start\" };\n const startId = rows[0]!.id;\n\n // maxHops === 0 -> no edge paths exist.\n if (query.maxHops === 0) {\n return { ok: true, hits: [], truncated: false };\n }\n\n const edgeTypes = query.edgeTypes ?? [];\n const typeClause =\n edgeTypes.length > 0\n ? `AND type IN (${edgeTypes.map(() => \"?\").join(\", \")})`\n : \"\";\n // Return the canonical (src, dst, type) so relationship-uniqueness keys\n // are direction-independent: traversing edge A->B outgoing then B->A\n // incoming reuses the SAME relationship and is blocked.\n const outgoingStmt = this.db.prepare(\n `SELECT dst AS neighbor, src, dst, type FROM edges WHERE src = ? ${typeClause}`,\n );\n const incomingStmt = this.db.prepare(\n `SELECT src AS neighbor, src, dst, type FROM edges WHERE dst = ? ${typeClause}`,\n );\n const nodeStmt = this.db.prepare(\n \"SELECT n.id, n.qualified_name, n.name, n.label, f.path AS file_path FROM nodes n JOIN files f ON n.file_id = f.id WHERE n.id = ?\",\n );\n\n type NodeRow = {\n id: string;\n qualified_name: string;\n name: string;\n label: string;\n file_path: string;\n };\n type EdgeRow = {\n neighbor: string;\n src: string;\n dst: string;\n type: string;\n };\n\n const nodeCache = new Map<string, NodeRow>();\n const getNode = (id: string): NodeRow | undefined => {\n const cached = nodeCache.get(id);\n if (cached) return cached;\n const row = expectRow<NodeRow>(nodeStmt.get(id), [\n \"id\",\n \"qualified_name\",\n \"name\",\n \"label\",\n \"file_path\",\n ]);\n if (row) nodeCache.set(id, row);\n return row;\n };\n\n const neighborsOf = (id: string): EdgeRow[] => {\n const params = [id, ...edgeTypes];\n const out: EdgeRow[] =\n direction === \"outgoing\" || direction === \"both\"\n ? expectRows<EdgeRow>(outgoingStmt.all(...params), [\n \"neighbor\",\n \"src\",\n \"dst\",\n \"type\",\n ])\n : [];\n const inn: EdgeRow[] =\n direction === \"incoming\" || direction === \"both\"\n ? expectRows<EdgeRow>(incomingStmt.all(...params), [\n \"neighbor\",\n \"src\",\n \"dst\",\n \"type\",\n ])\n : [];\n // Dedupe by the canonical (src, dst, type) key. Under\n // direction \"both\" a SELF-LOOP (src == dst) is matched by BOTH\n // the outgoing and incoming SELECTs, and because usedEdges is\n // cleared after each branch the same relationship-simple path\n // would otherwise be emitted twice -- violating the one-hit-per-\n // distinct-path contract and double-consuming the maxPaths cap\n // (chatgpt-codex-connector P2: 'Deduplicate self-loop edges for\n // both-direction traversal'). The UNIQUE(src,dst,type) table\n // constraint guarantees no dup within a single direction, so this\n // only ever collapses the both-direction self-loop overlap.\n const seenEdge = new Set<string>();\n const deduped: EdgeRow[] = [];\n for (const e of [...out, ...inn]) {\n const k = e.src + \"\\u0000\" + e.dst + \"\\u0000\" + e.type;\n if (seenEdge.has(k)) continue;\n seenEdge.add(k);\n deduped.push(e);\n }\n return deduped;\n };\n\n const hits: TraversePathHit[] = [];\n let truncated = false;\n const usedEdges = new Set<string>();\n const pathNodes: string[] = [startId];\n const pathEdgeTypes: string[] = [];\n const pathEndpoints: Array<{ src: string; dst: string }> = [];\n\n // Recursive DFS. `length` is the current path's hop count (edges taken).\n // We EXPLORE while length < maxHops (shorter prefixes must be walked to\n // reach longer paths) but EMIT only when newLength >= minHops, so the\n // maxPaths cap protects the in-range result set instead of being\n // consumed by discarded shorter prefixes (cursor Bugbot: 'Path cap\n // ignores hop minimum').\n const dfs = (currentId: string, length: number): void => {\n if (truncated) return;\n if (length >= query.maxHops) return;\n for (const e of neighborsOf(currentId)) {\n if (truncated) return;\n const key = `${e.src}\\u0000${e.dst}\\u0000${e.type}`;\n if (usedEdges.has(key)) continue;\n usedEdges.add(key);\n pathNodes.push(e.neighbor);\n pathEdgeTypes.push(e.type);\n pathEndpoints.push({ src: e.src, dst: e.dst });\n const newLength = length + 1;\n if (newLength >= minHops) {\n // Cap check on EMITTED (in-range) hits only.\n if (hits.length >= maxPaths) {\n truncated = true;\n } else {\n const info = getNode(e.neighbor);\n if (info) {\n hits.push({\n nodeId: info.id,\n qualifiedName: info.qualified_name,\n name: info.name,\n label: info.label,\n filePath: info.file_path,\n length: newLength,\n nodeIds: pathNodes.slice(),\n edgeTypes: pathEdgeTypes.slice(),\n edgeEndpoints: pathEndpoints.slice(),\n });\n }\n }\n }\n if (!truncated) dfs(e.neighbor, newLength);\n pathEndpoints.pop();\n pathEdgeTypes.pop();\n pathNodes.pop();\n usedEdges.delete(key);\n }\n };\n\n dfs(startId, 0);\n return { ok: true, hits, truncated };\n } catch (error) {\n logWriteFailure(error);\n return classifyReadError(error) as TraversePathsResult;\n }\n }\n /**\n * Structured node search. All filters are AND-combined; patterns use\n * SQLite LIKE (case-insensitive via COLLATE NOCASE). Patterns and\n * limits are parameter-bound, never string-interpolated, so user\n * input cannot inject SQL.\n */\n searchGraph(query: SearchQuery): SearchResult {\n if (this.closed) return { ok: false, code: \"store_closed\" };\n // Guard the query object before any dereference (see traverse).\n if (query == null || typeof query !== \"object\") {\n return { ok: false, code: \"invalid_query\" };\n }\n // Validate numeric inputs (rule 51). NaN / negative / non-integer\n // limits are rejected, not silently clamped, so callers learn what\n // they passed.\n if (\n (query.degreeMin !== undefined &&\n (typeof query.degreeMin !== \"number\" ||\n !Number.isInteger(query.degreeMin) ||\n query.degreeMin < 0)) ||\n (query.degreeMax !== undefined &&\n (typeof query.degreeMax !== \"number\" ||\n !Number.isInteger(query.degreeMax) ||\n query.degreeMax < 0))\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n if (\n query.degreeMin !== undefined &&\n query.degreeMax !== undefined &&\n query.degreeMin > query.degreeMax\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n const rawLimit = query.limit ?? 100;\n if (\n typeof rawLimit !== \"number\" ||\n !Number.isInteger(rawLimit) ||\n rawLimit < 0\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n // Clamp to [0, MAX_SEARCH_LIMIT]. A `limit: 0` returns an empty\n // hits array (rule 27 — guard the slice/LIMIT against zero).\n const MAX_SEARCH_LIMIT = 1000;\n const limit = Math.min(rawLimit, MAX_SEARCH_LIMIT);\n\n // Validate string filters are strings when present (rule 51 +\n // chatgpt-codex-connector P2: 'Reject non-string search patterns\n // instead of dropping filters'). A non-string like namePattern: 42\n // has undefined .length, so the guard below would silently drop\n // the filter and return unrelated nodes. Reject up-front instead.\n if (\n (query.label !== undefined && typeof query.label !== \"string\") ||\n (query.namePattern !== undefined &&\n typeof query.namePattern !== \"string\") ||\n (query.filePattern !== undefined &&\n typeof query.filePattern !== \"string\")\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n\n // Wrap DB operations in try/catch (cursor Bugbot: 'SQLite errors\n // escape read APIs'). Same shape as schemaStats/deadCode/traverse.\n try {\n // Build a single parameterized query. The degree subquery counts\n // inbound + outbound edges per node; the WHERE clause AND-combines\n // every present filter; the LIMIT is bound last. LIKE patterns are\n // bound as-is so SQLite interprets `%` and `_`.\n const params: (string | number)[] = [];\n const where: string[] = [];\n if (query.label !== undefined && query.label.length > 0) {\n where.push(\"n.label = ?\");\n params.push(query.label);\n }\n if (query.namePattern !== undefined && query.namePattern.length > 0) {\n where.push(\"n.name LIKE ? COLLATE NOCASE\");\n params.push(query.namePattern);\n }\n if (query.filePattern !== undefined && query.filePattern.length > 0) {\n where.push(\"f.path LIKE ? COLLATE NOCASE\");\n params.push(query.filePattern);\n }\n // Degree filter on the computed subquery. We re-emit the COUNT\n // subquery in the WHERE clause rather than using HAVING — SQLite\n // requires HAVING to be paired with GROUP BY, and this query has\n // no GROUP BY (each row is one node). The correlated subquery is\n // evaluated per-row; SQLite's planner caches it cheaply for the\n // graph sizes we target (issue #1552 scale targets).\n if (query.degreeMin !== undefined) {\n where.push(\n \"(SELECT COUNT(*) FROM edges e WHERE e.src = n.id OR e.dst = n.id) >= ?\",\n );\n params.push(query.degreeMin);\n }\n if (query.degreeMax !== undefined) {\n where.push(\n \"(SELECT COUNT(*) FROM edges e WHERE e.src = n.id OR e.dst = n.id) <= ?\",\n );\n params.push(query.degreeMax);\n }\n\n params.push(limit);\n const sql = `SELECT n.id AS node_id, n.qualified_name, n.name, n.label,\n f.path AS file_path,\n (SELECT COUNT(*) FROM edges e WHERE e.src = n.id OR e.dst = n.id) AS degree\n FROM nodes n\n JOIN files f ON n.file_id = f.id\n ${where.length > 0 ? \"WHERE \" + where.join(\" AND \") : \"\"}\n ORDER BY degree DESC, n.qualified_name ASC\n LIMIT ?`;\n const rows = expectRows<{\n node_id: string;\n qualified_name: string;\n name: string;\n label: string;\n file_path: string;\n degree: number;\n }>(this.db.prepare(sql).all(...params), [\n \"node_id\",\n \"qualified_name\",\n \"name\",\n \"label\",\n \"file_path\",\n \"degree\",\n ]);\n const hits: SearchHit[] = rows.map((r) => ({\n nodeId: r.node_id,\n qualifiedName: r.qualified_name,\n name: r.name,\n label: r.label,\n filePath: r.file_path,\n degree: r.degree,\n }));\n return { ok: true, hits };\n } catch (error) {\n logWriteFailure(error);\n return classifyReadError(error) as SearchResult;\n }\n }\n\n /**\n * Aggregate counts over the whole graph. Single round-trip: one\n * scalar per metric, two GROUP BY queries for the by-label /\n * by-type histograms.\n */\n schemaStats(): SchemaStatsResult {\n if (this.closed) return { ok: false, code: \"store_closed\" };\n try {\n const fileCount = expectRow<{ c: number }>(\n this.db.prepare(\"SELECT COUNT(*) AS c FROM files\").get(),\n [\"c\"],\n );\n const nodeCount = expectRow<{ c: number }>(\n this.db.prepare(\"SELECT COUNT(*) AS c FROM nodes\").get(),\n [\"c\"],\n );\n const edgeCount = expectRow<{ c: number }>(\n this.db.prepare(\"SELECT COUNT(*) AS c FROM edges\").get(),\n [\"c\"],\n );\n const labelRows = expectRows<{ label: string; c: number }>(\n this.db\n .prepare(\n \"SELECT label, COUNT(*) AS c FROM nodes GROUP BY label ORDER BY label\",\n )\n .all(),\n [\"label\", \"c\"],\n );\n const typeRows = expectRows<{ type: string; c: number }>(\n this.db\n .prepare(\n \"SELECT type, COUNT(*) AS c FROM edges GROUP BY type ORDER BY type\",\n )\n .all(),\n [\"type\", \"c\"],\n );\n const nodesByLabel: Record<string, number> = {};\n for (const r of labelRows) nodesByLabel[r.label] = r.c;\n const edgesByType: Record<string, number> = {};\n for (const r of typeRows) edgesByType[r.type] = r.c;\n return {\n ok: true,\n stats: {\n files: fileCount?.c ?? 0,\n nodes: nodeCount?.c ?? 0,\n edges: edgeCount?.c ?? 0,\n nodesByLabel,\n edgesByType,\n },\n };\n } catch (error) {\n logWriteFailure(error);\n const failure = classifyReadError(error);\n return failure as unknown as SchemaStatsResult;\n }\n }\n\n /**\n * Dead-code candidates: nodes with zero inbound\n * {@link DEAD_CODE_EXCLUSION.INBOUND_USAGE_EDGE_TYPES} edges, excluding\n * nodes whose `node_attributes` row marks them exported / route-handler\n * AND nodes whose file path matches the test / entry-point patterns\n * in {@link DEAD_CODE_EXCLUSION}.\n *\n * The exclusion criteria live in the named constant — not in\n * ad-hoc WHERE clauses (rule 53 analog). The stored flags come from\n * the write pipeline's `upsertFileAttributes` pass, which the IR's\n * `exports` and `routes` arrays feed.\n */\n deadCode(): DeadCodeResult {\n if (this.closed) return { ok: false, code: \"store_closed\" };\n try {\n // The inbound-usage edge-type list comes from the named\n // constant; bind it as a parameterized IN (...) so the criteria\n // are auditable in one place and a future edge-type add is a\n // one-line constant extension.\n const usageTypes = DEAD_CODE_EXCLUSION.INBOUND_USAGE_EDGE_TYPES;\n const typePlaceholders = usageTypes.map(() => \"?\").join(\", \");\n // LEFT JOIN node_attributes so a missing row reads as (0, 0).\n // COALESCE is belt-and-braces — the LEFT JOIN already produces\n // NULL for missing rows, and `NULL OR ...` would surface NULL\n // in the WHERE; explicit COALESCE collapses NULL → 0.\n // Self-edges (e.src <> n.id) are excluded from inbound usage: a\n // private recursive helper whose only edge is `fn → fn` is\n // unreachable from the rest of the program, so the self-call\n // must not count as external usage — otherwise deadCode() omits\n // it and the unreferenced symbol stays invisible\n // (chatgpt-codex-connector P2: 'Ignore self-edges in dead-code\n // reachability').\n const sql = `SELECT n.id AS node_id, n.qualified_name, n.name, n.label,\n f.path AS file_path\n FROM nodes n\n JOIN files f ON n.file_id = f.id\n LEFT JOIN node_attributes a ON a.node_id = n.id\n WHERE NOT EXISTS (\n SELECT 1 FROM edges e\n WHERE e.dst = n.id\n AND e.src <> n.id\n AND e.type IN (${typePlaceholders})\n )\n AND COALESCE(a.is_exported, 0) = 0\n AND COALESCE(a.is_route_handler, 0) = 0\n ORDER BY n.qualified_name ASC`;\n const rows = expectRows<{\n node_id: string;\n qualified_name: string;\n name: string;\n label: string;\n file_path: string;\n }>(this.db.prepare(sql).all(...usageTypes), [\n \"node_id\",\n \"qualified_name\",\n \"name\",\n \"label\",\n \"file_path\",\n ]);\n // Apply path-based exclusions in JS — SQLite's regex support is\n // opt-in and inconsistent across builds; doing it here keeps the\n // exclusion logic entirely in the named constant.\n const hits: DeadCodeHit[] = [];\n for (const r of rows) {\n if (isExcludedByPath(r.file_path)) continue;\n hits.push({\n nodeId: r.node_id,\n qualifiedName: r.qualified_name,\n name: r.name,\n label: r.label,\n filePath: r.file_path,\n });\n }\n return { ok: true, hits };\n } catch (error) {\n logWriteFailure(error);\n const failure = classifyReadError(error);\n return failure as unknown as DeadCodeResult;\n }\n }\n\n /**\n * Read a symbol's source span from disk. The store NEVER persists\n * file contents (privacy + DB size — issue #1552 design); this\n * method resolves `files.path` against {@link GraphStoreOptions.repoRoot}\n * and slices the half-open `[startByte, endByte)` span from the\n * on-disk bytes.\n */\n async snippetFor(query: SnippetQuery): Promise<SnippetResult> {\n if (this.closed) return { ok: false, code: \"store_closed\" };\n // Guard the query object before any dereference (see traverse).\n if (query == null || typeof query !== \"object\") {\n return { ok: false, code: \"invalid_query\" };\n }\n // Prefer a deterministic node id when supplied — it is unique, so it\n // never hits the qualified-name ambiguity path.\n const hasNodeId = typeof query.nodeId === \"string\" && query.nodeId.length > 0;\n if (\n !hasNodeId &&\n (typeof query.qualifiedName !== \"string\" ||\n query.qualifiedName.length === 0)\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n // Validate contextLines is a non-negative integer when present,\n // consistent with traverse's maxDepth and the other numeric read\n // fields. The old path coerced (Math.floor), so `1.9` silently\n // became 1 and `\"2\"` became 2 — reject malformed values up-front\n // instead (chatgpt-codex-connector P2: 'Reject invalid context\n // line counts').\n if (\n query.contextLines !== undefined &&\n (typeof query.contextLines !== \"number\" ||\n !Number.isInteger(query.contextLines) ||\n query.contextLines < 0)\n ) {\n return { ok: false, code: \"invalid_query\" };\n }\n const root = typeof query.repoRoot === \"string\" && query.repoRoot.length > 0\n ? query.repoRoot\n : this.repoRoot;\n if (root === undefined) {\n return { ok: false, code: \"repo_root_unset\" };\n }\n // Wrap the DB lookup in try/catch (cursor Bugbot: 'SQLite errors\n // escape read APIs'). The file read below has its own catch.\n let rows: {\n id: string;\n qualified_name: string;\n file_path: string;\n span_start: number;\n span_end: number;\n lang: string;\n }[];\n try {\n rows = expectRows<{\n id: string;\n qualified_name: string;\n file_path: string;\n span_start: number;\n span_end: number;\n lang: string;\n }>(\n this.db\n .prepare(\n `SELECT n.id, n.qualified_name, n.span_start, n.span_end, n.lang,\n f.path AS file_path\n FROM nodes n JOIN files f ON n.file_id = f.id\n WHERE ${hasNodeId ? \"n.id = ?\" : \"n.qualified_name = ?\"}`,\n )\n .all(hasNodeId ? query.nodeId : query.qualifiedName),\n [\"id\", \"qualified_name\", \"file_path\", \"span_start\", \"span_end\", \"lang\"],\n );\n } catch (error) {\n logWriteFailure(error);\n return classifyReadError(error) as unknown as SnippetResult;\n }\n if (rows.length === 0) return { ok: false, code: \"not_found\" };\n if (rows.length > 1) return { ok: false, code: \"ambiguous_name\" };\n const node = rows[0]!;\n const absolutePath = path.resolve(root, node.file_path);\n // Read the file from disk and slice the span. readFile is the\n // single fs call — no streaming, no mmap, just one allocation per\n // request. The store caches nothing; the caller may.\n let bytes: Buffer;\n try {\n bytes = await readFile(absolutePath);\n } catch (error) {\n logWriteFailure(error);\n return { ok: false, code: \"read_failed\" };\n }\n // Half-open [startByte, endByte). Guard endByte ≤ buffer.length\n // so a stale span after a file edit does not throw OutOfRange.\n const start = Math.max(0, node.span_start);\n const end = Math.min(bytes.length, node.span_end);\n if (start > end) {\n // The file shrank below the span — return an empty snippet\n // rather than throw; the caller can decide whether to re-ingest.\n return {\n ok: true,\n qualifiedName: node.qualified_name,\n filePath: node.file_path,\n absolutePath,\n startByte: node.span_start,\n endByte: node.span_end,\n text: \"\",\n lang: node.lang,\n };\n }\n let text = bytes.subarray(start, end).toString(\"utf8\");\n // Optional context lines (line-aligned expansion). contextLines\n // is bounded to a sane cap so a caller cannot ask for megabytes\n // of surrounding code.\n const ctx = query.contextLines ?? 0;\n if (ctx > 0) {\n const MAX_CTX = 200;\n const contextLines = Math.min(Math.max(0, Math.floor(ctx)), MAX_CTX);\n if (contextLines > 0) {\n // Line-aligned expansion. For contextLines=N we include the N\n // full lines preceding the span's line and the N full lines\n // following the span's line. Walk backward from `start`,\n // skipping (contextLines) line-end newlines, then walk to the\n // start of the (contextLines+1)th line back; walk forward\n // from `end` symmetrically.\n //\n // The first newline we hit going backward is the END of the\n // span's own line, NOT a context line — so we need to count\n // `contextLines` newlines after that boundary to find where\n // the context region begins. Concrete example for N=1:\n // \"...line one\\nline two\\n...\" with span starting at \"line two\"\n // walking back from start of \"line two\", we hit \\n (end of\n // \"line one\"). The line \"line one\" IS the context. Its start\n // is one further newline back (or buffer start).\n let lineStart = start;\n // Move lineStart to the beginning of the line containing `start`.\n while (lineStart > 0 && bytes[lineStart - 1] !== 0x0a) {\n lineStart -= 1;\n }\n // For each of contextLines, jump past the newline at\n // lineStart - 1 and walk to the previous line's start.\n for (let i = 0; i < contextLines && lineStart > 0; i += 1) {\n // Step past the newline ending the prior line.\n lineStart -= 1;\n // Walk to the start of THAT line.\n while (lineStart > 0 && bytes[lineStart - 1] !== 0x0a) {\n lineStart -= 1;\n }\n }\n let lineEnd = end;\n // Move lineEnd to the end of the line containing `end`\n // (inclusive of the trailing newline if present).\n while (lineEnd < bytes.length && bytes[lineEnd] !== 0x0a) {\n lineEnd += 1;\n }\n if (lineEnd < bytes.length && bytes[lineEnd] === 0x0a) {\n lineEnd += 1;\n }\n // For each of contextLines, advance past one more line.\n for (let i = 0; i < contextLines && lineEnd < bytes.length; i += 1) {\n while (lineEnd < bytes.length && bytes[lineEnd] !== 0x0a) {\n lineEnd += 1;\n }\n if (lineEnd < bytes.length && bytes[lineEnd] === 0x0a) {\n lineEnd += 1;\n }\n }\n text = bytes.subarray(lineStart, lineEnd).toString(\"utf8\");\n }\n }\n return {\n ok: true,\n qualifiedName: node.qualified_name,\n filePath: node.file_path,\n absolutePath,\n startByte: node.span_start,\n endByte: node.span_end,\n text,\n lang: node.lang,\n };\n }\n\n // ──────────────────────────────────────────────────────────────────────\n // Semantic layer (issue #1556): symbol_vectors table read/write.\n // The db is private; these methods are the ONLY surface the semantic\n // indexer/query path uses. Vectors are float32 BLOBs; content_hash is\n // the canonical-text hash (rule 37 — the cache invalidation key).\n // ──────────────────────────────────────────────────────────────────────\n\n /**\n * Upsert one symbol vector. Idempotent on (node_id, model_id). The\n * caller (the semantic indexer) has ALREADY decided to re-embed (the\n * content_hash differs from the cached row); this method just persists.\n */\n async writeSymbolVector(input: {\n readonly nodeId: string;\n readonly modelId: string;\n readonly contentHash: string;\n readonly dims: number;\n readonly vector: Float32Array;\n }): Promise<boolean> {\n // Honor the closing flag (not just closed) and serialize via the write\n // queue, matching upsertFileBatch / upsertEdges / clearSemanticSimilarToEdges\n // — otherwise concurrent graph ingestion can interleave a vector upsert\n // with a transactional node delete (cursor Bugbot: 'Vector writes ignore\n // closing flag' + 'Vector writes bypass write queue').\n if (this.closed || this.closing) return false;\n const buf = Buffer.from(input.vector.buffer, input.vector.byteOffset, input.vector.byteLength);\n await this.queue.schedule(async () => {\n this.db\n .prepare(\n `INSERT INTO symbol_vectors (node_id, model_id, content_hash, dims, vector)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(node_id, model_id) DO UPDATE SET\n content_hash = excluded.content_hash,\n dims = excluded.dims,\n vector = excluded.vector`,\n )\n .run(input.nodeId, input.modelId, input.contentHash, input.dims, buf);\n });\n return true;\n }\n\n /**\n * Read one vector row by (node_id, model_id). Returns null when absent.\n * Used by the indexer's cache-check path (skip re-embed when content_hash\n * matches) and by the cache-hit test.\n */\n readSymbolVector(\n nodeId: string,\n modelId: string,\n ): { readonly contentHash: string; readonly dims: number; readonly vector: Float32Array } | null {\n if (this.closed) return null;\n const row = expectRow<{ content_hash: string; dims: number; vector: Uint8Array }>(\n this.db\n .prepare(\n `SELECT content_hash, dims, vector FROM symbol_vectors\n WHERE node_id = ? AND model_id = ?`,\n )\n .get(nodeId, modelId),\n [\"content_hash\", \"dims\", \"vector\"],\n );\n if (!row) return null;\n return {\n contentHash: row.content_hash,\n dims: row.dims,\n vector: new Float32Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength / 4),\n };\n }\n\n /**\n * Read every vector row for a given model. Used by brute-force cosine\n * retrieval (SIMILAR_TO confirmation + semantic_query). Returns node\n * metadata alongside the vector so callers can hydrate hits without a\n * second round-trip.\n */\n readAllSymbolVectors(modelId: string): readonly {\n readonly nodeId: string;\n readonly qualifiedName: string;\n readonly filePath: string;\n readonly kind: string;\n readonly dims: number;\n readonly vector: Float32Array;\n readonly contentHash: string;\n }[] {\n if (this.closed) return [];\n const rows = expectRows<{\n node_id: string;\n qualified_name: string;\n label: string;\n file_path: string;\n dims: number;\n vector: Uint8Array;\n content_hash: string;\n }>(\n this.db\n .prepare(\n `SELECT sv.node_id, sv.dims, sv.vector, sv.content_hash,\n n.qualified_name, n.label, f.path AS file_path\n FROM symbol_vectors sv\n JOIN nodes n ON sv.node_id = n.id\n JOIN files f ON n.file_id = f.id\n WHERE sv.model_id = ?`,\n )\n .all(modelId),\n [\"node_id\", \"qualified_name\", \"label\", \"file_path\", \"dims\", \"vector\", \"content_hash\"],\n );\n return rows.map((r) => ({\n nodeId: r.node_id,\n qualifiedName: r.qualified_name,\n filePath: r.file_path,\n kind: r.label,\n dims: r.dims,\n contentHash: r.content_hash,\n vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.vector.byteLength / 4),\n }));\n }\n\n /**\n * Delete vector rows for a set of node ids (all models). Used by the\n * cache-invalidation path when a symbol's canonical text changed AND\n * it could not be re-embedded (provider gone) — the stale vector must\n * not survive to pollute cosine retrieval. Cascades via the schema's\n * ON DELETE CASCADE on nodes(id) when a node is pruned, so this method\n * is only for the targeted-invalidation path.\n */\n async deleteSymbolVectors(nodeIds: readonly string[]): Promise<void> {\n // Same closing-flag + write-queue discipline as writeSymbolVector\n // (cursor Bugbot: 'Vector writes ignore closing flag' + 'Vector writes\n // bypass write queue').\n if (this.closed || this.closing || nodeIds.length === 0) return;\n await this.queue.schedule(async () => {\n this.runChunkedDelete(\n \"DELETE FROM symbol_vectors WHERE node_id IN (%PH%)\",\n nodeIds,\n );\n });\n }\n\n /**\n * Remove every SIMILAR_TO edge written by the semantic similarity\n * pipeline (type 'SIMILAR_TO', provenance 'semantic'). The pipeline\n * recomputes the FULL near-clone edge set on each run, so callers MUST\n * clear the prior set before upserting the new one — otherwise an edge\n * between two symbols that stopped being similar survives indefinitely\n * and graph traversal keeps reporting a stale clone relationship\n * (chatgpt-codex-connector P2: 'Replace old SIMILAR_TO edges on\n * recompute'). Scoped to provenance 'semantic' so non-semantic edges\n * are untouched. Serialized via the write queue so it cannot interleave\n * a concurrent file-batch edge upsert.\n */\n async clearSemanticSimilarToEdges(): Promise<void> {\n if (this.closed || this.closing) return;\n await this.queue.schedule(async () => {\n this.db\n .prepare(\"DELETE FROM edges WHERE type = ? AND provenance = ?\")\n .run(\"SIMILAR_TO\", \"semantic\");\n });\n }\n\n /**\n * Read every node with its file path + span, for the semantic indexer.\n * The indexer reads source text from disk (via repoRoot) and builds\n * canonical text per node. Returns kind + qualified_name + span so the\n * indexer can reconstruct the SymbolIR-equivalent without a second\n * join. Ordered by qualified_name for deterministic processing order.\n */\n readNodesForSemantic(): readonly {\n readonly nodeId: string;\n readonly qualifiedName: string;\n readonly kind: string;\n readonly filePath: string;\n readonly startByte: number;\n readonly endByte: number;\n readonly lang: string;\n }[] {\n if (this.closed) return [];\n const rows = expectRows<{\n id: string;\n qualified_name: string;\n label: string;\n file_path: string;\n span_start: number;\n span_end: number;\n lang: string;\n }>(\n this.db\n .prepare(\n `SELECT n.id, n.qualified_name, n.label, n.span_start, n.span_end, n.lang,\n f.path AS file_path\n FROM nodes n JOIN files f ON n.file_id = f.id\n ORDER BY n.qualified_name ASC`,\n )\n .all(),\n [\"id\", \"qualified_name\", \"label\", \"file_path\", \"span_start\", \"span_end\", \"lang\"],\n );\n return rows.map((r) => ({\n nodeId: r.id,\n qualifiedName: r.qualified_name,\n kind: r.label,\n filePath: r.file_path,\n startByte: r.span_start,\n endByte: r.span_end,\n lang: r.lang,\n }));\n }\n\n /**\n * Read the callers and callees of a node by qualified name, for\n * semantic_query hydration (the issue: hydrate each hit with graph\n * context — defining file, direct callers/callees).\n */\n readNeighbors(\n qualifiedName: string,\n ): { readonly callers: readonly string[]; readonly callees: readonly string[] } {\n if (this.closed) return { callers: [], callees: [] };\n // Resolve the node id first.\n const nodeRow = expectRow<{ id: string }>(\n this.db\n .prepare(\"SELECT id FROM nodes WHERE qualified_name = ?\")\n .get(qualifiedName),\n [\"id\"],\n );\n if (!nodeRow) return { callers: [], callees: [] };\n const id = nodeRow.id;\n // Callers: nodes that CALL this node (edges where dst = id, type CALLS).\n const callerRows = expectRows<{ qualified_name: string }>(\n this.db\n .prepare(\n `SELECT n.qualified_name FROM edges e\n JOIN nodes n ON e.src = n.id\n WHERE e.dst = ? AND e.type = 'CALLS'`,\n )\n .all(id),\n [\"qualified_name\"],\n );\n // Callees: nodes this node CALLS (edges where src = id, type CALLS).\n const calleeRows = expectRows<{ qualified_name: string }>(\n this.db\n .prepare(\n `SELECT n.qualified_name FROM edges e\n JOIN nodes n ON e.dst = n.id\n WHERE e.src = ? AND e.type = 'CALLS'`,\n )\n .all(id),\n [\"qualified_name\"],\n );\n return {\n callers: callerRows.map((r) => r.qualified_name),\n callees: calleeRows.map((r) => r.qualified_name),\n };\n }\n\n /**\n * Read callers/callees by node id directly (avoids the qualified-name\n * ambiguity when duplicate names exist across files). Used by\n * semantic_query hydration (chatgpt-codex-connector: 'Use the hit node\n * id when hydrating neighbors').\n */\n readNeighborsByNodeId(\n nodeId: string,\n ): { readonly callers: readonly string[]; readonly callees: readonly string[] } {\n if (this.closed) return { callers: [], callees: [] };\n const callerRows = expectRows<{ qualified_name: string }>(\n this.db\n .prepare(\n `SELECT n.qualified_name FROM edges e\n JOIN nodes n ON e.src = n.id\n WHERE e.dst = ? AND e.type = 'CALLS'`,\n )\n .all(nodeId),\n [\"qualified_name\"],\n );\n const calleeRows = expectRows<{ qualified_name: string }>(\n this.db\n .prepare(\n `SELECT n.qualified_name FROM edges e\n JOIN nodes n ON e.dst = n.id\n WHERE e.src = ? AND e.type = 'CALLS'`,\n )\n .all(nodeId),\n [\"qualified_name\"],\n );\n return {\n callers: callerRows.map((r) => r.qualified_name),\n callees: calleeRows.map((r) => r.qualified_name),\n };\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Node id hashing — sorted key material (rule 23/38).\n// ──────────────────────────────────────────────────────────────────────────\n\nexport interface NodeIdInput {\n qualifiedName: string;\n filePath: string;\n label: string;\n}\n\n/**\n * sha256 over the sorted key material. The exact form MUST match between\n * ingest and lookup; tests assert this. Sort is stable (string compare),\n * no separators needed — the three fields are concatenated with a length\n * prefix so collision space is unambiguous.\n */\nexport function nodeIdFor(input: NodeIdInput): string {\n const fields = [\n [\"qualifiedName\", input.qualifiedName],\n [\"filePath\", input.filePath],\n [\"label\", input.label],\n ]\n .map(([k, v]) => [k as string, String(v)] as [string, string])\n .sort(([a], [b]) => a.localeCompare(b));\n const hash = createHash(\"sha256\");\n for (const [k, v] of fields) {\n hash.update(`${k.length}:${k}:`);\n hash.update(`${v.length}:${v}:`);\n }\n return hash.digest(\"hex\");\n}\n\n/**\n * Resolve a qualified_name to its deterministic node id.\n *\n * `inBatch` is the per-file map built from `nodes` rows owned by the\n * edge's source file (the FileIR.path the edge came in on). The\n * DB fallback is for cross-file edges whose src/dst lives in a\n * DIFFERENT file (in the same batch or a prior batch). Node\n * identity is the full `(qualifiedName, filePath, label)` triple\n * (see `nodeIdFor`), so a qualified_name match alone is ambiguous\n * when two files declare the same symbol. The fallback uses\n * `ORDER BY file_id, id` to pick deterministically, but only when\n * exactly one row matches — multiple matches return `undefined`\n * and the edge is dropped at insert time (per the dangling-edge\n * policy; the caller is responsible for the batch's canonical\n * file set). This is the conservative call for a write pipeline\n * whose caller knows the canonical file set on each batch\n * (rule 11, 40 — chatgpt-codex-connector P2 + cursor Bugbot\n * #1380bc89).\n */\nfunction resolveNodeId(\n qualifiedName: string,\n inBatch: Map<string, string>,\n db: BetterSqlite3Database,\n): string | undefined {\n const local = inBatch.get(qualifiedName);\n if (local) return local;\n const rows = expectRows<{ id: string; file_id: number }>(\n db\n .prepare(\n \"SELECT id, file_id FROM nodes WHERE qualified_name = ? ORDER BY file_id, id\",\n )\n .all(qualifiedName),\n [\"id\", \"file_id\"],\n );\n if (rows.length === 0) return undefined;\n if (rows.length > 1) {\n // Ambiguous — drop the edge rather than attach it to the wrong\n // node. Callers needing disambiguation should include the target\n // file in the same batch (the per-file map then wins) or extend\n // EdgeIR with file identity material.\n return undefined;\n }\n return rows[0]?.id;\n}\n\n/**\n * Resolve a dst node constrained to a path hint (issue #1894 review): the\n * node's file path must be the hint verbatim, `<hint>.<ext>`,\n * `<hint>/index.<ext>`, or `<hint>/__init__.<ext>` (Python packages) —\n * where `<ext>` is a SINGLE dot-free extension segment, so `main.test.ts`\n * and `main.d.ts` never satisfy a `main` hint (they are not what a module\n * resolver would load for `./main`). Candidate rows are fetched by\n * qualified name and the path shape is checked in JS: no LIKE/GLOB, so\n * hint characters are never pattern metacharacters. Zero or multiple\n * matches return `undefined` — the edge is dropped rather than guessed\n * (same conservative policy as {@link resolveNodeId}).\n */\n/**\n * Language families and the file extensions a module resolver in that\n * family accepts (issue #1894 round 13). A polyglot repo cannot let a\n * JS import bind a same-named .py file: constrain by importer language.\n */\nconst LANG_FAMILY_EXTENSIONS: Record<string, ReadonlySet<string>> = {\n js: new Set([\"ts\", \"tsx\", \"js\", \"jsx\", \"mjs\", \"cjs\", \"mts\", \"cts\"]),\n python: new Set([\"py\", \"pyi\"]),\n ruby: new Set([\"rb\"]),\n go: new Set([\"go\"]),\n rust: new Set([\"rs\"]),\n php: new Set([\"php\"]),\n java: new Set([\"java\"]),\n csharp: new Set([\"cs\"]),\n cpp: new Set([\"cc\", \"cpp\", \"cxx\", \"c\", \"hh\", \"hpp\", \"hxx\", \"h\"]),\n kotlin: new Set([\"kt\"]),\n swift: new Set([\"swift\"]),\n bash: new Set([\"sh\", \"bash\"]),\n};\n\nconst IMPORTER_LANG_FAMILY: Record<string, keyof typeof LANG_FAMILY_EXTENSIONS> = {\n typescript: \"js\",\n tsx: \"js\",\n javascript: \"js\",\n python: \"python\",\n ruby: \"ruby\",\n go: \"go\",\n rust: \"rust\",\n php: \"php\",\n java: \"java\",\n csharp: \"csharp\",\n c: \"cpp\",\n cpp: \"cpp\",\n kotlin: \"kotlin\",\n swift: \"swift\",\n bash: \"bash\",\n};\n\nfunction resolveNodeIdWithPathHint(\n qualifiedName: string,\n pathHint: string,\n db: BetterSqlite3Database,\n importerLanguage?: string,\n): string | undefined {\n const family = importerLanguage ? IMPORTER_LANG_FAMILY[importerLanguage] : undefined;\n const allowedExts = family ? LANG_FAMILY_EXTENSIONS[family] : undefined;\n const rows = expectRows<{ id: string; path: string }>(\n db\n .prepare(\n `SELECT n.id, f.path FROM nodes n JOIN files f ON n.file_id = f.id\n WHERE n.qualified_name = ?\n ORDER BY f.path, n.id`,\n )\n .all(qualifiedName),\n [\"id\", \"path\"],\n );\n const matches = rows.filter((row) => {\n if (row.path === pathHint) {\n // Language filter still applies on exact match: a TS import whose\n // normalized hint happens to equal a bare directory name must not\n // bind a same-named .py file (codex review round 15).\n if (allowedExts) {\n return false; // bare hint with no extension cannot match a family\n }\n return true;\n }\n if (!row.path.startsWith(pathHint)) return false;\n const rest = row.path.slice(pathHint.length);\n const m = /^(?:\\.([^./]+)|\\/(?:index|__init__)\\.([^./]+))$/.exec(rest);\n if (!m) return false;\n const ext = m[1] ?? m[2];\n // When the importer's language family is known, only that family's\n // extensions may satisfy the hint; otherwise any single extension.\n return !allowedExts || allowedExts.has(ext);\n });\n if (matches.length !== 1) return undefined;\n return matches[0]?.id;\n}\n\n/**\n * Resolve a standalone-edge endpoint by content-derived node id (issue #1677).\n *\n * `nodes.id` is the PRIMARY KEY, so the lookup is unique and unambiguous —\n * this is what lets a SIMILAR_TO edge between two same-qualified-name symbols\n * resolve where the qualified-name fallback would be ambiguous. The supplied\n * `qualifiedName` is validated against the row: a stale or mismatched\n * id+qname pair (stale body map, custom integration) returns `undefined` so\n * the caller skips the edge like a dangling endpoint instead of silently\n * writing an edge from the wrong node (chatgpt-codex-connector P2 — id/qname\n * consistency at the store boundary). `stmt` is the caller's prepared\n * `SELECT qualified_name FROM nodes WHERE id = ?`.\n */\nfunction resolveByNodeId(\n stmt: { get(...args: unknown[]): unknown },\n nodeId: string,\n qualifiedName: string,\n): string | undefined {\n const row = expectRow<{ qualified_name: string }>(stmt.get(nodeId), [\"qualified_name\"]);\n if (!row) return undefined;\n if (row.qualified_name !== qualifiedName) return undefined;\n return nodeId;\n}\n// ──────────────────────────────────────────────────────────────────────────\n// Error classification — tag SQLITE_BUSY / SQLITE_CORRUPT into the failure\n// shape (rule 34). better-sqlite3 surfaces them as `SqliteError` with `.code`.\n// ──────────────────────────────────────────────────────────────────────────\n\nfunction classifyError(error: unknown): GraphStoreFailure {\n const code = hasErrorCode(error) ? error.code : \"\";\n if (code === \"SQLITE_BUSY\" || code === \"SQLITE_LOCKED\") {\n return { ok: false, code: \"db_locked\" };\n }\n const msg = error instanceof Error ? error.message : String(error ?? \"\");\n if (\n code === \"SQLITE_CORRUPT\" ||\n code === \"SQLITE_NOTADB\" ||\n msg.includes(\"database disk image is malformed\")\n ) {\n return { ok: false, code: \"db_corrupt\" };\n }\n // Non-SQLite errors are NOT disk corruption — they are validation\n // failures (e.g. invalid edge provenance) or programming errors.\n // Conflating them with `db_corrupt` would tell the caller to stop\n // trusting the store for the wrong reason; re-throw so the caller\n // sees the real error (chatgpt-codex-connector P2).\n throw error instanceof Error ? error : new Error(String(error ?? \"\"));\n}\n\n/**\n * Read-path error classifier. Unlike {@link classifyError} (write\n * path), this is TOTAL — it never throws. Every unexpected error\n * maps to a tagged `db_error` failure so the read APIs\n * (`traverse`/`searchGraph`/`schemaStats`/`deadCode`/`snippetFor`)\n * honor their advertised discriminated-union contract: a caller that\n * exhaustively switches on `result.code` never observes a throw\n * (cursor Bugbot: 'Read APIs rethrow SQLite errors';\n * chatgpt-codex-connector P2: 'Return tagged failures from read\n * queries'). The write path keeps {@link classifyError} because its\n * validation throws (duplicate path, bad confidence, non-array\n * symbols) are intentional fail-loud contract violations that\n * callers and tests catch as rejections.\n *\n * `db_error` is deliberately distinct from `db_corrupt` so a generic\n * failure does not signal the caller to stop trusting the DB\n * (chatgpt-codex-connector P2: do not conflate unexpected errors\n * with corruption).\n */\nfunction classifyReadError(error: unknown): GraphStoreFailure {\n try {\n return classifyError(error);\n } catch {\n return { ok: false, code: \"db_error\" };\n }\n}\n\nfunction hasErrorCode(value: unknown): value is { code: string } {\n if (typeof value !== \"object\" || value === null) return false;\n if (!(\"code\" in value)) return false;\n const codeValue: unknown = (value as Record<string, unknown>)[\"code\"];\n return typeof codeValue === \"string\";\n}\n\n/**\n * Internal log for write failures. Never exposed to callers — the\n * public failure union only carries the code, so absolute paths\n * from `error.message` cannot leak into agents, HTTP responses, or\n * MCP tool results (rule 11).\n */\nfunction logWriteFailure(error: unknown): void {\n // eslint-disable-next-line no-console\n console.error(\n \"[coding-graph] write failure:\",\n error instanceof Error ? error.message : String(error ?? \"\"),\n );\n}\n\n/**\n * Reject non-canonical repo-relative paths at the store boundary. The\n * FileIR contract requires forward-slash, repo-relative paths; a caller\n * that emits `./src/a.ts`, backslashes, or an absolute path would hash\n * to a distinct files row + node id and leave duplicate/stale symbols a\n * later canonical ingest cannot match or prune\n * (chatgpt-codex-connector P2: 'Reject non-canonical file paths before\n * persisting').\n */\nfunction assertCanonicalFilePath(filePath: unknown): void {\n if (typeof filePath !== \"string\" || filePath.length === 0) {\n throw new Error(\n `graph-store: file path must be a non-empty string; received ${\n filePath === null ? \"null\" : typeof filePath\n }`,\n );\n }\n // Windows separators — the contract mandates forward slashes.\n if (filePath.includes(\"\\\\\")) {\n throw new Error(\n `graph-store: file path '${filePath}' must use forward slashes (backslash rejected — FileIR contract requires repo-relative POSIX paths)`,\n );\n }\n // Absolute POSIX path or a Windows drive root.\n if (filePath.startsWith(\"/\") || /^[A-Za-z]:[\\\\/]/.test(filePath)) {\n throw new Error(\n `graph-store: file path '${filePath}' must be repo-relative (absolute path rejected — FileIR contract requires repo-relative forward-slash paths)`,\n );\n }\n // `.` / `..` segments alias a canonical path (`./src/a.ts` vs\n // `src/a.ts`, or `src/../a.ts`) and would hash to a distinct files\n // row + node id, leaving duplicates the canonical ingest cannot\n // match or prune. Segment-based check avoids false positives on\n // names like `a..b.ts`.\n if (filePath.split(\"/\").some((segment) => segment === \".\" || segment === \"..\")) {\n throw new Error(\n `graph-store: file path '${filePath}' must be canonical (no '.' or '..' segments — FileIR contract requires repo-relative forward-slash paths)`,\n );\n }\n}\n\n/**\n * Reject malformed symbol spans before they are bound into span_start /\n * span_end. The FileIR contract documents half-open byte spans\n * `[startByte, endByte)`; a buggy parser or JSON caller can emit\n * startByte > endByte or non-integer values, and PR2 snippet/search\n * consumers will trust the offsets as-is, producing invalid source\n * slices. Reject at the boundary rather than persisting corrupt metadata\n * (chatgpt-codex-connector P2: 'Reject invalid symbol spans before\n * storing nodes'). Narrowing is done with typeof/in guards (no casts) so\n * the compiler verifies every access.\n */\nfunction assertValidSymbolSpan(sym: unknown, filePath: string): void {\n if (typeof sym !== \"object\" || sym === null) {\n throw new Error(\n `graph-store: file '${filePath}' has a non-object symbol; received ${\n sym === null ? \"null\" : typeof sym\n }`,\n );\n }\n if (!(\"span\" in sym)) {\n throw new Error(\n `graph-store: file '${filePath}' has a symbol with no span (FileIR contract requires startByte/endByte)`,\n );\n }\n const span: unknown = sym.span;\n if (\n typeof span !== \"object\" ||\n span === null ||\n !(\"startByte\" in span) ||\n !(\"endByte\" in span)\n ) {\n throw new Error(\n `graph-store: file '${filePath}' has a symbol with a malformed span — expected { startByte, endByte }; received ${JSON.stringify(span)}`,\n );\n }\n const startByte: unknown = span.startByte;\n const endByte: unknown = span.endByte;\n // typeof narrows unknown → number; Number.isInteger then rejects\n // NaN/Infinity, which typeof === \"number\" admits.\n if (\n typeof startByte !== \"number\" ||\n typeof endByte !== \"number\" ||\n !Number.isInteger(startByte) ||\n !Number.isInteger(endByte)\n ) {\n throw new Error(\n `graph-store: file '${filePath}' has a symbol with a non-integer span [${JSON.stringify(startByte)}, ${JSON.stringify(endByte)}) — startByte and endByte must be finite integers`,\n );\n }\n if (startByte < 0 || endByte < 0) {\n throw new Error(\n `graph-store: file '${filePath}' has a symbol with a negative span [${startByte}, ${endByte}) — byte offsets must be non-negative`,\n );\n }\n if (startByte > endByte) {\n throw new Error(\n `graph-store: file '${filePath}' has a symbol with startByte > endByte [${startByte}, ${endByte}) — half-open spans require startByte <= endByte`,\n );\n }\n}\n"],"mappings":"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","names":["SQLITE_VARIABLE_LIMIT"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MAX_TRAVERSE_PATHS_HOPS
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ABDBWCXU.js";
|
|
4
4
|
|
|
5
5
|
// src/cypher/query-parser.ts
|
|
6
6
|
var CYPHER_LABEL_TO_DB_LABEL = {
|
|
@@ -1039,4 +1039,4 @@ export {
|
|
|
1039
1039
|
executeAst,
|
|
1040
1040
|
executeCypher
|
|
1041
1041
|
};
|
|
1042
|
-
//# sourceMappingURL=chunk-
|
|
1042
|
+
//# sourceMappingURL=chunk-I4R6GAAA.js.map
|
package/dist/graph-store.d.ts
CHANGED
|
@@ -50,6 +50,23 @@ interface EdgeIR {
|
|
|
50
50
|
readonly srcNodeId?: string;
|
|
51
51
|
/** Optional content-derived destination node id — see {@link EdgeIR.srcNodeId}. */
|
|
52
52
|
readonly dstNodeId?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Repo-relative, extension-stripped path the dst must live in (issue
|
|
55
|
+
* #1894 review): derived from a relative import's module specifier. A
|
|
56
|
+
* hinted edge resolves ONLY among nodes whose file path matches the
|
|
57
|
+
* hint (`<hint>`, `<hint>.<ext>`, `<hint>/index.<ext>`, or
|
|
58
|
+
* `<hint>/__init__.<ext>`) — never via
|
|
59
|
+
* the global bare-name fallback — so `import { foo } from "./missing"`
|
|
60
|
+
* can never bind an unrelated same-named symbol elsewhere in the repo.
|
|
61
|
+
*/
|
|
62
|
+
readonly dstPathHint?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Language of the importing file (issue #1894 round 13): constrains the
|
|
65
|
+
* hinted dst's file extension to that language's module-resolution set
|
|
66
|
+
* so a polyglot repo cannot cross-bind a JS import to a same-named .py
|
|
67
|
+
* file.
|
|
68
|
+
*/
|
|
69
|
+
readonly dstImporterLanguage?: string;
|
|
53
70
|
}
|
|
54
71
|
/**
|
|
55
72
|
* Store input — the subset of @remnic/core's `FileIR` the store reads,
|
|
@@ -74,6 +91,16 @@ interface StoreFileIR {
|
|
|
74
91
|
readonly symbols: readonly SymbolIR[];
|
|
75
92
|
/** Store-specific edges derived from the IR by the caller. */
|
|
76
93
|
readonly edges?: readonly EdgeIR[];
|
|
94
|
+
/**
|
|
95
|
+
* When present, the stale-edge delete in `upsertFileEdges` is scoped to
|
|
96
|
+
* edges whose provenance is in this list: prior src-owned edges of OTHER
|
|
97
|
+
* provenances survive un-asserted (issue #1891). The reindex pipeline
|
|
98
|
+
* asserts `["heuristic"]` because a fresh parse says nothing about
|
|
99
|
+
* `trace`/`lsp` edges; deleting them on every re-ingest would destroy
|
|
100
|
+
* state the parse never contradicted (rule 25). Absent = legacy
|
|
101
|
+
* behavior: every stale src-owned edge is deleted.
|
|
102
|
+
*/
|
|
103
|
+
readonly assertedEdgeProvenances?: readonly EdgeProvenance[];
|
|
77
104
|
/**
|
|
78
105
|
* Per-file export list (mirrors core FileIR.exports). When present,
|
|
79
106
|
* the write pipeline marks every node in this file whose `name`
|
|
@@ -600,6 +627,26 @@ declare class GraphStore {
|
|
|
600
627
|
* (rule 40).
|
|
601
628
|
*/
|
|
602
629
|
upsertEdges(edges: readonly EdgeIR[]): Promise<UpsertEdgesResult>;
|
|
630
|
+
/**
|
|
631
|
+
* Retire stale LSP-provenance edges for a file (issue #1895).
|
|
632
|
+
*
|
|
633
|
+
* The LSP resolution pass re-derives edges from the CURRENT source on each
|
|
634
|
+
* run. After writing the new `lsp` edges for a file, this method deletes
|
|
635
|
+
* prior `lsp`-provenance edges owned by that file's nodes whose
|
|
636
|
+
* `(src, dst, type)` key is NOT in the asserted set. This is the LSP
|
|
637
|
+
* layer's side of the provenance-lifecycle contract: each layer owns its
|
|
638
|
+
* own stale-edge retirement (#1894 established that reindex's heuristic
|
|
639
|
+
* scope never touches `lsp` rows).
|
|
640
|
+
*
|
|
641
|
+
* Heuristic, trace, and semantic edges are never touched.
|
|
642
|
+
*
|
|
643
|
+
* @returns the number of retired edges.
|
|
644
|
+
*/
|
|
645
|
+
reconcileLspEdges(filePath: string, assertedEdges: ReadonlyArray<{
|
|
646
|
+
srcQualifiedName: string;
|
|
647
|
+
dstQualifiedName: string;
|
|
648
|
+
type: string;
|
|
649
|
+
}>): number;
|
|
603
650
|
/** Wait for pending writes to drain — test seam. */
|
|
604
651
|
drain(): Promise<void>;
|
|
605
652
|
/**
|