import lbug from '@ladybugdb/core'; import { KnowledgeGraph } from '../graph/types.js'; import { NodeTableName } from './schema.js'; import type { PdgEmitManifest } from './pdg-emit-sink.js'; import type { CachedEmbedding } from '../embeddings/types.js'; import { type ExtensionEnsureOptions } from './extension-loader.js'; /** Result of splitting the relationship CSV into per-label-pair files. */ export interface RelCsvSplitResult { relHeader: string; relsByPairMeta: Map; pairWriteStreams: Map; skippedRels: number; totalValidRels: number; } /** Expose the current Database for pool adapter reuse in tests. */ export declare const getDatabase: () => lbug.Database | null; /** * Return true when the error message indicates a write was attempted against * a read-only LadybugDB connection. The MCP query pool opens DBs read-only, * so any path that calls a `CREATE_*` procedure there will surface this * (e.g. defensive `ensureFTSIndex` calls). Owners of the writable analyze * path should ignore this error — index creation is owned by `gitnexus * analyze` and either already happened or will happen on the next run. */ export declare const isReadOnlyDbError: (err: unknown) => boolean; /** * Acquire a cross-process init lock for `dbPath`. * Uses `O_CREAT | O_EXCL` for atomic create-or-fail semantics. * * Returns a release function that removes the lock file. The release * function is idempotent and safe to call even if the lock was already * cleaned up externally. * * Throws if the lock cannot be acquired after `INIT_LOCK_MAX_ATTEMPTS`. */ export declare const acquireInitLock: (dbPath: string) => Promise<() => Promise>; /** Exported for testing — returns the lock file path for a given dbPath. */ export declare const _initLockPathForTest: (dbPath: string) => string; export declare const initLbug: (dbPath: string) => Promise<{ db: lbug.Database; conn: lbug.Connection; }>; /** * Execute multiple queries against one repo DB atomically. * While the callback runs, no other request can switch the active DB. * * Automatically retries up to DB_LOCK_RETRY_ATTEMPTS times when the * database is busy (e.g. `gitnexus analyze` holds the write lock). * Each retry waits DB_LOCK_RETRY_DELAY_MS * attempt milliseconds. */ export declare const withLbugDb: (dbPath: string, operation: () => Promise, options?: { readOnly?: boolean; }) => Promise; export type LbugProgressCallback = (message: string) => void; /** * Persist a KnowledgeGraph: stream CSVs, then bulk-COPY nodes (overlapped with * relationship emit — see the body) and relationships. * * NOT TRANSACTIONAL (#2226). Each `COPY` commits independently and there is no * surrounding transaction, so a failure partway through — a node `COPY` that * throws at the FK barrier, a relationship `COPY` failure, or a `pdgEmitManifest` * collision raised after node rows have already committed in the overlap path — * leaves a partially-loaded DB. The caller surfaces the error; recovery is a * `--force` re-analyze (a full rebuild), not a partial retry. Callers must not * assume the DB is either fully loaded or untouched after a rejection. */ export declare const loadGraphToLbug: (graph: KnowledgeGraph, repoPath: string, storagePath: string, onProgress?: LbugProgressCallback, /** * Streamed PDG-emit manifest (#2202). When present (streaming was on, full * rebuild), the BasicBlock node CSV + per-pair PDG-edge CSVs it points at * were already flushed to disk during the emit loop; they are merged into the * COPY plan below so they load alongside the structural CSVs. When streaming * was on the in-memory `graph` holds zero BasicBlocks, so `streamAllCSVsToDisk` * emits none — the manifest is the sole source and there is no double-COPY. */ pdgEmitManifest?: PdgEmitManifest) => Promise<{ success: boolean; insertedRels: number; skippedRels: number; warnings: string[]; }>; export declare const COPY_CSV_OPTS = "(HEADER=true, ESCAPE='\"', DELIM=',', QUOTE='\"', PARALLEL=false, auto_detect=false)"; export declare const getCopyQuery: (table: NodeTableName, filePath: string) => string; /** * Insert a single node to LadybugDB * @param label - Node type (File, Function, Class, etc.) * @param properties - Node properties * @param dbPath - Path to LadybugDB database (optional if already initialized) */ export declare const insertNodeToLbug: (label: string, properties: Record, dbPath?: string) => Promise; /** * Batch insert multiple nodes to LadybugDB using a single connection * @param nodes - Array of {label, properties} to insert * @param dbPath - Path to LadybugDB database * @returns Object with success count and error count */ export declare const batchInsertNodesToLbug: (nodes: Array<{ label: string; properties: Record; }>, dbPath: string) => Promise<{ inserted: number; failed: number; }>; export declare const executeQuery: (cypher: string) => Promise; export declare const streamQuery: (cypher: string, onRow: (row: any) => void | Promise) => Promise; /** * Execute a single parameterized query (prepare/execute pattern). * Prevents Cypher injection by binding values as parameters. */ export declare const executePrepared: (cypher: string, params: Record) => Promise; export declare const executeWithReusedStatement: (cypher: string, paramsList: Array>) => Promise; export declare const getLbugStats: () => Promise<{ nodes: number; edges: number; }>; /** * Load cached embeddings from LadybugDB before a rebuild. * Returns all embedding vectors so they can be re-inserted after the graph is reloaded, * avoiding expensive re-embedding of unchanged nodes. * * Detects old schema (no chunkIndex column) and returns empty cache to trigger rebuild. */ export declare const loadCachedEmbeddings: () => Promise<{ embeddingNodeIds: Set; embeddings: CachedEmbedding[]; }>; /** * Fetch existing embedding hashes from CodeEmbedding table for incremental embedding. * Returns a Map suitable for passing to `runEmbeddingPipeline`. * Handles legacy DBs without the `contentHash` column (all rows treated as stale with empty hash). * Returns undefined if the CodeEmbedding table does not exist. * * @param execQuery - Cypher query executor (typically pool-adapter's `executeQuery`) */ export declare const fetchExistingEmbeddingHashes: (execQuery: (cypher: string) => Promise) => Promise | undefined>; /** * Flush the WAL so all pending writes are visible to subsequent readers. * * Best-effort: swallows errors from older LadybugDB versions or schemaless * databases that do not support the CHECKPOINT command. A no-op when there * is nothing pending, so safe (and cheap) to call unconditionally after any * write path. * * Use this instead of safeClose when the connection must stay open * (e.g. the /api/embed handler that keeps serving queries after flushing). * * @see safeClose — CHECKPOINT + connection/database close */ export declare const flushWAL: () => Promise; /** * Issue a manual `CHECKPOINT` against the current connection and surface * any engine error to the caller. Unlike {@link flushWAL}, this variant * does NOT swallow Ladybug rename/remove IO failures — the manual * checkpoint driver (`wal-checkpoint-driver.ts`) relies on the rejection * to drive its bounded retry loop. Returns `false` when no connection is * open (the caller treats this as a no-op success — there is no WAL to * flush). Returns `true` after a successful CHECKPOINT + drain. * * The split from `flushWAL` is deliberate: every other CHECKPOINT site * (server flush, safeClose) is best-effort and prefers a silent skip; * the manual driver, by contrast, must observe failures to decide * whether to retry. */ export declare const tryFlushWAL: () => Promise; /** * Flush the WAL and close the connection and database handles. * * Consolidates the CHECKPOINT + close pattern into a single function so * callers never call conn.close() or db.close() directly (#1376). * An ESLint no-restricted-syntax rule enforces this — see eslint.config.mjs. * * @see flushWAL — CHECKPOINT-only (connection stays open) * @see closeLbug — safeClose + module state reset (full teardown) */ export declare const safeClose: () => Promise; /** * CHECKPOINT for durability, then DELIBERATELY skip the native connection/database * teardown. The name encodes the contract — there is no boolean flag to misuse: * call this ONLY from a path that guarantees a `process.exit` immediately after * (the CLI analyze success/SIGINT paths and the forked worker). * * LadybugDB's ClientContext/Connection destructor can double-free after large * --pdg writes (gdb: `double free or corruption` in ClientContext::~ClientContext * via NodeConnection::Close), aborting the process AFTER a fully-written, * checkpointed index. flushWAL already persisted the data; process exit reclaims * the native handles. We leave the handles referenced and module state intact so a * GC finalizer cannot run the same destructor before exit, and any post-analyze * read reuses the live connection. Mirrors the pool adapter's fire-and-forget * native teardown (pool-adapter.ts) and the ONNX native-cleanup philosophy. * Workaround for a LadybugDB engine bug (to be reported upstream). * * SAFETY: only valid when a process.exit is guaranteed to follow. Long-lived * callers (MCP server, tests) leave `skipNativeCloseOnExit` unset, so * runFullAnalysis closes for real via {@link closeLbug} — never this. */ export declare const closeLbugBeforeExit: () => Promise; export declare const closeLbug: () => Promise; export declare const isLbugReady: () => boolean; /** * Delete all nodes (and their relationships) for a specific file from LadybugDB * @param filePath - The file path to delete nodes for * @param dbPath - Optional path to LadybugDB for per-query connection * @returns Object with counts of deleted nodes */ export declare const deleteNodesForFile: (filePath: string, dbPath?: string) => Promise<{ deletedNodes: number; }>; export declare const getEmbeddingTableName: () => string; /** * Return the distinct repo-relative paths of files that import * `targetFilePath` according to the IMPORTS edges currently in the * DB. Used by the incremental writeback path to expand the * "files-to-rewrite" set so that files importing a changed file get * their edges (which may have been refined by cross-file resolution) * re-emitted, rather than left stale in the DB. * * The DB query reads the *previous* run's state — pre-pipeline, before * any nodes are deleted — so the returned importers are "files that * USED TO import the target". That's the right set to invalidate: * those are the files whose edges in the DB might no longer match * what cross-file resolution produces given the changed file's new * exports. */ export declare const queryImporters: (targetFilePath: string) => Promise; /** * Drop every Community and Process node (and their MEMBER_OF / * STEP_IN_PROCESS edges via DETACH DELETE). Used at the start of an * incremental run so the communities and processes phases regenerate * them from scratch on the merged graph — required for the * "Leiden runs on the FULL graph" correctness invariant. */ export declare const deleteAllCommunitiesAndProcesses: () => Promise<{ nodesDeleted: number; }>; /** * Drop every interprocedural `TAINT_PATH` relationship (#2084 M4 U6). Used at * the start of an incremental `--pdg` writeback so the `taintSummaries` phase * re-materialises them from scratch on the FULL recomputed graph. * * TAINT_PATH validity is a WHOLE-PROGRAM property (a flow A→C can be * invalidated by a change to an INTERMEDIATE function whose file is neither A * nor C). The endpoint-writability extract rule (`extractChangedSubgraph`) * cannot see that — an A→C edge between two unchanged files would be skipped * and a stale finding would survive. So, exactly like Community/Process, the * sound move is delete-all-then-rebuild: cheap because TAINT_PATH is sparse * (per-run capped), and the compute side already rebuilds every summary each * run. Relationship-level (TAINT_PATH is an edge type, not a node label), so a * plain DELETE on the typed CodeRelation rows — endpoints are untouched. */ export declare const deleteAllInterprocTaintPaths: () => Promise<{ edgesDeleted: number; }>; /** * Drop every `CALL_SUMMARY` relationship (PDG FU-C, U-C3). Used at the start of * an incremental `--pdg` writeback so the `callSummaries` phase re-materialises * them from scratch on the FULL recomputed graph. * * Mirrors {@link deleteAllInterprocTaintPaths}: CALL_SUMMARY is a self-loop edge * type (not a node label), so a plain DELETE on the typed CodeRelation rows * leaves endpoints untouched. `extractChangedSubgraph` re-includes ALL of them * from the fresh graph (`isGraphWideRelType`), so delete-all-then-rebuild keeps * an unchanged function's summary from being lost. */ export declare const deleteAllCallSummaries: () => Promise<{ edgesDeleted: number; }>; /** * Drop every `INJECTS` relationship (DI collection injection, #2200). Used at * the start of an incremental writeback — UNCONDITIONALLY, unlike the * pdg-gated twins above, because the `di` phase runs on every persisting * analyze — so the phase re-materialises them from scratch on the FULL * recomputed graph. * * Mirrors {@link deleteAllInterprocTaintPaths}: INJECTS validity is a * whole-program property (a change to the interface, or a new/removed * implementer, on a THIRD file creates/invalidates edges between two * untouched files), so endpoint-writability extraction can't refresh them. * `extractChangedSubgraph` re-includes ALL of them from the fresh graph * (`isGraphWideRelType`), so delete-all-then-rebuild is the sound move. * Relationship-level (INJECTS is an edge type, not a node label), so a plain * DELETE on the typed CodeRelation rows — endpoints are untouched. */ export declare const deleteAllInjects: () => Promise<{ edgesDeleted: number; }>; /** * Load the FTS extension on the supplied connection (or the singleton * writable connection when none is given). * * Delegates to the shared `ExtensionManager` so install policy (auto / * load-only / never), out-of-process bounded INSTALL, and capability * caching are owned in one place. The module-level `ftsLoaded` flag is * kept purely as a per-call short-circuit on the singleton writable * connection so repeated callers (e.g. createFTSIndex) avoid an extra * `LOAD` round-trip per invocation. Pool adapter callers pass * `{ policy: 'load-only' }` so query paths never block on a network install. */ export declare const loadFTSExtension: (targetConn?: lbug.Connection, opts?: ExtensionEnsureOptions) => Promise; /** * Load the VECTOR extension on the supplied connection (or the singleton * writable connection when none is given). Returns false when VECTOR is * unavailable so semantic search can fall back to exact scan. */ export declare const loadVectorExtension: (targetConn?: lbug.Connection, opts?: ExtensionEnsureOptions) => Promise; /** * Default stemmer for FTS indexes. Single source so the analyze path * (`getSearchFTSStemmer`) and the read-only `createFTSIndex`/`ensureFTSIndex` * defaults can never silently diverge. */ export declare const DEFAULT_FTS_STEMMER = "porter"; /** * Create a full-text search index on a table * @param tableName - The node table name (e.g., 'File', 'CodeSymbol') * @param indexName - Name for the FTS index * @param properties - List of properties to index (e.g., ['name', 'code']) * @param stemmer - Stemming algorithm (default: 'porter') */ export declare const createFTSIndex: (tableName: string, indexName: string, properties: string[], stemmer?: string) => Promise; /** * Create the HNSW vector index on the CodeEmbedding table. * * MUST run via `conn.query()` (here through `queryAndDrain`), NOT through the * prepared `executeQuery`/`conn.prepare()` path: `CALL CREATE_VECTOR_INDEX(...)` * compiles to multiple statements, which LadybugDB cannot prepare — it fails * with "Connection Exception: We do not support prepare multiple statements." * Routing index creation through `executeQuery` (prepared) is exactly what * broke vector-index creation during `analyze` (#2114; the singleton * `executeQuery` was switched to the prepared path in #1655 while FTS index * creation kept using `conn.query()`, which is why FTS survived and VECTOR did * not). Mirrors `createFTSIndex` above. * * Returns `true` on success (or when the index already exists — idempotent so * incremental re-runs don't spuriously downgrade to exact scan), `false` when * the VECTOR extension is unavailable or the connection is read-only. Any other * failure propagates so the caller can log it. */ export declare const createVectorIndex: () => Promise; /** * Lazy-create an FTS index, caching the fact in-process. * * Kept for writable maintenance paths that need to lazily materialize an * index. Read-only query paths must not call this; production analysis owns * creating the configured search indexes before the database is served. * * Safe to call repeatedly — the in-process Set guarantees only the first * call hits LadybugDB. `closeLbug` clears the cache so re-init starts fresh. * * Defense in depth: if the active connection is read-only (e.g. the MCP * pool adapter), `CREATE_FTS_INDEX` will fail with "Cannot execute write * operations in a read-only database". Treat that as a no-op and cache * the key so callers don't loop on a path that can never succeed here — * the index is owned by `gitnexus analyze` (writable) and either already * exists or will be created on the next analyze. */ export declare const ensureFTSIndex: (tableName: string, indexName: string, properties: string[], stemmer?: string) => Promise; /** * Query a full-text search index * @param tableName - The node table name * @param indexName - FTS index name * @param query - Search query string * @param limit - Maximum results * @param conjunctive - If true, all terms must match (AND); if false, any term matches (OR) * @returns Array of { node properties, score } */ export declare const queryFTS: (tableName: string, indexName: string, query: string, limit?: number, conjunctive?: boolean) => Promise>; /** * Drop an FTS index */ export declare const dropFTSIndex: (tableName: string, indexName: string) => Promise;