index.ts 30 KB

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