index.ts 29 KB

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