| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157 |
- /**
- * 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<CodeGraph> {
- 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<CodeGraph> {
- 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<IndexResult> {
- 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.<member>` 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<IndexResult> {
- 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<SyncResult> {
- 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.<member>` 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<void> {
- 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<ResolutionResult> {
- 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<string> {
- 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<string | null> {
- 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<Subgraph> {
- 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<TaskContext | string> {
- 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;
|