index.ts 30 KB

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