import { EdgeProvenance } from './graph-schema.js'; import { CodingGraphLanguage, SymbolIR, ExportIR, RouteIR } from '@remnic/core/coding/coding-graph-types'; export { CallSiteIR, CodingGraphLanguage, ExportIR, FileIR, ImportIR, RouteIR, SymbolIR } from '@remnic/core/coding/coding-graph-types'; import '@remnic/core/runtime/better-sqlite'; /** * Half-open byte span `[startByte, endByte)` — matches @remnic/core's * inline span type. Kept as a named alias for API consumers that import * `ByteSpan` from the store subpath (issue #1551 / rule 35). */ type ByteSpan = { readonly startByte: number; readonly endByte: number; }; /** * Symbol kind union — matches @remnic/core's `SymbolIR["kind"]` exactly * (core does not export this as a named type). */ type SymbolKind = "function" | "class" | "method" | "interface" | "enum" | "type" | "module"; /** * Store-specific edge — references nodes by `qualifiedName` so the store * can resolve them against the same batch's symbol set plus the on-disk * node table. PR1 only carries CALLS-style edges; PR2 adds the rest of * #1552's edge types. * * Optional `srcNodeId` / `dstNodeId` (issue #1677) carry the content- * derived node id (the same canonical hash form the store uses as * `nodes.id`, see `nodeIdFor`). When present, the standalone * `upsertEdges` path resolves the endpoint by `nodes.id` (unique) instead * of by qualified name, so a SIMILAR_TO edge between two symbols that * share a qualified name across files is persisted rather than dropped as * ambiguous. The qname-keyed file-batch path and the existing * `ambiguous … drops edges` behavior are unchanged. Only populated by * callers that originate edges from node-id-keyed pairs (the semantic * SIMILAR_TO pipeline); structural/trace edges keep the qname path. */ interface EdgeIR { /** Qualified name of the source node (caller / definition site). */ srcQualifiedName: string; /** Qualified name of the destination node (callee / type used). */ dstQualifiedName: string; type: string; confidence: number; provenance: EdgeProvenance; /** * Optional content-derived source node id (`nodes.id`). When present on * a standalone-edge upsert, the store resolves the endpoint by id * (unambiguous) instead of falling back to qualified-name resolution. */ readonly srcNodeId?: string; /** Optional content-derived destination node id — see {@link EdgeIR.srcNodeId}. */ readonly dstNodeId?: string; /** * Repo-relative, extension-stripped path the dst must live in (issue * #1894 review): derived from a relative import's module specifier. A * hinted edge resolves ONLY among nodes whose file path matches the * hint (``, `.`, `/index.`, or * `/__init__.`) — never via * the global bare-name fallback — so `import { foo } from "./missing"` * can never bind an unrelated same-named symbol elsewhere in the repo. */ readonly dstPathHint?: string; /** * Language of the importing file (issue #1894 round 13): constrains the * hinted dst's file extension to that language's module-resolution set * so a polyglot repo cannot cross-bind a JS import to a same-named .py * file. */ readonly dstImporterLanguage?: string; } /** * Store input — the subset of @remnic/core's `FileIR` the store reads, * plus the store-specific `edges` extension. A core `FileIR` (from * `ParseResult.ir`) is structurally assignable here: all required fields * (path, language, contentHash, symbols, imports, exports, callSites, * routes) match by name and readonly-ness. PR2 callers pass * `{ ...parseResult.ir, edges }` (or the bare IR when edges are absent) * with zero casts or field-name translation. * * PR2 adds optional `exports` and `routes` consumption: when present, * the write pipeline marks matching nodes in `node_attributes` so the * `deadCode()` query can exclude them via the * {@link DEAD_CODE_EXCLUSION} constant. Both fields are optional because * a PR1-era caller (or a JSON-IR caller that strips them) still ingests * cleanly — the dead-code query simply sees no exclusion flags. */ interface StoreFileIR { readonly path: string; readonly language: CodingGraphLanguage; readonly contentHash: string; readonly symbols: readonly SymbolIR[]; /** Store-specific edges derived from the IR by the caller. */ readonly edges?: readonly EdgeIR[]; /** * When present, the stale-edge delete in `upsertFileEdges` is scoped to * edges whose provenance is in this list: prior src-owned edges of OTHER * provenances survive un-asserted (issue #1891). The reindex pipeline * asserts `["heuristic"]` because a fresh parse says nothing about * `trace`/`lsp` edges; deleting them on every re-ingest would destroy * state the parse never contradicted (rule 25). Absent = legacy * behavior: every stale src-owned edge is deleted. */ readonly assertedEdgeProvenances?: readonly EdgeProvenance[]; /** * Per-file export list (mirrors core FileIR.exports). When present, * the write pipeline marks every node in this file whose `name` * matches an ExportIR.name as `is_exported=1` in `node_attributes`. * Name-matching is the conventional pattern: a parser that emits a * `export const foo` declaration also emits a SymbolIR named `foo` * (or omits it if foo is a non-symbol like a plain variable); the * dead-code query then excludes surviving exported symbols. */ readonly exports?: readonly ExportIR[]; /** * Per-file HTTP route declarations (mirrors core FileIR.routes). When * present, the write pipeline marks the node whose `qualifiedName` * equals `route.handlerQualifiedName` as `is_route_handler=1` in * `node_attributes`. Route handlers are reachable from HTTP traffic * regardless of whether any other indexed node CALLS them. */ readonly routes?: readonly RouteIR[]; } type GraphStoreFailureCode = "db_locked" | "db_corrupt" | "db_error" | "store_closed"; interface GraphStoreFailure { ok: false; code: GraphStoreFailureCode; } interface UpsertResult { path: string; fileId: number; nodeCount: number; edgeCount: number; /** * Dangling edges observed while deleting the file's prior subgraph * (cross-file edges whose `dst` belonged to a node owned by this file). * Per the PR1 dangling-edge policy in {@link graph-schema}, they are * DROPPED, not kept with a marker. Surfaced here so callers can log * the loss (rule 11, 40). */ droppedDanglingEdges: number; } interface UpsertSuccess { ok: true; results: UpsertResult[]; } type UpsertBatchResult = UpsertSuccess | GraphStoreFailure; /** * Result of {@link GraphStore.upsertEdges} — a standalone-edge write used by * the codegraph ingest_traces surface (issue #1554). `persisted` counts * edges actually inserted/updated; `skipped` counts edges whose src or dst * did not resolve to exactly one node (dangling-edge policy). */ interface UpsertEdgesSuccess { ok: true; persisted: number; skipped: number; } type UpsertEdgesResult = UpsertEdgesSuccess | GraphStoreFailure; /** Direction of traversal relative to the edge's src→dst orientation. */ type TraverseDirection = "outgoing" | "incoming" | "both"; /** * Iterative frontier BFS over the edges table. Cycle-safe via a JS * visited set keyed by node id; predictable memory regardless of graph * shape (recursive CTEs are the documented fallback if benchmarks ever * justify them — issue #1552 design section). */ interface TraverseQuery { /** * Start node. Accepts either a node id or a qualified name — the * store resolves a qualified name to its deterministic id via the * same `(qualifiedName, filePath, label)` identity used at ingest * time. When the qualified name is ambiguous (declared in more than * one file), the query is rejected with `code: "ambiguous_start"` * so the caller can pass an explicit node id instead. */ start: string; /** Default `"outgoing"`. */ direction?: TraverseDirection; /** * Edge types to follow (e.g. `["CALLS", "USES_TYPE"]`). When omitted * or empty, every edge type in the table is followed. Unknown edge * types simply contribute no rows — the query is not rejected * because the schema places no CHECK constraint on `edges.type`. */ edgeTypes?: readonly string[]; /** * Maximum BFS depth. Half-open: a node at depth == maxDepth IS * included; a node at depth maxDepth+1 is NOT (rule 35). The start * node itself sits at depth 0 and is always included in the result * set when it exists. A maxDepth of 0 returns just the start node. * MUST be a non-negative integer — invalid values are rejected with * `code: "invalid_query"` rather than silently clamped (rule 51). */ maxDepth: number; } interface TraverseHit { nodeId: string; qualifiedName: string; name: string; label: string; /** Repo-relative file path of the node (joined from files.path). */ filePath: string; /** BFS depth from the start node (start = 0). */ depth: number; } type TraverseResult = { ok: true; hits: TraverseHit[]; } | ({ ok: false; } & GraphStoreFailure) | { ok: false; code: "unknown_start" | "ambiguous_start" | "invalid_query"; }; /** * Default cap on the number of concrete paths {@link GraphStore.traversePaths} * enumerates before stopping and flagging `truncated`. Bounds the worst-case * exponential blowup of relationship-simple path enumeration on dense * subgraphs (issue #1650). Callers may override per-query via * {@link TraversePathsQuery.maxPaths}. */ declare const DEFAULT_TRAVERSE_PATHS_MAX = 10000; /** * Hard upper bound on {@link TraversePathsQuery.maxHops}. The DFS recurses * once per hop; an unbounded depth (e.g. a Cypher `*15000`) would overflow * the call stack before `maxPaths` could stop it. 1000 is ~100x any * realistic code-graph depth and recurses safely (chatgpt-codex-connector * P2: 'Avoid recursive DFS for deep bounded paths'). */ declare const MAX_TRAVERSE_PATHS_HOPS = 1000; /** * Path-enumerating traversal query (issue #1650). Mirrors {@link TraverseQuery} * but yields CONCRETE paths rather than BFS-shortest-depth reachability, so an * exact `*N` (N > 1) hop count is honored for nodes reachable at both a shorter * and a length-N path. */ interface TraversePathsQuery { /** Start node id or qualified name (same resolution rules as {@link TraverseQuery.start}). */ start: string; /** Default `"outgoing"`. */ direction?: TraverseDirection; /** Edge types to follow; omitted/empty means every type. */ edgeTypes?: readonly string[]; /** * Inclusive upper bound on enumerated path LENGTH (hop count). MUST be a * non-negative integer. A `maxHops` of 0 yields no paths (every enumerated * path has length >= 1); callers that need the length-0 trivial path add it * themselves. */ maxHops: number; /** * Inclusive LOWER bound on EMITTED path length (hop count). Defaults * to 1. The DFS still EXPLORES shorter prefixes to reach longer paths, * but only EMITS (and counts toward {@link maxPaths}) paths whose length * is in `[minHops, maxHops]` -- so an exact `*N` cap is not consumed by * the shorter prefixes (cursor Bugbot: 'Path cap ignores hop minimum'). * MUST be a positive integer (>= 1) when present. */ minHops?: number; /** * Safety cap on total enumerated paths. Defaults to * {@link DEFAULT_TRAVERSE_PATHS_MAX}. When the cap is reached, enumeration * STOPS and the result carries `truncated: true` so callers can detect that * the result is incomplete (e.g. to narrow the query or raise the cap). */ maxPaths?: number; } /** * One enumerated path. The endpoint node is fully resolved; the full node-id * sequence lets callers reconstruct the path (issue #1650 acceptance). */ interface TraversePathHit { nodeId: string; qualifiedName: string; name: string; label: string; filePath: string; /** Length of this path in hops (>= 1). */ length: number; /** Full path as node ids, start-first (`length + 1` entries). */ nodeIds: string[]; /** * Edge type per hop, parallel to {@link nodeIds} (`length` entries). Two * distinct relationships can connect the same node pair with different * types (the edges table is UNIQUE on `(src, dst, type)`); exposing the * type per hop lets callers distinguish those otherwise-identical-node * paths (chatgpt-codex-connector P2: 'Include edge identity in path * hits'). */ edgeTypes: string[]; /** * Per-hop edge endpoints, parallel to {@link nodeIds} (`length` entries). * Under `direction: "both"` antiparallel same-type edges (A->B and B->A) * yield distinct relationship-simple paths that share nodeIds + edgeTypes; * the src/dst per hop disambiguates which edge was traversed and in which * direction (chatgpt-codex-connector P2: 'Include edge endpoints in path * hits'). */ edgeEndpoints: Array<{ src: string; dst: string; }>; } type TraversePathsResult = { ok: true; hits: TraversePathHit[]; truncated: boolean; } | ({ ok: false; } & GraphStoreFailure) | { ok: false; code: "unknown_start" | "ambiguous_start" | "invalid_query"; }; /** * Structured node search. All filters are AND-combined; every filter * is optional so the bare query `{}` returns the whole graph (capped * by `limit`). Patterns use SQLite `LIKE` semantics — `%` matches any * run, `_` matches one character — applied case-insensitively via * `LIKE ... COLLATE NOCASE`. Patterns are parameter-bound, never * string-interpolated, so a `%`/`_` in user input cannot inject SQL. */ interface SearchQuery { /** Filter by node label (the symbol kind, e.g. `"function"`). */ label?: string; /** LIKE pattern on `nodes.name` (case-insensitive). */ namePattern?: string; /** LIKE pattern on `files.path` (case-insensitive). */ filePattern?: string; /** * Inclusive lower bound on total degree (in + out edge count). * Combined with {@link degreeMax} for a half-open? — no, inclusive * on both ends by convention since degree is an integer count, not * a span (rule 35 covers byte/time spans, not integer ranges). */ degreeMin?: number; /** Inclusive upper bound on total degree. */ degreeMax?: number; /** * Cap on returned rows. Default 100; clamped to [0, 1000]. A * `limit: 0` returns an empty `hits` array (rule 27 — guard the * slice/LIMIT against the zero case). */ limit?: number; } interface SearchHit { nodeId: string; qualifiedName: string; name: string; label: string; filePath: string; /** Total in + out edge count for this node. */ degree: number; } type SearchResult = { ok: true; hits: SearchHit[]; } | ({ ok: false; } & GraphStoreFailure) | { ok: false; code: "invalid_query"; }; /** Aggregate counts over the whole graph — single round-trip. */ interface SchemaStats { files: number; nodes: number; edges: number; /** Node count grouped by `label` (symbol kind). */ nodesByLabel: Record; /** Edge count grouped by `type`. */ edgesByType: Record; } type SchemaStatsResult = { ok: true; stats: SchemaStats; } | ({ ok: false; } & GraphStoreFailure); interface DeadCodeHit { nodeId: string; qualifiedName: string; name: string; label: string; filePath: string; } type DeadCodeResult = { ok: true; hits: DeadCodeHit[]; } | ({ ok: false; } & GraphStoreFailure); /** * Read a symbol's source span from disk. The store NEVER persists file * contents (privacy + DB size — issue #1552 design); `snippetFor` * resolves the node's `files.path` against {@link GraphStoreOptions.repoRoot} * and slices `[span_start, span_end)` from the on-disk bytes. */ interface SnippetQuery { /** * Qualified name to resolve. Optional when `nodeId` is supplied — the * guard requires at least one of the two. */ qualifiedName?: string; /** * Optional repo root override. When set, the snippet is read from this * root instead of the root captured at GraphStore.open() time, so a * caller that supplies its own repoRoot (e.g. semanticQuery) hydrates * snippets even when the store was opened without one (chatgpt-codex- * connector + cursor: 'Snippet hydration ignores query repoRoot'). */ repoRoot?: string; /** * Optional deterministic node id. When set, the lookup resolves by * `nodes.id` (unique) instead of `qualified_name`, so a hit whose * qualified name is duplicated across files still hydrates the exact * node's snippet instead of failing with `ambiguous_name` * (chatgpt-codex-connector P2: 'Hydrate snippets by node id as well'). */ nodeId?: string; /** * Optional lines of context to include before and after the span * (default 0 — exact span only). Context is line-aligned: the slice * expands to the nearest line boundary at each end. */ contextLines?: number; } interface SnippetSuccess { ok: true; qualifiedName: string; filePath: string; /** Absolute path the bytes were read from (`repoRoot/files.path`). */ absolutePath: string; startByte: number; endByte: number; /** The decoded source slice (UTF-8). */ text: string; lang: string; } type SnippetFailureCode = "not_found" | "ambiguous_name" | "repo_root_unset" | "read_failed" | "invalid_query" | "store_closed" | "db_locked" | "db_corrupt" | "db_error"; type SnippetResult = SnippetSuccess | { ok: false; code: SnippetFailureCode; }; /** Result of readMeta — `{ ok: true; value: null }` is a genuinely absent key; * a tagged failure is a backend error (rule 22). */ type ReadMetaResult = { ok: true; value: string | null; } | ({ ok: false; } & GraphStoreFailure); /** Result of readFileHashes — `{ ok: true; hashes: }` is an empty * index; a tagged failure is a backend error (rule 22). */ type ReadFileHashesResult = { ok: true; hashes: Map; } | ({ ok: false; } & GraphStoreFailure); /** A co-change edge row returned by readCoChanges. */ interface ReadCoChangeEdge { readonly fileA: string; readonly fileB: string; readonly support: number; readonly confidence: number; } /** Result of readCoChanges — `{ ok: true; edges: [] }` means no edges * recorded; a tagged failure is a backend error (rule 22). */ type ReadCoChangesResult = { ok: true; edges: readonly ReadCoChangeEdge[]; } | ({ ok: false; } & GraphStoreFailure); /** * The single source of truth for what `deadCode()` EXCLUDES from the * candidate set. Anything matched by these patterns or flags is treated * as a non-dead surface even when it has zero inbound call/usage edges. * * This constant exists so the exclusion criteria are NAMED, DOCUMENTED, * and auditable in one place — not scattered across ad-hoc `WHERE` * clauses (rule 53 analog). Adding a new exclusion category means * extending this constant plus the matching `node_attributes` column; * the query then picks both up automatically. * * Categories: * - {@link INBOUND_USAGE_EDGE_TYPES} — an inbound edge of any of these * types disqualifies a node from being dead. * - {@link TEST_PATH_PATTERNS} — a node whose `files.path` matches is * in a test file; tests can call into private code without the * production graph seeing the edge. * - {@link ENTRY_POINT_PATH_PATTERNS} — process entry points (index, * main, cli, bin/); these are reachable from outside the graph. * - {@link EXCLUDED_ATTRIBUTE_FLAGS} — per-node flags stored in * `node_attributes` (set at write time from FileIR.exports / * FileIR.routes); `is_exported` and `is_route_handler`. */ declare const DEAD_CODE_EXCLUSION: { /** * Edge types that — when pointing INTO a node — count as "this node * is used". Mirrors the issue's `CALLS/USAGE` wording plus the four * call-flavored edge types in the wider coding-graph vocabulary. */ readonly INBOUND_USAGE_EDGE_TYPES: readonly ["CALLS", "USES_TYPE", "ASYNC_CALLS", "HTTP_CALLS", "DATA_FLOWS"]; /** * File-path regexes identifying test files. Matched against * `files.path` (repo-relative, forward slashes). */ readonly TEST_PATH_PATTERNS: readonly [RegExp, RegExp, RegExp, RegExp, RegExp, RegExp]; /** * File-path regexes identifying entry points (reachable from * outside the indexed code). Matched against `files.path`. Kept * deliberately narrow — `server.ts` / `app.ts` are intentionally * NOT treated as entry points because they are common module * names that may also contain dead helpers. The conservative * direction is to report a symbol as dead rather than hide it. */ readonly ENTRY_POINT_PATH_PATTERNS: readonly [RegExp, RegExp, RegExp, RegExp, RegExp]; /** * Columns on `node_attributes` whose value being `1` excludes the * node. Names mirror the schema so a future column add is a one-line * constant extension + a query clause (no scattered edits). */ readonly EXCLUDED_ATTRIBUTE_FLAGS: readonly ["is_exported", "is_route_handler"]; }; interface GraphStoreOptions { /** Absolute path to the SQLite file. The caller resolves the namespace. */ dbPath: string; /** * Optional absolute path to the repo root. When set, `snippetFor()` * resolves a node's repo-relative `files.path` against this root to * read its source span from disk. When unset, `snippetFor()` returns * `code: "repo_root_unset"` for every call. The store NEVER persists * file contents (privacy + DB size — issue #1552 design); this is * the only path the read-side uses. */ repoRoot?: string; } /** * One DB per instance. The store does NOT mutate its path or close the * handle until {@link close} is called explicitly (rule 11). */ declare class GraphStore { private readonly db; private readonly queue; private readonly repoRoot; private closed; private closing; /** * True once close() has begun (closing) or completed (closed). Public so * callers that hold a GraphStore reference can return the documented * 'store_closed' degradation code instead of treating a closed store as * an empty graph (cursor Bugbot: 'Closed store reports success'). The * read primitives already short-circuit on this internally; this getter * lets the semantic entry points do the same BEFORE calling a read that * would return []. */ get isClosed(): boolean; private closePromise; private constructor(); /** * Open a store at the given dbPath. Creates parent directories and * applies the schema (idempotent — also handles upgrade). The dbPath * does no namespace resolution. */ static open(options: GraphStoreOptions): Promise; /** * The current schema_version row. Test seam — never expires, never * cached so migrations land without a restart. */ schemaVersion(): number; /** * Ingest a batch of IR files atomically. One transaction wraps every * file's delete + insert; if any file throws, the whole batch rolls * back (rule 34 — never partial-write a coding graph). * * Re-ingesting the same IR is a no-op once the rows are written * (idempotency — node ids are deterministic so the second pass collides * on PRIMARY KEY). * * Two-pass ordering: pass 1 upserts every file's nodes (so FTS stays * in sync and cross-file edge targets exist by the time pass 2 runs), * pass 2 resolves edges against the full batch's node map and deletes * prior edges owned by these files so changed confidence/provenance * values overwrite (chatgpt-codex-connector P1 + cursor medium + PR1 * design anchor in graph-schema). * * Tagging: * - `{ok:true, results}` — every file's counts. * - `{ok:false, code:"db_locked"}` — busy_timeout elapsed; caller may * retry. NOT a thrown error so the agent can degrade gracefully. * - `{ok:false, code:"db_corrupt"}` — SQLite reported * `database disk image is malformed`; the caller must surface and * stop trusting this DB. */ upsertFileBatch(files: StoreFileIR[], /** * Optional paths to delete in the SAME transaction as the upsert * (issue #1553 — the reindex executor prunes deleted files atomically * with the changed-files upsert so a mid-batch failure cannot leave * the graph with committed deletions but no re-ingested replacements). * Cascades to nodes + edges + node_attributes via the schema's * `ON DELETE CASCADE`. Empty/omitted = no deletions. */ deletePaths?: readonly string[]): Promise; /** * Upsert standalone edges whose endpoints are resolved from the FULL * database (not just a per-file batch). Used by the codegraph * ingest_traces surface (issue #1554) to persist runtime HTTP_CALLS * observations as edges with `provenance: "trace"` — upgrading * confidence on existing edges and inserting new ones. * * Endpoint resolution: when an edge carries `srcNodeId` / `dstNodeId` * (issue #1677 — the SIMILAR_TO pipeline populates them from * content-derived node ids), the endpoint is resolved by `nodes.id` * (unique primary key), so an edge between two symbols that share a * qualified name across files is persisted rather than dropped as * ambiguous. Edges WITHOUT node ids fall back to qualified_name * resolution via the global `resolveNodeId` (unambiguous single-match * policy). Edges whose endpoints do not resolve (missing node id row OR * an ambiguous/dangling qualified name) are skipped (and counted in * `skipped`) rather than attached to the wrong node — the dangling-edge * policy from `upsertFileBatch` applies. * * Serialized on the store's write queue like `upsertFileBatch` so a * concurrent file-batch upsert and a trace upsert cannot interleave * (rule 40). */ upsertEdges(edges: readonly EdgeIR[]): Promise; /** * Retire stale LSP-provenance edges for a file (issue #1895). * * The LSP resolution pass re-derives edges from the CURRENT source on each * run. After writing the new `lsp` edges for a file, this method deletes * prior `lsp`-provenance edges owned by that file's nodes whose * `(src, dst, type)` key is NOT in the asserted set. This is the LSP * layer's side of the provenance-lifecycle contract: each layer owns its * own stale-edge retirement (#1894 established that reindex's heuristic * scope never touches `lsp` rows). * * Heuristic, trace, and semantic edges are never touched. * * @returns the number of retired edges. */ reconcileLspEdges(filePath: string, assertedEdges: ReadonlyArray<{ srcQualifiedName: string; dstQualifiedName: string; type: string; }>): number; /** Wait for pending writes to drain — test seam. */ drain(): Promise; /** * Read a value from the `meta` table. Returns `null` when the key is * absent. Synchronous (like the other read primitives) so the reindex * planner can read `last_indexed_head` without an await. */ readMeta(key: string): ReadMetaResult; /** * Write a key/value pair to the `meta` table. Synchronous — runs in its * own implicit transaction. The reindex executor calls this AFTER * `upsertFileBatch` resolves (rule 25: head/state updates only after * the data transaction commits). A crash between the two leaves the old * head, and the next run re-ingests idempotently (deterministic node ids). */ writeMeta(key: string, value: string): void; /** * Read every file row's path → content_hash. Used by hash_scan mode * to detect content drift without a reachable base commit (issue #1553). */ readFileHashes(): ReadFileHashesResult; /** * Drop file rows by path, cascading to their nodes + edges + * node_attributes (the schema's `ON DELETE CASCADE` from `files(id)` * handles the cascade — `foreign_keys = ON` is set in `open()`). * Used by the reindex executor to prune deleted files. * * Paths are chunked under the SQLite variable limit (rule 23 pattern). */ dropFiles(paths: readonly string[]): Promise; /** * Chunk a parameterized DELETE-with-IN-list under SQLite's variable * bind limit. Mirrors the chunking pattern used by `runChunkedUpdate` * and the stale-edge deletes. */ private runChunkedDelete; /** * PR3 (issue #1553): upsert co-change edges into the `co_changes` * table. Clears existing edges then inserts the new set in one * transaction (idempotent — re-running on unchanged history produces * the same table). Serialized through the write queue. */ /** * PR3 (issue #1553): upsert co-change edges into the `co_changes` * table. Clears existing edges then inserts the new set in one * transaction (idempotent — re-running on unchanged history produces * the same table). Serialized through the write queue. * * Returns `{ ok: false, code: "store_closed" }` when the store is * closed/closing so the caller does NOT believe mining succeeded * while nothing was persisted (cursor Bugbot: 'Co-change store * reports false success'). */ upsertCoChanges(edges: readonly { readonly fileA: string; readonly fileB: string; readonly support: number; readonly confidence: number; }[]): Promise<{ ok: true; } | { ok: false; code: "store_closed"; } | { ok: false; code: "db_error"; }>; /** * PR3 (issue #1553): read co-change edges for a file. Returns edges * where the file is either `file_a` or `file_b`. Synchronous read. */ readCoChanges(filePath: string): ReadCoChangesResult; /** * Close the SQLite handle after draining the write queue. A batch * that has already been scheduled on the queue would otherwise run * against a closed DB and surface as `db_corrupt` — the caller * would stop trusting the store for unrelated reasons. Drain first, * then close (cursor Bugbot #09be5784). */ close(): Promise; /** Drain queued writes then close the SQLite handle exactly once. */ private finishClose; private runUpsert; /** * Standalone-edge upsert body (runs under the write queue). Resolves * both endpoints from the full DB via the unambiguous single-match * `resolveNodeId` fallback, then upserts each edge with the same * ON CONFLICT(src,dst,type) policy as the file-batch path. Edges whose * src or dst do not resolve to exactly one node are skipped (counted * in `skipped`) per the dangling-edge policy. */ private runUpsertEdges; /** * Pass 1a: upsert the file row and every symbol node, refreshing the * contentless `nodes_fts` index in lockstep, and compute the set of * stale node ids this file wants to prune (deterministic id, NOT * qualified_name, so a kind change gets a new id and the OLD row is * deleted). The prune itself — and the dangling-edge count that * gates it — is deferred to {@link pruneFileNodes} so the whole batch * shares one batch-wide view of what is being pruned before any * cascade runs. */ private upsertFileNodes; /** * Pass 1b: count the dangling edges this file's prune will drop and * perform the cascade delete + FTS cleanup. A dangling edge is one * whose dst is pruned by THIS file but whose src survives — and * "survives" is judged against the BATCH-WIDE pruned set, so an edge * whose both ends are pruned (possibly in different files) is * cascade-deleted and never reported as dangling. This makes the * reported loss independent of the order files are visited in * (chatgpt-codex-connector P2: 'Count dangling edges against the * whole batch'). */ private pruneFileNodes; /** * Pass 2: re-insert edges for one file. Runs AFTER every file's * nodes are in place (the full batch is committed to nodes) so * cross-file edges resolve regardless of input order. Stale edges * for nodes owned by this file are deleted first so a changed * `confidence` or `provenance` actually overwrites the prior row * (chatgpt-codex-connector P1: ON CONFLICT DO NOTHING silently * kept stale edges across re-ingests). */ private upsertFileEdges; /** * Pass 3 (PR2): upsert `node_attributes` rows for this file's * surviving nodes, derived from the IR's optional `exports` and * `routes` arrays. Per-field preservation semantics (mirrors the * edges pass, generalized to two independent flags): * - `exports == null` (omitted) → preserve existing `is_exported` * flags untouched (PR1-era IR has no exports field). The * `is_route_handler` flag is rebuilt independently from * `routes` — the two columns do NOT interact. * - `exports === []` (explicit empty) → wipe the file's * `is_exported` flags (the caller is asserting "this file * exports nothing"). * - same rule for `routes` / `is_route_handler`. * * A symbol is `is_exported=1` when its `name` matches an entry in * `ir.exports` (multiple symbols with the same name in one file all * get the flag — the dead-code query treats this conservatively, * never silently picking one). A symbol is `is_route_handler=1` * when its `qualifiedName` equals a route's `handlerQualifiedName`. * * Implementation: per-flag UPDATE, not a delete-then-insert (the * original PR2 implementation wiped both flags whenever either field * was present, so a re-ingest with only `exports` silently dropped * `is_route_handler` — cursor Bugbot + chatgpt-codex-connector P2). * The two flags live in the same row keyed by node_id; INSERT OR * IGNORE ensures a row exists, then UPDATE-per-flag changes only * the column the IR is asserting. */ private upsertFileAttributes; /** * Chunk a parameterized UPDATE-with-IN-list under SQLite's variable * bind limit. The SQL template uses `%PH%` as a placeholder for the * `?,?,…` list. Mirrors the chunking pattern PR1 uses for deletes. */ private runChunkedUpdate; /** * Iterative frontier BFS over the edges table. Cycle-safe via a JS * visited set keyed by node id; depth-capped by {@link TraverseQuery.maxDepth} * (half-open — depth==maxDepth is INCLUDED, maxDepth+1 is NOT — rule 35). * The start node is always included at depth 0 when it exists. * * Reads the edges table via a single prepared statement per * direction; the frontier expands level-by-level so memory is * bounded by the visited set's size, not the recursion depth. */ traverse(query: TraverseQuery): TraverseResult; /** * Path-enumerating traversal (issue #1650). Unlike {@link traverse}'s BFS — * which visits each node ONCE at its shortest-path depth and so cannot honor * an exact `*N` (N > 1) hop count for nodes reachable at both a shorter and a * length-N path — this primitive enumerates concrete relationship-simple * paths from the start, yielding one hit per distinct (path, endpoint) pair * up to {@link TraversePathsQuery.maxHops}. * * Cycle safety uses RELATIONSHIP UNIQUENESS (the real Cypher rule): a single * path never traverses the same edge twice, keyed by the edge's canonical * `(src, dst, type)` identity. A node MAY recur in a path via distinct edges * (e.g. A->B->A over two different edges) — that is correct Cypher behavior. * The {@link TraversePathsQuery.maxHops} cap bounds each path's length; * {@link TraversePathsQuery.maxPaths} bounds the total enumerated count so a * dense subgraph cannot blow enumeration up exponentially without notice * (when hit, enumeration stops and the result carries `truncated: true`). * * Every yielded path has length >= 1 (at least one edge). A length-0 "path" * (the trivial start->start) is NOT enumerated; callers that need the start * node for a `*0..N` bound add it themselves. */ traversePaths(query: TraversePathsQuery): TraversePathsResult; /** * Structured node search. All filters are AND-combined; patterns use * SQLite LIKE (case-insensitive via COLLATE NOCASE). Patterns and * limits are parameter-bound, never string-interpolated, so user * input cannot inject SQL. */ searchGraph(query: SearchQuery): SearchResult; /** * Aggregate counts over the whole graph. Single round-trip: one * scalar per metric, two GROUP BY queries for the by-label / * by-type histograms. */ schemaStats(): SchemaStatsResult; /** * Dead-code candidates: nodes with zero inbound * {@link DEAD_CODE_EXCLUSION.INBOUND_USAGE_EDGE_TYPES} edges, excluding * nodes whose `node_attributes` row marks them exported / route-handler * AND nodes whose file path matches the test / entry-point patterns * in {@link DEAD_CODE_EXCLUSION}. * * The exclusion criteria live in the named constant — not in * ad-hoc WHERE clauses (rule 53 analog). The stored flags come from * the write pipeline's `upsertFileAttributes` pass, which the IR's * `exports` and `routes` arrays feed. */ deadCode(): DeadCodeResult; /** * Find the innermost node whose span contains a byte offset in a file * (issue #1917). Used by the LSP resolution pass's NodeLocator to map * definition locations back to indexed nodes. Returns the node's * qualified name, or null when no node contains the offset. */ findNodeBySpan(filePath: string, byteOffset: number): string | null; /** * Callee NAMES already linked from a caller symbol via CALLS edges of a * given provenance (issue #1917 wiring). The LSP pass uses this to skip * call sites Phase A (provenance "heuristic") already resolved WITHOUT * also skipping sites whose only edge is a prior "lsp" edge — those must * be re-asserted each run or reconciliation would retire them. */ resolvedCalleeNames(srcQualifiedName: string, provenance: string, filePath?: string): string[]; /** * Current CALLS edges of a given provenance owned by a caller symbol in * a file (#1923 review threads). The LSP pass uses this two ways: * provenance "lsp" lists a filtered caller's existing lsp edges, and * provenance "heuristic" lists its current Phase-A resolutions — an lsp * edge is preserved at reconcile time only when it duplicates a current * heuristic resolution (same dst), so removed member calls' edges retire * while a filtered bare call's covering edge survives. */ callEdgesForCaller(srcQualifiedName: string, filePath: string, provenance: string): Array<{ srcQualifiedName: string; dstQualifiedName: string; dstName: string; type: string; }>; /** * Read a symbol's source span from disk. The store NEVER persists * file contents (privacy + DB size — issue #1552 design); this * method resolves `files.path` against {@link GraphStoreOptions.repoRoot} * and slices the half-open `[startByte, endByte)` span from the * on-disk bytes. */ snippetFor(query: SnippetQuery): Promise; /** * Upsert one symbol vector. Idempotent on (node_id, model_id). The * caller (the semantic indexer) has ALREADY decided to re-embed (the * content_hash differs from the cached row); this method just persists. */ writeSymbolVector(input: { readonly nodeId: string; readonly modelId: string; readonly contentHash: string; readonly dims: number; readonly vector: Float32Array; }): Promise; /** * Read one vector row by (node_id, model_id). Returns null when absent. * Used by the indexer's cache-check path (skip re-embed when content_hash * matches) and by the cache-hit test. */ readSymbolVector(nodeId: string, modelId: string): { readonly contentHash: string; readonly dims: number; readonly vector: Float32Array; } | null; /** * Read every vector row for a given model. Used by brute-force cosine * retrieval (SIMILAR_TO confirmation + semantic_query). Returns node * metadata alongside the vector so callers can hydrate hits without a * second round-trip. */ readAllSymbolVectors(modelId: string): readonly { readonly nodeId: string; readonly qualifiedName: string; readonly filePath: string; readonly kind: string; readonly dims: number; readonly vector: Float32Array; readonly contentHash: string; }[]; /** * Delete vector rows for a set of node ids (all models). Used by the * cache-invalidation path when a symbol's canonical text changed AND * it could not be re-embedded (provider gone) — the stale vector must * not survive to pollute cosine retrieval. Cascades via the schema's * ON DELETE CASCADE on nodes(id) when a node is pruned, so this method * is only for the targeted-invalidation path. */ deleteSymbolVectors(nodeIds: readonly string[]): Promise; /** * Remove every SIMILAR_TO edge written by the semantic similarity * pipeline (type 'SIMILAR_TO', provenance 'semantic'). The pipeline * recomputes the FULL near-clone edge set on each run, so callers MUST * clear the prior set before upserting the new one — otherwise an edge * between two symbols that stopped being similar survives indefinitely * and graph traversal keeps reporting a stale clone relationship * (chatgpt-codex-connector P2: 'Replace old SIMILAR_TO edges on * recompute'). Scoped to provenance 'semantic' so non-semantic edges * are untouched. Serialized via the write queue so it cannot interleave * a concurrent file-batch edge upsert. */ clearSemanticSimilarToEdges(): Promise; /** * Read every node with its file path + span, for the semantic indexer. * The indexer reads source text from disk (via repoRoot) and builds * canonical text per node. Returns kind + qualified_name + span so the * indexer can reconstruct the SymbolIR-equivalent without a second * join. Ordered by qualified_name for deterministic processing order. */ readNodesForSemantic(): readonly { readonly nodeId: string; readonly qualifiedName: string; readonly kind: string; readonly filePath: string; readonly startByte: number; readonly endByte: number; readonly lang: string; }[]; /** * Read the callers and callees of a node by qualified name, for * semantic_query hydration (the issue: hydrate each hit with graph * context — defining file, direct callers/callees). */ readNeighbors(qualifiedName: string): { readonly callers: readonly string[]; readonly callees: readonly string[]; }; /** * Read callers/callees by node id directly (avoids the qualified-name * ambiguity when duplicate names exist across files). Used by * semantic_query hydration (chatgpt-codex-connector: 'Use the hit node * id when hydrating neighbors'). */ readNeighborsByNodeId(nodeId: string): { readonly callers: readonly string[]; readonly callees: readonly string[]; }; } interface NodeIdInput { qualifiedName: string; filePath: string; label: string; } /** * sha256 over the sorted key material. The exact form MUST match between * ingest and lookup; tests assert this. Sort is stable (string compare), * no separators needed — the three fields are concatenated with a length * prefix so collision space is unambiguous. */ declare function nodeIdFor(input: NodeIdInput): string; export { type ByteSpan, DEAD_CODE_EXCLUSION, DEFAULT_TRAVERSE_PATHS_MAX, type DeadCodeHit, type DeadCodeResult, type EdgeIR, GraphStore, type GraphStoreFailure, type GraphStoreFailureCode, type GraphStoreOptions, MAX_TRAVERSE_PATHS_HOPS, type NodeIdInput, type ReadCoChangeEdge, type ReadCoChangesResult, type ReadFileHashesResult, type ReadMetaResult, type SchemaStats, type SchemaStatsResult, type SearchHit, type SearchQuery, type SearchResult, type SnippetFailureCode, type SnippetQuery, type SnippetResult, type SnippetSuccess, type StoreFileIR, type SymbolKind, type TraverseDirection, type TraverseHit, type TraversePathHit, type TraversePathsQuery, type TraversePathsResult, type TraverseQuery, type TraverseResult, type UpsertBatchResult, type UpsertEdgesResult, type UpsertEdgesSuccess, type UpsertResult, type UpsertSuccess, nodeIdFor };