index.ts 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  1. /**
  2. * CodeGraph
  3. *
  4. * A local-first code intelligence system that builds a semantic
  5. * knowledge graph from any codebase.
  6. */
  7. import * as path from 'path';
  8. import {
  9. Node,
  10. Edge,
  11. FileRecord,
  12. ExtractionResult,
  13. Subgraph,
  14. TraversalOptions,
  15. SearchOptions,
  16. SearchResult,
  17. Context,
  18. GraphStats,
  19. TaskInput,
  20. TaskContext,
  21. BuildContextOptions,
  22. FindRelevantContextOptions,
  23. } from './types';
  24. import { DatabaseConnection, getDatabasePath } from './db';
  25. import { QueryBuilder } from './db/queries';
  26. import {
  27. isInitialized,
  28. createDirectory,
  29. removeDirectory,
  30. validateDirectory,
  31. } from './directory';
  32. import {
  33. ExtractionOrchestrator,
  34. IndexProgress,
  35. IndexResult,
  36. SyncResult,
  37. extractFromSource,
  38. initGrammars,
  39. } from './extraction';
  40. import {
  41. ReferenceResolver,
  42. createResolver,
  43. ResolutionResult,
  44. } from './resolution';
  45. import { GraphTraverser, GraphQueryManager } from './graph';
  46. import { ContextBuilder, createContextBuilder } from './context';
  47. import { Mutex, FileLock } from './utils';
  48. import { FileWatcher, WatchOptions, PendingFile, LockUnavailableError } from './sync';
  49. // Re-export types for consumers
  50. export * from './types';
  51. export { getDatabasePath } from './db';
  52. export {
  53. getCodeGraphDir,
  54. isInitialized,
  55. findNearestCodeGraphRoot,
  56. CODEGRAPH_DIR,
  57. } from './directory';
  58. export { IndexProgress, IndexResult, SyncResult } from './extraction';
  59. export { detectLanguage, isLanguageSupported, isGrammarLoaded, getSupportedLanguages, initGrammars, loadGrammarsForLanguages, loadAllGrammars } from './extraction';
  60. export { ResolutionResult } from './resolution';
  61. export {
  62. CodeGraphError,
  63. FileError,
  64. ParseError,
  65. DatabaseError,
  66. SearchError,
  67. VectorError,
  68. ConfigError,
  69. Logger,
  70. setLogger,
  71. getLogger,
  72. silentLogger,
  73. defaultLogger,
  74. } from './errors';
  75. export { Mutex, FileLock, processInBatches, debounce, throttle, MemoryMonitor } from './utils';
  76. export { FileWatcher, WatchOptions, PendingFile, LockUnavailableError } from './sync';
  77. export { MCPServer } from './mcp';
  78. /**
  79. * Options for initializing a new CodeGraph project
  80. */
  81. export interface InitOptions {
  82. /** Whether to run initial indexing after init */
  83. index?: boolean;
  84. /** Progress callback for indexing */
  85. onProgress?: (progress: IndexProgress) => void;
  86. }
  87. /**
  88. * Options for opening an existing CodeGraph project
  89. */
  90. export interface OpenOptions {
  91. /** Whether to run sync if files have changed */
  92. sync?: boolean;
  93. /** Whether to run in read-only mode */
  94. readOnly?: boolean;
  95. }
  96. /**
  97. * Options for indexing
  98. */
  99. export interface IndexOptions {
  100. /** Progress callback */
  101. onProgress?: (progress: IndexProgress) => void;
  102. /** Abort signal for cancellation */
  103. signal?: AbortSignal;
  104. /** Enable verbose logging (worker lifecycle, memory, timeouts) */
  105. verbose?: boolean;
  106. }
  107. /**
  108. * Main CodeGraph class
  109. *
  110. * Provides the primary interface for interacting with the code knowledge graph.
  111. */
  112. export class CodeGraph {
  113. private db: DatabaseConnection;
  114. private queries: QueryBuilder;
  115. private projectRoot: string;
  116. private orchestrator: ExtractionOrchestrator;
  117. private resolver: ReferenceResolver;
  118. private graphManager: GraphQueryManager;
  119. private traverser: GraphTraverser;
  120. private contextBuilder: ContextBuilder;
  121. // Mutex for preventing concurrent indexing operations (in-process)
  122. private indexMutex = new Mutex();
  123. // File lock for preventing concurrent writes across processes (CLI, MCP, git hooks)
  124. private fileLock: FileLock;
  125. // File watcher for auto-sync on file changes
  126. private watcher: FileWatcher | null = null;
  127. private constructor(
  128. db: DatabaseConnection,
  129. queries: QueryBuilder,
  130. projectRoot: string
  131. ) {
  132. this.db = db;
  133. this.queries = queries;
  134. this.projectRoot = projectRoot;
  135. this.fileLock = new FileLock(
  136. path.join(projectRoot, '.codegraph', 'codegraph.lock')
  137. );
  138. this.orchestrator = new ExtractionOrchestrator(projectRoot, queries);
  139. this.resolver = createResolver(projectRoot, queries);
  140. this.graphManager = new GraphQueryManager(queries);
  141. this.traverser = new GraphTraverser(queries);
  142. this.contextBuilder = createContextBuilder(
  143. projectRoot,
  144. queries,
  145. this.traverser
  146. );
  147. }
  148. // ===========================================================================
  149. // Lifecycle Methods
  150. // ===========================================================================
  151. /**
  152. * Initialize a new CodeGraph project
  153. *
  154. * Creates the .CodeGraph directory, database, and configuration.
  155. *
  156. * @param projectRoot - Path to the project root directory
  157. * @param options - Initialization options
  158. * @returns A new CodeGraph instance
  159. */
  160. static async init(projectRoot: string, options: InitOptions = {}): Promise<CodeGraph> {
  161. await initGrammars();
  162. const resolvedRoot = path.resolve(projectRoot);
  163. // Check if already initialized
  164. if (isInitialized(resolvedRoot)) {
  165. throw new Error(`CodeGraph already initialized in ${resolvedRoot}`);
  166. }
  167. // Create directory structure
  168. createDirectory(resolvedRoot);
  169. // Initialize database
  170. const dbPath = getDatabasePath(resolvedRoot);
  171. const db = DatabaseConnection.initialize(dbPath);
  172. const queries = new QueryBuilder(db.getDb());
  173. const instance = new CodeGraph(db, queries, resolvedRoot);
  174. // Run initial indexing if requested
  175. if (options.index) {
  176. await instance.indexAll({ onProgress: options.onProgress });
  177. }
  178. return instance;
  179. }
  180. /**
  181. * Initialize synchronously (without indexing)
  182. */
  183. static initSync(projectRoot: string): CodeGraph {
  184. const resolvedRoot = path.resolve(projectRoot);
  185. // Check if already initialized
  186. if (isInitialized(resolvedRoot)) {
  187. throw new Error(`CodeGraph already initialized in ${resolvedRoot}`);
  188. }
  189. // Create directory structure
  190. createDirectory(resolvedRoot);
  191. // Initialize database
  192. const dbPath = getDatabasePath(resolvedRoot);
  193. const db = DatabaseConnection.initialize(dbPath);
  194. const queries = new QueryBuilder(db.getDb());
  195. return new CodeGraph(db, queries, resolvedRoot);
  196. }
  197. /**
  198. * Open an existing CodeGraph project
  199. *
  200. * @param projectRoot - Path to the project root directory
  201. * @param options - Open options
  202. * @returns A CodeGraph instance
  203. */
  204. static async open(projectRoot: string, options: OpenOptions = {}): Promise<CodeGraph> {
  205. await initGrammars();
  206. const resolvedRoot = path.resolve(projectRoot);
  207. // Check if initialized
  208. if (!isInitialized(resolvedRoot)) {
  209. throw new Error(`CodeGraph not initialized in ${resolvedRoot}. Run init() first.`);
  210. }
  211. // Validate directory structure
  212. const validation = validateDirectory(resolvedRoot);
  213. if (!validation.valid) {
  214. throw new Error(`Invalid CodeGraph directory: ${validation.errors.join(', ')}`);
  215. }
  216. // Open database
  217. const dbPath = getDatabasePath(resolvedRoot);
  218. const db = DatabaseConnection.open(dbPath);
  219. const queries = new QueryBuilder(db.getDb());
  220. const instance = new CodeGraph(db, queries, resolvedRoot);
  221. // Sync if requested
  222. if (options.sync) {
  223. await instance.sync();
  224. }
  225. return instance;
  226. }
  227. /**
  228. * Open synchronously (without sync)
  229. */
  230. static openSync(projectRoot: string): CodeGraph {
  231. const resolvedRoot = path.resolve(projectRoot);
  232. // Check if initialized
  233. if (!isInitialized(resolvedRoot)) {
  234. throw new Error(`CodeGraph not initialized in ${resolvedRoot}. Run init() first.`);
  235. }
  236. // Validate directory structure
  237. const validation = validateDirectory(resolvedRoot);
  238. if (!validation.valid) {
  239. throw new Error(`Invalid CodeGraph directory: ${validation.errors.join(', ')}`);
  240. }
  241. // Open database
  242. const dbPath = getDatabasePath(resolvedRoot);
  243. const db = DatabaseConnection.open(dbPath);
  244. const queries = new QueryBuilder(db.getDb());
  245. return new CodeGraph(db, queries, resolvedRoot);
  246. }
  247. /**
  248. * Check if a directory has been initialized as a CodeGraph project
  249. */
  250. static isInitialized(projectRoot: string): boolean {
  251. return isInitialized(path.resolve(projectRoot));
  252. }
  253. /**
  254. * Close the CodeGraph instance and release resources
  255. */
  256. close(): void {
  257. this.unwatch();
  258. // Release file lock if held
  259. this.fileLock.release();
  260. this.db.close();
  261. }
  262. /**
  263. * Get the project root directory
  264. */
  265. getProjectRoot(): string {
  266. return this.projectRoot;
  267. }
  268. // ===========================================================================
  269. // Indexing
  270. // ===========================================================================
  271. /**
  272. * Index all files in the project
  273. *
  274. * Uses a mutex to prevent concurrent indexing operations.
  275. */
  276. async indexAll(options: IndexOptions = {}): Promise<IndexResult> {
  277. return this.indexMutex.withLock(async () => {
  278. try {
  279. this.fileLock.acquire();
  280. } catch {
  281. 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 };
  282. }
  283. try {
  284. const result = await this.orchestrator.indexAll(options.onProgress, options.signal, options.verbose);
  285. // Re-detect frameworks now that the index is populated. The resolver
  286. // is constructed with createResolver() before any files exist, so
  287. // framework resolvers whose detect() consults the indexed file list
  288. // (e.g. UIKit/SwiftUI scanning for imports, swift-objc-bridge looking
  289. // for both Swift and ObjC files) all return false on that initial pass
  290. // and silently drop themselves. Re-initializing here gives them a
  291. // chance to see the actual project before resolution runs.
  292. if (result.success && result.filesIndexed > 0) {
  293. this.resolver.initialize();
  294. // Cross-file finalization (e.g. NestJS RouterModule prefixes). Runs
  295. // before resolution so updated names show up in subsequent reads.
  296. this.resolver.runPostExtract();
  297. }
  298. // Resolve references to create call/import/extends edges
  299. if (result.success && result.filesIndexed > 0) {
  300. // Get count without loading all refs into memory
  301. const unresolvedCount = this.queries.getUnresolvedReferencesCount();
  302. options.onProgress?.({
  303. phase: 'resolving',
  304. current: 0,
  305. total: unresolvedCount,
  306. });
  307. await this.resolveReferencesBatched((current, total) => {
  308. options.onProgress?.({
  309. phase: 'resolving',
  310. current,
  311. total,
  312. });
  313. });
  314. }
  315. // Refresh planner stats + checkpoint the WAL after bulk writes.
  316. // Cheap and non-blocking; never load-bearing for correctness.
  317. if (result.success && result.filesIndexed > 0) {
  318. this.db.runMaintenance();
  319. }
  320. return result;
  321. } finally {
  322. this.fileLock.release();
  323. }
  324. });
  325. }
  326. /**
  327. * Index specific files
  328. *
  329. * Uses a mutex to prevent concurrent indexing operations.
  330. */
  331. async indexFiles(filePaths: string[]): Promise<IndexResult> {
  332. return this.indexMutex.withLock(async () => {
  333. try {
  334. this.fileLock.acquire();
  335. } catch {
  336. 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 };
  337. }
  338. try {
  339. return this.orchestrator.indexFiles(filePaths);
  340. } finally {
  341. this.fileLock.release();
  342. }
  343. });
  344. }
  345. /**
  346. * Sync with current file state (incremental update)
  347. *
  348. * Uses a mutex to prevent concurrent indexing operations.
  349. */
  350. async sync(options: IndexOptions = {}): Promise<SyncResult> {
  351. return this.indexMutex.withLock(async () => {
  352. try {
  353. this.fileLock.acquire();
  354. } catch {
  355. return { filesChecked: 0, filesAdded: 0, filesModified: 0, filesRemoved: 0, nodesUpdated: 0, durationMs: 0 };
  356. }
  357. try {
  358. const result = await this.orchestrator.sync(options.onProgress);
  359. // Cross-file finalization (e.g. NestJS RouterModule prefixes). Run on
  360. // every sync that touched files so edits to `app.module.ts` propagate
  361. // to controllers in unchanged files. The pass is idempotent and cheap
  362. // (regex over *.module.ts only).
  363. if (result.filesAdded > 0 || result.filesModified > 0) {
  364. this.resolver.runPostExtract();
  365. }
  366. // Resolve references if files were updated
  367. if (result.filesAdded > 0 || result.filesModified > 0) {
  368. if (result.changedFilePaths) {
  369. // Scope resolution to changed files (git fast path — bounded set)
  370. const unresolvedRefs = this.queries.getUnresolvedReferencesByFiles(result.changedFilePaths);
  371. options.onProgress?.({
  372. phase: 'resolving',
  373. current: 0,
  374. total: unresolvedRefs.length,
  375. });
  376. this.resolver.resolveAndPersist(unresolvedRefs, (current, total) => {
  377. options.onProgress?.({
  378. phase: 'resolving',
  379. current,
  380. total,
  381. });
  382. });
  383. } else {
  384. // No git info — use batched resolution to avoid OOM
  385. const unresolvedCount = this.queries.getUnresolvedReferencesCount();
  386. options.onProgress?.({
  387. phase: 'resolving',
  388. current: 0,
  389. total: unresolvedCount,
  390. });
  391. await this.resolveReferencesBatched((current, total) => {
  392. options.onProgress?.({
  393. phase: 'resolving',
  394. current,
  395. total,
  396. });
  397. });
  398. }
  399. }
  400. // Refresh planner stats + checkpoint the WAL after bulk writes.
  401. if (result.filesAdded > 0 || result.filesModified > 0 || result.filesRemoved > 0) {
  402. this.db.runMaintenance();
  403. }
  404. return result;
  405. } finally {
  406. this.fileLock.release();
  407. }
  408. });
  409. }
  410. /**
  411. * Check if an indexing operation is currently in progress
  412. */
  413. isIndexing(): boolean {
  414. return this.indexMutex.isLocked();
  415. }
  416. // ===========================================================================
  417. // File Watching
  418. // ===========================================================================
  419. /**
  420. * Start watching for file changes and auto-syncing.
  421. *
  422. * Uses native OS file events (FSEvents on macOS, inotify on Linux 19+,
  423. * ReadDirectoryChangesW on Windows) with debouncing to avoid thrashing.
  424. *
  425. * @param options - Watch options (debounce delay, callbacks)
  426. * @returns true if watching started successfully
  427. */
  428. watch(options: WatchOptions = {}): boolean {
  429. if (this.watcher?.isActive()) return true;
  430. this.watcher = new FileWatcher(
  431. this.projectRoot,
  432. async () => {
  433. const result = await this.sync();
  434. // sync() returns this exact zero-shape iff it failed to acquire the
  435. // file lock (a real empty sync always has filesChecked > 0 because
  436. // scanDirectory ran). Surface that to the watcher as a typed error
  437. // so it keeps pendingFiles + reschedules instead of clearing them
  438. // (#449).
  439. if (result.filesChecked === 0 && result.durationMs === 0) {
  440. throw new LockUnavailableError();
  441. }
  442. const filesChanged = result.filesAdded + result.filesModified + result.filesRemoved;
  443. return { filesChanged, durationMs: result.durationMs };
  444. },
  445. options
  446. );
  447. return this.watcher.start();
  448. }
  449. /**
  450. * Stop watching for file changes.
  451. */
  452. unwatch(): void {
  453. if (this.watcher) {
  454. this.watcher.stop();
  455. this.watcher = null;
  456. }
  457. }
  458. /**
  459. * Check if the file watcher is active.
  460. */
  461. isWatching(): boolean {
  462. return this.watcher?.isActive() ?? false;
  463. }
  464. /**
  465. * Files seen by the file watcher since the last successful sync —
  466. * the per-file "stale" signal MCP tools attach to responses so an agent
  467. * can fall back to {@link Read} for just the affected file without
  468. * waiting for a debounced sync to complete (issue #403).
  469. *
  470. * Returns an empty list when the watcher isn't active, or no events have
  471. * arrived. Each entry includes `firstSeenMs` and `lastSeenMs` (wall-clock
  472. * `Date.now()` values) so callers can render "edited Nms ago", plus an
  473. * `indexing` flag indicating whether the in-flight sync (if any) will
  474. * absorb that file.
  475. */
  476. getPendingFiles(): PendingFile[] {
  477. return this.watcher?.getPendingFiles() ?? [];
  478. }
  479. /**
  480. * Resolves once the file watcher has finished its initial chokidar scan.
  481. * Useful for tests that need a deterministic boundary before asserting on
  482. * `getPendingFiles()`. Resolves immediately when no watcher is active.
  483. */
  484. waitUntilWatcherReady(timeoutMs?: number): Promise<void> {
  485. return this.watcher ? this.watcher.waitUntilReady(timeoutMs) : Promise.resolve();
  486. }
  487. /**
  488. * Get files that have changed since last index
  489. */
  490. getChangedFiles(): { added: string[]; modified: string[]; removed: string[] } {
  491. return this.orchestrator.getChangedFiles();
  492. }
  493. /**
  494. * Extract nodes and edges from source code (without storing)
  495. */
  496. extractFromSource(filePath: string, source: string): ExtractionResult {
  497. return extractFromSource(filePath, source);
  498. }
  499. // ===========================================================================
  500. // Reference Resolution
  501. // ===========================================================================
  502. /**
  503. * Resolve unresolved references and create edges
  504. *
  505. * This method takes unresolved references from extraction and attempts
  506. * to resolve them using multiple strategies:
  507. * - Framework-specific patterns (React, Express, Laravel)
  508. * - Import-based resolution
  509. * - Name-based symbol matching
  510. */
  511. resolveReferences(onProgress?: (current: number, total: number) => void): ResolutionResult {
  512. // Get all unresolved references from the database
  513. const unresolvedRefs = this.queries.getUnresolvedReferences();
  514. return this.resolver.resolveAndPersist(unresolvedRefs, onProgress);
  515. }
  516. /**
  517. * Resolve references in batches to keep memory bounded on large codebases.
  518. * Processes chunks of unresolved refs, persisting results after each batch.
  519. */
  520. async resolveReferencesBatched(onProgress?: (current: number, total: number) => void): Promise<ResolutionResult> {
  521. return this.resolver.resolveAndPersistBatched(onProgress);
  522. }
  523. /**
  524. * Get detected frameworks in the project
  525. */
  526. getDetectedFrameworks(): string[] {
  527. return this.resolver.getDetectedFrameworks();
  528. }
  529. /**
  530. * Re-initialize the resolver (useful after adding new files)
  531. */
  532. reinitializeResolver(): void {
  533. this.resolver.initialize();
  534. }
  535. // ===========================================================================
  536. // Graph Statistics
  537. // ===========================================================================
  538. /**
  539. * Get statistics about the knowledge graph
  540. */
  541. getStats(): GraphStats {
  542. const stats = this.queries.getStats();
  543. stats.dbSizeBytes = this.db.getSize();
  544. return stats;
  545. }
  546. /**
  547. * Active SQLite backend for this project's connection (`node-sqlite` — Node's
  548. * built-in real-SQLite module). Surfaced via `codegraph status` and the
  549. * `codegraph_status` MCP tool alongside the effective journal mode.
  550. */
  551. getBackend(): import('./db').SqliteBackend {
  552. return this.db.getBackend();
  553. }
  554. /**
  555. * The journal mode actually in effect ('wal', 'delete', …). 'wal' means
  556. * readers never block on a concurrent writer; anything else means they can,
  557. * which is the precondition for the "database is locked" failures in issue
  558. * #238. Surfaced via `codegraph status` and the `codegraph_status` MCP tool.
  559. */
  560. getJournalMode(): string {
  561. return this.db.getJournalMode();
  562. }
  563. // ===========================================================================
  564. // Node Operations
  565. // ===========================================================================
  566. /**
  567. * Get a node by ID
  568. */
  569. getNode(id: string): Node | null {
  570. return this.queries.getNodeById(id);
  571. }
  572. /**
  573. * Get all nodes in a file
  574. */
  575. getNodesInFile(filePath: string): Node[] {
  576. return this.queries.getNodesByFile(filePath);
  577. }
  578. /**
  579. * Get all nodes of a specific kind
  580. */
  581. getNodesByKind(kind: Node['kind']): Node[] {
  582. return this.queries.getNodesByKind(kind);
  583. }
  584. /**
  585. * Search nodes by text
  586. */
  587. searchNodes(query: string, options?: SearchOptions): SearchResult[] {
  588. return this.queries.searchNodes(query, options);
  589. }
  590. // ===========================================================================
  591. // Edge Operations
  592. // ===========================================================================
  593. /**
  594. * Get outgoing edges from a node
  595. */
  596. getOutgoingEdges(nodeId: string): Edge[] {
  597. return this.queries.getOutgoingEdges(nodeId);
  598. }
  599. /**
  600. * Get incoming edges to a node
  601. */
  602. getIncomingEdges(nodeId: string): Edge[] {
  603. return this.queries.getIncomingEdges(nodeId);
  604. }
  605. // ===========================================================================
  606. // File Operations
  607. // ===========================================================================
  608. /**
  609. * Get a file record by path
  610. */
  611. getFile(filePath: string): FileRecord | null {
  612. return this.queries.getFileByPath(filePath);
  613. }
  614. /**
  615. * Get all tracked files
  616. */
  617. getFiles(): FileRecord[] {
  618. return this.queries.getAllFiles();
  619. }
  620. // ===========================================================================
  621. // Graph Query Methods
  622. // ===========================================================================
  623. /**
  624. * Get the context for a node (ancestors, children, references)
  625. *
  626. * Returns comprehensive context about a node including its containment
  627. * hierarchy, children, incoming/outgoing references, type information,
  628. * and relevant imports.
  629. *
  630. * @param nodeId - ID of the focal node
  631. * @returns Context object with all related information
  632. */
  633. getContext(nodeId: string): Context {
  634. return this.graphManager.getContext(nodeId);
  635. }
  636. /**
  637. * Traverse the graph from a starting node
  638. *
  639. * Uses breadth-first search by default. Supports filtering by edge types,
  640. * node types, and traversal direction.
  641. *
  642. * @param startId - Starting node ID
  643. * @param options - Traversal options
  644. * @returns Subgraph containing traversed nodes and edges
  645. */
  646. traverse(startId: string, options?: TraversalOptions): Subgraph {
  647. return this.traverser.traverseBFS(startId, options);
  648. }
  649. /**
  650. * Get the call graph for a function
  651. *
  652. * Returns both callers (functions that call this function) and
  653. * callees (functions called by this function) up to the specified depth.
  654. *
  655. * @param nodeId - ID of the function/method node
  656. * @param depth - Maximum depth in each direction (default: 2)
  657. * @returns Subgraph containing the call graph
  658. */
  659. getCallGraph(nodeId: string, depth: number = 2): Subgraph {
  660. return this.traverser.getCallGraph(nodeId, depth);
  661. }
  662. /**
  663. * Get the type hierarchy for a class/interface
  664. *
  665. * Returns both ancestors (types this extends/implements) and
  666. * descendants (types that extend/implement this).
  667. *
  668. * @param nodeId - ID of the class/interface node
  669. * @returns Subgraph containing the type hierarchy
  670. */
  671. getTypeHierarchy(nodeId: string): Subgraph {
  672. return this.traverser.getTypeHierarchy(nodeId);
  673. }
  674. /**
  675. * Find all usages of a symbol
  676. *
  677. * Returns all nodes that reference the specified symbol through
  678. * any edge type (calls, references, type_of, etc.).
  679. *
  680. * @param nodeId - ID of the symbol node
  681. * @returns Array of nodes and edges that reference this symbol
  682. */
  683. findUsages(nodeId: string): Array<{ node: Node; edge: Edge }> {
  684. return this.traverser.findUsages(nodeId);
  685. }
  686. /**
  687. * Get callers of a function/method
  688. *
  689. * @param nodeId - ID of the function/method node
  690. * @param maxDepth - Maximum depth to traverse (default: 1)
  691. * @returns Array of nodes that call this function
  692. */
  693. getCallers(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> {
  694. return this.traverser.getCallers(nodeId, maxDepth);
  695. }
  696. /**
  697. * Get callees of a function/method
  698. *
  699. * @param nodeId - ID of the function/method node
  700. * @param maxDepth - Maximum depth to traverse (default: 1)
  701. * @returns Array of nodes called by this function
  702. */
  703. getCallees(nodeId: string, maxDepth: number = 1): Array<{ node: Node; edge: Edge }> {
  704. return this.traverser.getCallees(nodeId, maxDepth);
  705. }
  706. /**
  707. * Calculate the impact radius of a node
  708. *
  709. * Returns all nodes that could be affected by changes to this node.
  710. *
  711. * @param nodeId - ID of the node
  712. * @param maxDepth - Maximum depth to traverse (default: 3)
  713. * @returns Subgraph containing potentially impacted nodes
  714. */
  715. getImpactRadius(nodeId: string, maxDepth: number = 3): Subgraph {
  716. return this.traverser.getImpactRadius(nodeId, maxDepth);
  717. }
  718. /**
  719. * Find the shortest path between two nodes
  720. *
  721. * @param fromId - Starting node ID
  722. * @param toId - Target node ID
  723. * @param edgeKinds - Edge types to consider (all if empty)
  724. * @returns Array of nodes and edges forming the path, or null if no path exists
  725. */
  726. findPath(
  727. fromId: string,
  728. toId: string,
  729. edgeKinds?: Edge['kind'][]
  730. ): Array<{ node: Node; edge: Edge | null }> | null {
  731. return this.traverser.findPath(fromId, toId, edgeKinds);
  732. }
  733. /**
  734. * Get ancestors of a node in the containment hierarchy
  735. *
  736. * @param nodeId - ID of the node
  737. * @returns Array of ancestor nodes from immediate parent to root
  738. */
  739. getAncestors(nodeId: string): Node[] {
  740. return this.traverser.getAncestors(nodeId);
  741. }
  742. /**
  743. * Get immediate children of a node
  744. *
  745. * @param nodeId - ID of the node
  746. * @returns Array of child nodes
  747. */
  748. getChildren(nodeId: string): Node[] {
  749. return this.traverser.getChildren(nodeId);
  750. }
  751. /**
  752. * Get dependencies of a file
  753. *
  754. * @param filePath - Path to the file
  755. * @returns Array of file paths this file depends on
  756. */
  757. getFileDependencies(filePath: string): string[] {
  758. return this.graphManager.getFileDependencies(filePath);
  759. }
  760. /**
  761. * Get dependents of a file
  762. *
  763. * @param filePath - Path to the file
  764. * @returns Array of file paths that depend on this file
  765. */
  766. getFileDependents(filePath: string): string[] {
  767. return this.graphManager.getFileDependents(filePath);
  768. }
  769. /**
  770. * Find circular dependencies in the codebase
  771. *
  772. * @returns Array of cycles, each cycle is an array of file paths
  773. */
  774. findCircularDependencies(): string[][] {
  775. return this.graphManager.findCircularDependencies();
  776. }
  777. /**
  778. * Find dead code (unreferenced symbols)
  779. *
  780. * @param kinds - Node kinds to check (default: functions, methods, classes)
  781. * @returns Array of unreferenced nodes
  782. */
  783. findDeadCode(kinds?: Node['kind'][]): Node[] {
  784. return this.graphManager.findDeadCode(kinds);
  785. }
  786. /**
  787. * Get complexity metrics for a node
  788. *
  789. * @param nodeId - ID of the node
  790. * @returns Object containing various complexity metrics
  791. */
  792. getNodeMetrics(nodeId: string): {
  793. incomingEdgeCount: number;
  794. outgoingEdgeCount: number;
  795. callCount: number;
  796. callerCount: number;
  797. childCount: number;
  798. depth: number;
  799. } {
  800. return this.graphManager.getNodeMetrics(nodeId);
  801. }
  802. // ===========================================================================
  803. // Context Building
  804. // ===========================================================================
  805. /**
  806. * Get the source code for a node
  807. *
  808. * Reads the file and extracts the code between startLine and endLine.
  809. *
  810. * @param nodeId - ID of the node
  811. * @returns Code string or null if not found
  812. */
  813. async getCode(nodeId: string): Promise<string | null> {
  814. return this.contextBuilder.getCode(nodeId);
  815. }
  816. /**
  817. * Find relevant subgraph for a query
  818. *
  819. * Combines semantic search with graph traversal to find the most
  820. * relevant nodes and their relationships for a given query.
  821. *
  822. * @param query - Natural language query describing the task
  823. * @param options - Search and traversal options
  824. * @returns Subgraph of relevant nodes and edges
  825. */
  826. async findRelevantContext(
  827. query: string,
  828. options?: FindRelevantContextOptions
  829. ): Promise<Subgraph> {
  830. return this.contextBuilder.findRelevantContext(query, options);
  831. }
  832. /**
  833. * Build context for a task
  834. *
  835. * Creates comprehensive context by:
  836. * 1. Running FTS search to find entry points
  837. * 2. Expanding the graph around entry points
  838. * 3. Extracting code blocks for key nodes
  839. * 4. Formatting output for Claude
  840. *
  841. * @param input - Task description (string or {title, description})
  842. * @param options - Build options (maxNodes, includeCode, format, etc.)
  843. * @returns TaskContext object or formatted string (markdown/JSON)
  844. */
  845. async buildContext(
  846. input: TaskInput,
  847. options?: BuildContextOptions
  848. ): Promise<TaskContext | string> {
  849. return this.contextBuilder.buildContext(input, options);
  850. }
  851. // ===========================================================================
  852. // Database Management
  853. // ===========================================================================
  854. /**
  855. * Optimize the database (vacuum and analyze)
  856. */
  857. optimize(): void {
  858. this.db.optimize();
  859. }
  860. /**
  861. * Clear all data from the graph
  862. */
  863. clear(): void {
  864. this.queries.clear();
  865. }
  866. /**
  867. * Alias for close() for backwards compatibility.
  868. * @deprecated Use close() instead
  869. */
  870. destroy(): void {
  871. this.close();
  872. }
  873. /**
  874. * Completely remove CodeGraph from the project.
  875. * This closes the database and deletes the .CodeGraph directory.
  876. *
  877. * WARNING: This permanently deletes all CodeGraph data for the project.
  878. */
  879. uninitialize(): void {
  880. this.close();
  881. removeDirectory(this.projectRoot);
  882. }
  883. }
  884. // Default export
  885. export default CodeGraph;