index.ts 32 KB

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