index.ts 28 KB

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