/** * CodeGraph * * A local-first code intelligence system that builds a semantic * knowledge graph from any codebase. */ import * as path from 'path'; import { Node, Edge, FileRecord, ExtractionResult, Subgraph, TraversalOptions, SearchOptions, SearchResult, Context, GraphStats, TaskInput, TaskContext, BuildContextOptions, FindRelevantContextOptions, } from './types'; import { DatabaseConnection, getDatabasePath } from './db'; import { QueryBuilder } from './db/queries'; import { isInitialized, createDirectory, removeDirectory, validateDirectory, } from './directory'; import { ExtractionOrchestrator, IndexProgress, IndexResult, SyncResult, extractFromSource, initGrammars, } from './extraction'; import { ReferenceResolver, createResolver, ResolutionResult, } from './resolution'; import { GraphTraverser, GraphQueryManager } from './graph'; import { ContextBuilder, createContextBuilder } from './context'; import { Mutex, FileLock } from './utils'; import { FileWatcher, WatchOptions, PendingFile, LockUnavailableError } from './sync'; import { EXTRACTION_VERSION } from './extraction/extraction-version'; import { getCodeGraphDir } from './directory'; import { deriveProjectNameTokens } from './search/query-utils'; import { CodeGraphPackageVersion } from './mcp/version'; // Re-export types for consumers export * from './types'; // Storage building blocks for embedded/SDK consumers that drive the graph // directly (open a DB, run prepared queries) rather than through the CodeGraph // facade. Exposed from the package entry so they no longer require deep imports // into dist/ (issue #354). export { getDatabasePath, DatabaseConnection } from './db'; export { QueryBuilder } from './db/queries'; export { getCodeGraphDir, isInitialized, findNearestCodeGraphRoot, CODEGRAPH_DIR, } from './directory'; export { IndexProgress, IndexResult, SyncResult } from './extraction'; export { detectLanguage, isLanguageSupported, isGrammarLoaded, getSupportedLanguages, initGrammars, loadGrammarsForLanguages, loadAllGrammars } from './extraction'; export { ResolutionResult } from './resolution'; export { CodeGraphError, FileError, ParseError, DatabaseError, SearchError, VectorError, ConfigError, Logger, setLogger, getLogger, silentLogger, defaultLogger, } from './errors'; export { Mutex, FileLock, processInBatches, debounce, throttle, MemoryMonitor } from './utils'; export { FileWatcher, WatchOptions, PendingFile, LockUnavailableError } from './sync'; export { MCPServer } from './mcp'; /** * Options for initializing a new CodeGraph project */ export interface InitOptions { /** Whether to run initial indexing after init */ index?: boolean; /** Progress callback for indexing */ onProgress?: (progress: IndexProgress) => void; } /** * Options for opening an existing CodeGraph project */ export interface OpenOptions { /** Whether to run sync if files have changed */ sync?: boolean; /** Whether to run in read-only mode */ readOnly?: boolean; } /** * Options for indexing */ export interface IndexOptions { /** Progress callback */ onProgress?: (progress: IndexProgress) => void; /** Abort signal for cancellation */ signal?: AbortSignal; /** Enable verbose logging (worker lifecycle, memory, timeouts) */ verbose?: boolean; } /** * Main CodeGraph class * * Provides the primary interface for interacting with the code knowledge graph. */ export class CodeGraph { private db: DatabaseConnection; private queries: QueryBuilder; private projectRoot: string; private orchestrator: ExtractionOrchestrator; private resolver: ReferenceResolver; private graphManager: GraphQueryManager; private traverser: GraphTraverser; private contextBuilder: ContextBuilder; // Mutex for preventing concurrent indexing operations (in-process) private indexMutex = new Mutex(); // File lock for preventing concurrent writes across processes (CLI, MCP, git hooks) private fileLock: FileLock; // File watcher for auto-sync on file changes private watcher: FileWatcher | null = null; private constructor( db: DatabaseConnection, queries: QueryBuilder, projectRoot: string ) { this.db = db; this.queries = queries; this.projectRoot = projectRoot; // Down-weight the project name as a query term in search ranking — it names // the whole repo, not a symbol, so it has no discriminative value (#720). try { this.queries.setProjectNameTokens(deriveProjectNameTokens(projectRoot)); } catch { // Best-effort: ranking still works without it. } this.fileLock = new FileLock( path.join(getCodeGraphDir(projectRoot), 'codegraph.lock') ); this.orchestrator = new ExtractionOrchestrator(projectRoot, queries); this.resolver = createResolver(projectRoot, queries); this.graphManager = new GraphQueryManager(queries); this.traverser = new GraphTraverser(queries); this.contextBuilder = createContextBuilder( projectRoot, queries, this.traverser ); } // =========================================================================== // Lifecycle Methods // =========================================================================== /** * Initialize a new CodeGraph project * * Creates the .CodeGraph directory, database, and configuration. * * @param projectRoot - Path to the project root directory * @param options - Initialization options * @returns A new CodeGraph instance */ static async init(projectRoot: string, options: InitOptions = {}): Promise { await initGrammars(); const resolvedRoot = path.resolve(projectRoot); // Check if already initialized if (isInitialized(resolvedRoot)) { throw new Error(`CodeGraph already initialized in ${resolvedRoot}`); } // Create directory structure createDirectory(resolvedRoot); // Initialize database const dbPath = getDatabasePath(resolvedRoot); const db = DatabaseConnection.initialize(dbPath); const queries = new QueryBuilder(db.getDb()); const instance = new CodeGraph(db, queries, resolvedRoot); // Run initial indexing if requested if (options.index) { await instance.indexAll({ onProgress: options.onProgress }); } return instance; } /** * Initialize synchronously (without indexing) */ static initSync(projectRoot: string): CodeGraph { const resolvedRoot = path.resolve(projectRoot); // Check if already initialized if (isInitialized(resolvedRoot)) { throw new Error(`CodeGraph already initialized in ${resolvedRoot}`); } // Create directory structure createDirectory(resolvedRoot); // Initialize database const dbPath = getDatabasePath(resolvedRoot); const db = DatabaseConnection.initialize(dbPath); const queries = new QueryBuilder(db.getDb()); return new CodeGraph(db, queries, resolvedRoot); } /** * Open an existing CodeGraph project * * @param projectRoot - Path to the project root directory * @param options - Open options * @returns A CodeGraph instance */ static async open(projectRoot: string, options: OpenOptions = {}): Promise { await initGrammars(); const resolvedRoot = path.resolve(projectRoot); // Check if initialized if (!isInitialized(resolvedRoot)) { throw new Error(`CodeGraph not initialized in ${resolvedRoot}. Run init() first.`); } // Validate directory structure const validation = validateDirectory(resolvedRoot); if (!validation.valid) { throw new Error(`Invalid CodeGraph directory: ${validation.errors.join(', ')}`); } // Open database const dbPath = getDatabasePath(resolvedRoot); const db = DatabaseConnection.open(dbPath); const queries = new QueryBuilder(db.getDb()); const instance = new CodeGraph(db, queries, resolvedRoot); // Sync if requested if (options.sync) { await instance.sync(); } return instance; } /** * Open synchronously (without sync) */ static openSync(projectRoot: string): CodeGraph { const resolvedRoot = path.resolve(projectRoot); // Check if initialized if (!isInitialized(resolvedRoot)) { throw new Error(`CodeGraph not initialized in ${resolvedRoot}. Run init() first.`); } // Validate directory structure const validation = validateDirectory(resolvedRoot); if (!validation.valid) { throw new Error(`Invalid CodeGraph directory: ${validation.errors.join(', ')}`); } // Open database const dbPath = getDatabasePath(resolvedRoot); const db = DatabaseConnection.open(dbPath); const queries = new QueryBuilder(db.getDb()); return new CodeGraph(db, queries, resolvedRoot); } /** * Check if a directory has been initialized as a CodeGraph project */ static isInitialized(projectRoot: string): boolean { return isInitialized(path.resolve(projectRoot)); } /** * Close the CodeGraph instance and release resources */ close(): void { this.unwatch(); // Release file lock if held this.fileLock.release(); this.db.close(); } /** * Get the project root directory */ getProjectRoot(): string { return this.projectRoot; } // =========================================================================== // Indexing // =========================================================================== /** * Index all files in the project * * Uses a mutex to prevent concurrent indexing operations. */ async indexAll(options: IndexOptions = {}): Promise { return this.indexMutex.withLock(async () => { try { this.fileLock.acquire(); } catch { return { success: false, filesIndexed: 0, filesSkipped: 0, filesErrored: 0, nodesCreated: 0, edgesCreated: 0, errors: [{ message: 'Could not acquire file lock - another process may be indexing', severity: 'error' as const }], durationMs: 0 }; } try { const before = this.queries.getNodeAndEdgeCount(); const result = await this.orchestrator.indexAll(options.onProgress, options.signal, options.verbose); // Re-detect frameworks now that the index is populated. The resolver // is constructed with createResolver() before any files exist, so // framework resolvers whose detect() consults the indexed file list // (e.g. UIKit/SwiftUI scanning for imports, swift-objc-bridge looking // for both Swift and ObjC files) all return false on that initial pass // and silently drop themselves. Re-initializing here gives them a // chance to see the actual project before resolution runs. if (result.success && result.filesIndexed > 0) { this.resolver.initialize(); // Cross-file finalization (e.g. NestJS RouterModule prefixes). Runs // before resolution so updated names show up in subsequent reads. this.resolver.runPostExtract(); } // Resolve references to create call/import/extends edges if (result.success && result.filesIndexed > 0) { // Get count without loading all refs into memory const unresolvedCount = this.queries.getUnresolvedReferencesCount(); options.onProgress?.({ phase: 'resolving', current: 0, total: unresolvedCount, }); await this.resolveReferencesBatched((current, total) => { options.onProgress?.({ phase: 'resolving', current, total, }); }); // Second pass: chained calls whose method lives on a supertype the // receiver conforms to (protocol-extension / inherited / default- // interface). Needs the implements/extends edges the main pass just // built, so it runs after resolution (#750). this.resolver.resolveChainedCallsViaConformance(); // Same lifecycle for `this.` callback registrations whose // member is inherited from a supertype (#808). this.resolver.resolveDeferredThisMemberRefs(); } // Refresh planner stats + checkpoint the WAL after bulk writes. // Cheap and non-blocking; never load-bearing for correctness. if (result.success && result.filesIndexed > 0) { this.db.runMaintenance(); } // The orchestrator only sees extraction-phase counts; resolution and // synthesizer edges (often >50% of the graph on JVM repos) come later. // Recompute against the DB so the CLI summary reports the true totals. if (result.success && result.filesIndexed > 0) { const after = this.queries.getNodeAndEdgeCount(); result.nodesCreated = after.nodes - before.nodes; result.edgesCreated = after.edges - before.edges; } // Stamp the index with the engine that built it, so `codegraph status` // and `codegraph upgrade` can recommend a re-index when the running // engine produces richer extraction than the one on disk. Only on a // real full index — a sync touches a subset, so it must NOT advance the // extraction stamp (the bulk would still be stale). See extraction-version.ts. if (result.success && result.filesIndexed > 0) { try { this.queries.setMetadata('indexed_with_version', CodeGraphPackageVersion); this.queries.setMetadata('indexed_with_extraction_version', String(EXTRACTION_VERSION)); } catch { /* metadata is advisory — never fail an index over it */ } } return result; } finally { this.fileLock.release(); } }); } /** * Index specific files * * Uses a mutex to prevent concurrent indexing operations. */ async indexFiles(filePaths: string[]): Promise { return this.indexMutex.withLock(async () => { try { this.fileLock.acquire(); } catch { return { success: false, filesIndexed: 0, filesSkipped: 0, filesErrored: 0, nodesCreated: 0, edgesCreated: 0, errors: [{ message: 'Could not acquire file lock - another process may be indexing', severity: 'error' as const }], durationMs: 0 }; } try { return this.orchestrator.indexFiles(filePaths); } finally { this.fileLock.release(); } }); } /** * Sync with current file state (incremental update) * * Uses a mutex to prevent concurrent indexing operations. */ async sync(options: IndexOptions = {}): Promise { return this.indexMutex.withLock(async () => { try { this.fileLock.acquire(); } catch { return { filesChecked: 0, filesAdded: 0, filesModified: 0, filesRemoved: 0, nodesUpdated: 0, durationMs: 0 }; } try { const result = await this.orchestrator.sync(options.onProgress); // Cross-file finalization (e.g. NestJS RouterModule prefixes). Run on // every sync that touched files so edits to `app.module.ts` propagate // to controllers in unchanged files. The pass is idempotent and cheap // (regex over *.module.ts only). if (result.filesAdded > 0 || result.filesModified > 0) { this.resolver.runPostExtract(); } // Resolve references if files were updated if (result.filesAdded > 0 || result.filesModified > 0) { if (result.changedFilePaths) { // Scope resolution to changed files (git fast path — bounded set) const unresolvedRefs = this.queries.getUnresolvedReferencesByFiles(result.changedFilePaths); options.onProgress?.({ phase: 'resolving', current: 0, total: unresolvedRefs.length, }); this.resolver.resolveAndPersist(unresolvedRefs, (current, total) => { options.onProgress?.({ phase: 'resolving', current, total, }); }); } else { // No git info — use batched resolution to avoid OOM const unresolvedCount = this.queries.getUnresolvedReferencesCount(); options.onProgress?.({ phase: 'resolving', current: 0, total: unresolvedCount, }); await this.resolveReferencesBatched((current, total) => { options.onProgress?.({ phase: 'resolving', current, total, }); }); } // Second pass: chained calls whose method lives on a supertype the // receiver conforms to (protocol-extension / inherited). Needs the // implements/extends edges built above (#750). this.resolver.resolveChainedCallsViaConformance(); // Same lifecycle for `this.` callback registrations whose // member is inherited from a supertype (#808). this.resolver.resolveDeferredThisMemberRefs(); } // Refresh planner stats + checkpoint the WAL after bulk writes. if (result.filesAdded > 0 || result.filesModified > 0 || result.filesRemoved > 0) { this.db.runMaintenance(); } return result; } finally { this.fileLock.release(); } }); } /** * Check if an indexing operation is currently in progress */ isIndexing(): boolean { return this.indexMutex.isLocked(); } // =========================================================================== // File Watching // =========================================================================== /** * Start watching for file changes and auto-syncing. * * Uses native OS file events (FSEvents on macOS, inotify on Linux 19+, * ReadDirectoryChangesW on Windows) with debouncing to avoid thrashing. * * @param options - Watch options (debounce delay, callbacks) * @returns true if watching started successfully */ watch(options: WatchOptions = {}): boolean { if (this.watcher?.isActive()) return true; this.watcher = new FileWatcher( this.projectRoot, async () => { const result = await this.sync(); // sync() returns this exact zero-shape iff it failed to acquire the // file lock (a real empty sync always has filesChecked > 0 because // scanDirectory ran). Surface that to the watcher as a typed error // so it keeps pendingFiles + reschedules instead of clearing them // (#449). if (result.filesChecked === 0 && result.durationMs === 0) { throw new LockUnavailableError(); } const filesChanged = result.filesAdded + result.filesModified + result.filesRemoved; return { filesChanged, durationMs: result.durationMs }; }, options ); return this.watcher.start(); } /** * Stop watching for file changes. */ unwatch(): void { if (this.watcher) { this.watcher.stop(); this.watcher = null; } } /** * Check if the file watcher is active. */ isWatching(): boolean { return this.watcher?.isActive() ?? false; } /** * True once live watching has permanently degraded (OS watch-resource * exhaustion, or a write lock held past the retry budget) and auto-sync is * disabled until the next {@link watch} call. Distinct from `!isWatching()`: * a stopped/never-started watcher is inactive but NOT degraded. MCP tools use * this to surface a whole-index "results may be stale" notice, since * `getPendingFiles()` goes empty once watching stops (#876). */ isWatcherDegraded(): boolean { return this.watcher?.isDegraded() ?? false; } /** The reason live watching degraded, or null if it is healthy (#876). */ getWatcherDegradedReason(): string | null { return this.watcher?.getDegradedReason() ?? null; } /** * Files seen by the file watcher since the last successful sync — * the per-file "stale" signal MCP tools attach to responses so an agent * can fall back to {@link Read} for just the affected file without * waiting for a debounced sync to complete (issue #403). * * Returns an empty list when the watcher isn't active, or no events have * arrived. Each entry includes `firstSeenMs` and `lastSeenMs` (wall-clock * `Date.now()` values) so callers can render "edited Nms ago", plus an * `indexing` flag indicating whether the in-flight sync (if any) will * absorb that file. */ getPendingFiles(): PendingFile[] { return this.watcher?.getPendingFiles() ?? []; } /** * Resolves once the file watcher has installed its watch set. Useful for * tests that need a deterministic boundary before asserting on * `getPendingFiles()`. Resolves immediately when no watcher is active. */ waitUntilWatcherReady(timeoutMs?: number): Promise { return this.watcher ? this.watcher.waitUntilReady(timeoutMs) : Promise.resolve(); } /** * Get files that have changed since last index */ getChangedFiles(): { added: string[]; modified: string[]; removed: string[] } { return this.orchestrator.getChangedFiles(); } /** * Most recent index timestamp (ms since epoch) across all tracked files, or * null when nothing is indexed yet. Lets library consumers check index * freshness without shelling out to `codegraph status --json`. (#329) */ getLastIndexedAt(): number | null { return this.queries.getLastIndexedAt(); } /** * Which engine built the current index: the package version + extraction * version stamped at the last full `indexAll`. Either field is null for an * index built before stamping existed (treated as stale). See * `extraction-version.ts` and `isIndexStale()`. */ getIndexBuildInfo(): { version: string | null; extractionVersion: number | null } { const version = this.queries.getMetadata('indexed_with_version'); const ev = this.queries.getMetadata('indexed_with_extraction_version'); const parsed = ev != null ? parseInt(ev, 10) : NaN; return { version, extractionVersion: Number.isFinite(parsed) ? parsed : null }; } /** * True when the on-disk index was built by an engine whose extraction is * older than the one now running — i.e. a re-index would add data a migration * can't backfill. False when there's no index yet (nothing to refresh) or the * stamp is current. This is the signal behind `codegraph status`'s re-index * hint and `codegraph upgrade`'s reminder. */ isIndexStale(): boolean { if (this.queries.getLastIndexedAt() == null) return false; const { extractionVersion } = this.getIndexBuildInfo(); return extractionVersion == null || extractionVersion < EXTRACTION_VERSION; } /** * Extract nodes and edges from source code (without storing) */ extractFromSource(filePath: string, source: string): ExtractionResult { return extractFromSource(filePath, source); } // =========================================================================== // Reference Resolution // =========================================================================== /** * Resolve unresolved references and create edges * * This method takes unresolved references from extraction and attempts * to resolve them using multiple strategies: * - Framework-specific patterns (React, Express, Laravel) * - Import-based resolution * - Name-based symbol matching */ resolveReferences(onProgress?: (current: number, total: number) => void): ResolutionResult { // Get all unresolved references from the database const unresolvedRefs = this.queries.getUnresolvedReferences(); return this.resolver.resolveAndPersist(unresolvedRefs, onProgress); } /** * Resolve references in batches to keep memory bounded on large codebases. * Processes chunks of unresolved refs, persisting results after each batch. */ async resolveReferencesBatched(onProgress?: (current: number, total: number) => void): Promise { return this.resolver.resolveAndPersistBatched(onProgress); } /** * Get detected frameworks in the project */ getDetectedFrameworks(): string[] { return this.resolver.getDetectedFrameworks(); } /** * Re-initialize the resolver (useful after adding new files) */ reinitializeResolver(): void { this.resolver.initialize(); } // =========================================================================== // Graph Statistics // =========================================================================== /** * Get statistics about the knowledge graph */ getStats(): GraphStats { const stats = this.queries.getStats(); stats.dbSizeBytes = this.db.getSize(); return stats; } /** * Active SQLite backend for this project's connection (`node-sqlite` — Node's * built-in real-SQLite module). Surfaced via `codegraph status` and the * `codegraph_status` MCP tool alongside the effective journal mode. */ getBackend(): import('./db').SqliteBackend { return this.db.getBackend(); } /** * The journal mode actually in effect ('wal', 'delete', …). 'wal' means * readers never block on a concurrent writer; anything else means they can, * which is the precondition for the "database is locked" failures in issue * #238. Surfaced via `codegraph status` and the `codegraph_status` MCP tool. */ getJournalMode(): string { return this.db.getJournalMode(); } // =========================================================================== // Node Operations // =========================================================================== /** * Get a node by ID */ getNode(id: string): Node | null { return this.queries.getNodeById(id); } /** * Get all nodes in a file */ getNodesInFile(filePath: string): Node[] { return this.queries.getNodesByFile(filePath); } /** * Get all nodes of a specific kind */ getNodesByKind(kind: Node['kind']): Node[] { return this.queries.getNodesByKind(kind); } /** * Get ALL nodes with an exact name (direct index lookup, not FTS-ranked/capped). * Used to enumerate every overload of a heavily-overloaded name so the specific * definition the caller wants is never dropped below a search cut. */ getNodesByName(name: string): Node[] { return this.queries.getNodesByName(name); } /** * Search nodes by text */ searchNodes(query: string, options?: SearchOptions): SearchResult[] { return this.queries.searchNodes(query, options); } /** * Normalized project-name tokens (go.mod / package.json / repo dir) used to * down-weight the non-discriminative project name in search ranking (#720). * Exposed so explore can exclude it from the PascalCase type-disambiguation * bias, which would otherwise pull overloaded tokens toward whichever stack * embeds the project name. */ getProjectNameTokens(): Set { return this.queries.getProjectNameTokens(); } /** * Find the project's "primary route file" — the file with the densest * concentration of framework-emitted `route` nodes (≥3 routes, ≥30% * of all non-test routes). Used to inline the routing config in * `codegraph_explore` responses on small realworld template repos * (rails-realworld, laravel-realworld, drupal-admintoolbar, …) where * Glob+Read of `routes.rb`/`urls.py`/etc. otherwise beats codegraph. */ getTopRouteFile(): { filePath: string; routeCount: number; totalRoutes: number } | null { return this.queries.getTopRouteFile(); } /** * Build a URL → handler routing manifest from the index. Each entry * pairs a route node (URL + method) with its handler function/method * via the `references` edge that framework resolvers emit. Returns * null when fewer than 3 valid (non-test) routes exist. */ getRoutingManifest(limit?: number): { entries: Array<{ url: string; handler: string; handlerFile: string; handlerLine: number; handlerKind: string }>; topHandlerFile: string | null; topHandlerFileCount: number; totalRoutes: number; } | null { return this.queries.getRoutingManifest(limit); } // =========================================================================== // Edge Operations // =========================================================================== /** * Get outgoing edges from a node */ getOutgoingEdges(nodeId: string): Edge[] { return this.queries.getOutgoingEdges(nodeId); } /** * Get incoming edges to a node */ getIncomingEdges(nodeId: string): Edge[] { return this.queries.getIncomingEdges(nodeId); } // =========================================================================== // File Operations // =========================================================================== /** * Get a file record by path */ getFile(filePath: string): FileRecord | null { return this.queries.getFileByPath(filePath); } /** * Get all tracked files */ getFiles(): FileRecord[] { return this.queries.getAllFiles(); } // =========================================================================== // Graph Query Methods // =========================================================================== /** * Get the context for a node (ancestors, children, references) * * Returns comprehensive context about a node including its containment * hierarchy, children, incoming/outgoing references, type information, * and relevant imports. * * @param nodeId - ID of the focal node * @returns Context object with all related information */ getContext(nodeId: string): Context { return this.graphManager.getContext(nodeId); } /** * Traverse the graph from a starting node * * Uses breadth-first search by default. Supports filtering by edge types, * node types, and traversal direction. * * @param startId - Starting node ID * @param options - Traversal options * @returns Subgraph containing traversed nodes and edges */ traverse(startId: string, options?: TraversalOptions): Subgraph { return this.traverser.traverseBFS(startId, options); } /** * Get the call graph for a function * * Returns both callers (functions that call this function) and * callees (functions called by this function) up to the specified depth. * * @param nodeId - ID of the function/method node * @param depth - Maximum depth in each direction (default: 2) * @returns Subgraph containing the call graph */ getCallGraph(nodeId: string, depth: number = 2): Subgraph { return this.traverser.getCallGraph(nodeId, depth); } /** * Get the type hierarchy for a class/interface * * Returns both ancestors (types this extends/implements) and * descendants (types that extend/implement this). * * @param nodeId - ID of the class/interface node * @returns Subgraph containing the type hierarchy */ getTypeHierarchy(nodeId: string): Subgraph { return this.traverser.getTypeHierarchy(nodeId); } /** * Find all usages of a symbol * * Returns all nodes that reference the specified symbol through * any edge type (calls, references, type_of, etc.). * * @param nodeId - ID of the symbol node * @returns Array of nodes and edges that reference this symbol */ findUsages(nodeId: string): Array<{ node: Node; edge: Edge }> { return this.traverser.findUsages(nodeId); } /** * Get callers of a function/method * * @param nodeId - ID of the function/method node * @param maxDepth - Maximum depth to traverse (default: 1) * @returns Array of nodes that call this function */ getCallers(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> { return this.traverser.getCallers(nodeId, maxDepth); } /** * Get callees of a function/method * * @param nodeId - ID of the function/method node * @param maxDepth - Maximum depth to traverse (default: 1) * @returns Array of nodes called by this function */ getCallees(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> { return this.traverser.getCallees(nodeId, maxDepth); } /** * Calculate the impact radius of a node * * Returns all nodes that could be affected by changes to this node. * * @param nodeId - ID of the node * @param maxDepth - Maximum depth to traverse (default: 3) * @returns Subgraph containing potentially impacted nodes */ getImpactRadius(nodeId: string, maxDepth: number = 3): Subgraph { return this.traverser.getImpactRadius(nodeId, maxDepth); } /** * Find the shortest path between two nodes * * @param fromId - Starting node ID * @param toId - Target node ID * @param edgeKinds - Edge types to consider (all if empty) * @returns Array of nodes and edges forming the path, or null if no path exists */ findPath( fromId: string, toId: string, edgeKinds?: Edge['kind'][] ): Array<{ node: Node; edge: Edge | null }> | null { return this.traverser.findPath(fromId, toId, edgeKinds); } /** * Get ancestors of a node in the containment hierarchy * * @param nodeId - ID of the node * @returns Array of ancestor nodes from immediate parent to root */ getAncestors(nodeId: string): Node[] { return this.traverser.getAncestors(nodeId); } /** * Get immediate children of a node * * @param nodeId - ID of the node * @returns Array of child nodes */ getChildren(nodeId: string): Node[] { return this.traverser.getChildren(nodeId); } /** * Get dependencies of a file * * @param filePath - Path to the file * @returns Array of file paths this file depends on */ getFileDependencies(filePath: string): string[] { return this.graphManager.getFileDependencies(filePath); } /** * Get dependents of a file * * @param filePath - Path to the file * @returns Array of file paths that depend on this file */ getFileDependents(filePath: string): string[] { return this.graphManager.getFileDependents(filePath); } /** * Find circular dependencies in the codebase * * @returns Array of cycles, each cycle is an array of file paths */ findCircularDependencies(): string[][] { return this.graphManager.findCircularDependencies(); } /** * Find dead code (unreferenced symbols) * * @param kinds - Node kinds to check (default: functions, methods, classes) * @returns Array of unreferenced nodes */ findDeadCode(kinds?: Node['kind'][]): Node[] { return this.graphManager.findDeadCode(kinds); } /** * Get complexity metrics for a node * * @param nodeId - ID of the node * @returns Object containing various complexity metrics */ getNodeMetrics(nodeId: string): { incomingEdgeCount: number; outgoingEdgeCount: number; callCount: number; callerCount: number; childCount: number; depth: number; } { return this.graphManager.getNodeMetrics(nodeId); } // =========================================================================== // Context Building // =========================================================================== /** * Get the source code for a node * * Reads the file and extracts the code between startLine and endLine. * * @param nodeId - ID of the node * @returns Code string or null if not found */ async getCode(nodeId: string): Promise { return this.contextBuilder.getCode(nodeId); } /** * Find relevant subgraph for a query * * Combines semantic search with graph traversal to find the most * relevant nodes and their relationships for a given query. * * @param query - Natural language query describing the task * @param options - Search and traversal options * @returns Subgraph of relevant nodes and edges */ async findRelevantContext( query: string, options?: FindRelevantContextOptions ): Promise { return this.contextBuilder.findRelevantContext(query, options); } /** * Build context for a task * * Creates comprehensive context by: * 1. Running FTS search to find entry points * 2. Expanding the graph around entry points * 3. Extracting code blocks for key nodes * 4. Formatting output for Claude * * @param input - Task description (string or {title, description}) * @param options - Build options (maxNodes, includeCode, format, etc.) * @returns TaskContext object or formatted string (markdown/JSON) */ async buildContext( input: TaskInput, options?: BuildContextOptions ): Promise { return this.contextBuilder.buildContext(input, options); } // =========================================================================== // Database Management // =========================================================================== /** * Optimize the database (vacuum and analyze) */ optimize(): void { this.db.optimize(); } /** * Clear all data from the graph */ clear(): void { this.queries.clear(); } /** * Alias for close() for backwards compatibility. * @deprecated Use close() instead */ destroy(): void { this.close(); } /** * Completely remove CodeGraph from the project. * This closes the database and deletes the .CodeGraph directory. * * WARNING: This permanently deletes all CodeGraph data for the project. */ uninitialize(): void { this.close(); removeDirectory(this.projectRoot); } } // Default export export default CodeGraph;