@optave/codegraph 3.8.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/README.md +9 -8
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +95 -87
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/metrics.d.ts +0 -3
  6. package/dist/ast-analysis/metrics.d.ts.map +1 -1
  7. package/dist/ast-analysis/metrics.js +30 -13
  8. package/dist/ast-analysis/metrics.js.map +1 -1
  9. package/dist/ast-analysis/shared.d.ts.map +1 -1
  10. package/dist/ast-analysis/shared.js +24 -19
  11. package/dist/ast-analysis/shared.js.map +1 -1
  12. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitor-utils.js +55 -39
  14. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  15. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitor.js +91 -70
  17. package/dist/ast-analysis/visitor.js.map +1 -1
  18. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  19. package/dist/ast-analysis/visitors/ast-store-visitor.js +54 -58
  20. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  21. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  22. package/dist/ast-analysis/visitors/complexity-visitor.js +32 -39
  23. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  24. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  25. package/dist/ast-analysis/visitors/dataflow-visitor.js +57 -38
  26. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  27. package/dist/cli/commands/watch.d.ts.map +1 -1
  28. package/dist/cli/commands/watch.js +16 -2
  29. package/dist/cli/commands/watch.js.map +1 -1
  30. package/dist/db/connection.d.ts.map +1 -1
  31. package/dist/db/connection.js +29 -26
  32. package/dist/db/connection.js.map +1 -1
  33. package/dist/db/query-builder.d.ts.map +1 -1
  34. package/dist/db/query-builder.js +16 -5
  35. package/dist/db/query-builder.js.map +1 -1
  36. package/dist/db/repository/base.d.ts +10 -0
  37. package/dist/db/repository/base.d.ts.map +1 -1
  38. package/dist/db/repository/base.js +17 -0
  39. package/dist/db/repository/base.js.map +1 -1
  40. package/dist/db/repository/native-repository.d.ts +6 -1
  41. package/dist/db/repository/native-repository.d.ts.map +1 -1
  42. package/dist/db/repository/native-repository.js +77 -1
  43. package/dist/db/repository/native-repository.js.map +1 -1
  44. package/dist/db/repository/nodes.d.ts.map +1 -1
  45. package/dist/db/repository/nodes.js +8 -4
  46. package/dist/db/repository/nodes.js.map +1 -1
  47. package/dist/db/repository/sqlite-repository.d.ts +3 -0
  48. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  49. package/dist/db/repository/sqlite-repository.js +26 -0
  50. package/dist/db/repository/sqlite-repository.js.map +1 -1
  51. package/dist/domain/analysis/brief.d.ts.map +1 -1
  52. package/dist/domain/analysis/brief.js +13 -17
  53. package/dist/domain/analysis/brief.js.map +1 -1
  54. package/dist/domain/analysis/context.d.ts.map +1 -1
  55. package/dist/domain/analysis/context.js +14 -11
  56. package/dist/domain/analysis/context.js.map +1 -1
  57. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  58. package/dist/domain/analysis/dependencies.js +53 -52
  59. package/dist/domain/analysis/dependencies.js.map +1 -1
  60. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  61. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  62. package/dist/domain/analysis/fn-impact.js +33 -31
  63. package/dist/domain/analysis/fn-impact.js.map +1 -1
  64. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  65. package/dist/domain/analysis/implementations.js +11 -19
  66. package/dist/domain/analysis/implementations.js.map +1 -1
  67. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  68. package/dist/domain/analysis/module-map.js +55 -76
  69. package/dist/domain/analysis/module-map.js.map +1 -1
  70. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  71. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  72. package/dist/domain/analysis/query-helpers.js +15 -1
  73. package/dist/domain/analysis/query-helpers.js.map +1 -1
  74. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  75. package/dist/domain/graph/builder/pipeline.js +255 -105
  76. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  77. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  78. package/dist/domain/graph/builder/stages/build-edges.js +22 -17
  79. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  80. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  81. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  82. package/dist/domain/graph/builder/stages/finalize.js +2 -2
  83. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  84. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  85. package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
  86. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  87. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  88. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  89. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  90. package/dist/domain/graph/cycles.d.ts +6 -0
  91. package/dist/domain/graph/cycles.d.ts.map +1 -1
  92. package/dist/domain/graph/cycles.js +114 -22
  93. package/dist/domain/graph/cycles.js.map +1 -1
  94. package/dist/domain/graph/resolve.js +1 -1
  95. package/dist/domain/graph/resolve.js.map +1 -1
  96. package/dist/domain/graph/watcher.d.ts +2 -0
  97. package/dist/domain/graph/watcher.d.ts.map +1 -1
  98. package/dist/domain/graph/watcher.js +170 -75
  99. package/dist/domain/graph/watcher.js.map +1 -1
  100. package/dist/domain/parser.d.ts +0 -5
  101. package/dist/domain/parser.d.ts.map +1 -1
  102. package/dist/domain/parser.js +13 -28
  103. package/dist/domain/parser.js.map +1 -1
  104. package/dist/domain/search/generator.js +1 -1
  105. package/dist/domain/search/generator.js.map +1 -1
  106. package/dist/domain/search/models.d.ts +4 -3
  107. package/dist/domain/search/models.d.ts.map +1 -1
  108. package/dist/domain/search/models.js +18 -5
  109. package/dist/domain/search/models.js.map +1 -1
  110. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  111. package/dist/domain/search/search/hybrid.js +29 -18
  112. package/dist/domain/search/search/hybrid.js.map +1 -1
  113. package/dist/extractors/go.js +36 -33
  114. package/dist/extractors/go.js.map +1 -1
  115. package/dist/extractors/helpers.d.ts.map +1 -1
  116. package/dist/extractors/helpers.js +40 -29
  117. package/dist/extractors/helpers.js.map +1 -1
  118. package/dist/extractors/java.js +58 -46
  119. package/dist/extractors/java.js.map +1 -1
  120. package/dist/extractors/javascript.js +46 -45
  121. package/dist/extractors/javascript.js.map +1 -1
  122. package/dist/extractors/kotlin.js +84 -78
  123. package/dist/extractors/kotlin.js.map +1 -1
  124. package/dist/extractors/python.js +29 -24
  125. package/dist/extractors/python.js.map +1 -1
  126. package/dist/extractors/rust.js +41 -32
  127. package/dist/extractors/rust.js.map +1 -1
  128. package/dist/extractors/solidity.js +58 -67
  129. package/dist/extractors/solidity.js.map +1 -1
  130. package/dist/extractors/swift.js +83 -81
  131. package/dist/extractors/swift.js.map +1 -1
  132. package/dist/extractors/zig.js +58 -60
  133. package/dist/extractors/zig.js.map +1 -1
  134. package/dist/features/ast.d.ts +16 -14
  135. package/dist/features/ast.d.ts.map +1 -1
  136. package/dist/features/ast.js +83 -81
  137. package/dist/features/ast.js.map +1 -1
  138. package/dist/features/audit.d.ts.map +1 -1
  139. package/dist/features/audit.js +8 -6
  140. package/dist/features/audit.js.map +1 -1
  141. package/dist/features/branch-compare.d.ts.map +1 -1
  142. package/dist/features/branch-compare.js +69 -72
  143. package/dist/features/branch-compare.js.map +1 -1
  144. package/dist/features/communities.d.ts.map +1 -1
  145. package/dist/features/communities.js +19 -7
  146. package/dist/features/communities.js.map +1 -1
  147. package/dist/features/complexity.d.ts.map +1 -1
  148. package/dist/features/complexity.js +120 -125
  149. package/dist/features/complexity.js.map +1 -1
  150. package/dist/features/dataflow.d.ts.map +1 -1
  151. package/dist/features/dataflow.js +136 -137
  152. package/dist/features/dataflow.js.map +1 -1
  153. package/dist/features/flow.d.ts.map +1 -1
  154. package/dist/features/flow.js +84 -79
  155. package/dist/features/flow.js.map +1 -1
  156. package/dist/features/structure-query.d.ts.map +1 -1
  157. package/dist/features/structure-query.js +69 -65
  158. package/dist/features/structure-query.js.map +1 -1
  159. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  160. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  161. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  162. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  163. package/dist/graph/algorithms/leiden/partition.js +288 -266
  164. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  165. package/dist/graph/model.d.ts.map +1 -1
  166. package/dist/graph/model.js +5 -1
  167. package/dist/graph/model.js.map +1 -1
  168. package/dist/infrastructure/config.d.ts.map +1 -1
  169. package/dist/infrastructure/config.js +6 -4
  170. package/dist/infrastructure/config.js.map +1 -1
  171. package/dist/infrastructure/suppress.d.ts +25 -0
  172. package/dist/infrastructure/suppress.d.ts.map +1 -0
  173. package/dist/infrastructure/suppress.js +43 -0
  174. package/dist/infrastructure/suppress.js.map +1 -0
  175. package/dist/mcp/server.d.ts.map +1 -1
  176. package/dist/mcp/server.js +29 -24
  177. package/dist/mcp/server.js.map +1 -1
  178. package/dist/presentation/dataflow.d.ts.map +1 -1
  179. package/dist/presentation/dataflow.js +47 -38
  180. package/dist/presentation/dataflow.js.map +1 -1
  181. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  182. package/dist/presentation/diff-impact-mermaid.js +60 -51
  183. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  184. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  185. package/dist/presentation/queries-cli/exports.js +20 -14
  186. package/dist/presentation/queries-cli/exports.js.map +1 -1
  187. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  188. package/dist/presentation/queries-cli/impact.js +15 -13
  189. package/dist/presentation/queries-cli/impact.js.map +1 -1
  190. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  191. package/dist/presentation/queries-cli/inspect.js +101 -79
  192. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  193. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  194. package/dist/presentation/queries-cli/overview.js +25 -16
  195. package/dist/presentation/queries-cli/overview.js.map +1 -1
  196. package/dist/presentation/queries-cli/path.js +26 -20
  197. package/dist/presentation/queries-cli/path.js.map +1 -1
  198. package/dist/presentation/result-formatter.d.ts +10 -0
  199. package/dist/presentation/result-formatter.d.ts.map +1 -1
  200. package/dist/presentation/result-formatter.js +16 -1
  201. package/dist/presentation/result-formatter.js.map +1 -1
  202. package/dist/presentation/viewer.d.ts.map +1 -1
  203. package/dist/presentation/viewer.js +18 -12
  204. package/dist/presentation/viewer.js.map +1 -1
  205. package/dist/shared/errors.d.ts +5 -0
  206. package/dist/shared/errors.d.ts.map +1 -1
  207. package/dist/shared/errors.js +5 -0
  208. package/dist/shared/errors.js.map +1 -1
  209. package/dist/shared/hierarchy.d.ts +8 -2
  210. package/dist/shared/hierarchy.d.ts.map +1 -1
  211. package/dist/shared/hierarchy.js +42 -1
  212. package/dist/shared/hierarchy.js.map +1 -1
  213. package/dist/shared/normalize.d.ts +6 -1
  214. package/dist/shared/normalize.d.ts.map +1 -1
  215. package/dist/shared/normalize.js +20 -12
  216. package/dist/shared/normalize.js.map +1 -1
  217. package/dist/shared/paginate.d.ts +0 -9
  218. package/dist/shared/paginate.d.ts.map +1 -1
  219. package/dist/shared/paginate.js +0 -15
  220. package/dist/shared/paginate.js.map +1 -1
  221. package/dist/types.d.ts +10 -4
  222. package/dist/types.d.ts.map +1 -1
  223. package/package.json +7 -7
  224. package/src/ast-analysis/engine.ts +126 -105
  225. package/src/ast-analysis/metrics.ts +33 -11
  226. package/src/ast-analysis/shared.ts +33 -24
  227. package/src/ast-analysis/visitor-utils.ts +52 -32
  228. package/src/ast-analysis/visitor.ts +132 -71
  229. package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
  230. package/src/ast-analysis/visitors/complexity-visitor.ts +35 -40
  231. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  232. package/src/cli/commands/watch.ts +16 -2
  233. package/src/db/connection.ts +29 -28
  234. package/src/db/query-builder.ts +15 -3
  235. package/src/db/repository/base.ts +20 -0
  236. package/src/db/repository/native-repository.ts +79 -1
  237. package/src/db/repository/nodes.ts +13 -8
  238. package/src/db/repository/sqlite-repository.ts +29 -0
  239. package/src/domain/analysis/brief.ts +15 -25
  240. package/src/domain/analysis/context.ts +17 -10
  241. package/src/domain/analysis/dependencies.ts +67 -76
  242. package/src/domain/analysis/fn-impact.ts +36 -43
  243. package/src/domain/analysis/implementations.ts +11 -17
  244. package/src/domain/analysis/module-map.ts +58 -92
  245. package/src/domain/analysis/query-helpers.ts +18 -1
  246. package/src/domain/graph/builder/pipeline.ts +286 -97
  247. package/src/domain/graph/builder/stages/build-edges.ts +22 -18
  248. package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
  249. package/src/domain/graph/builder/stages/finalize.ts +2 -2
  250. package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
  251. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  252. package/src/domain/graph/cycles.ts +110 -23
  253. package/src/domain/graph/resolve.ts +1 -1
  254. package/src/domain/graph/watcher.ts +202 -96
  255. package/src/domain/parser.ts +14 -26
  256. package/src/domain/search/generator.ts +1 -1
  257. package/src/domain/search/models.ts +17 -4
  258. package/src/domain/search/search/hybrid.ts +69 -51
  259. package/src/extractors/go.ts +43 -33
  260. package/src/extractors/helpers.ts +37 -23
  261. package/src/extractors/java.ts +66 -47
  262. package/src/extractors/javascript.ts +45 -46
  263. package/src/extractors/kotlin.ts +84 -77
  264. package/src/extractors/python.ts +31 -25
  265. package/src/extractors/rust.ts +37 -29
  266. package/src/extractors/solidity.ts +57 -61
  267. package/src/extractors/swift.ts +81 -80
  268. package/src/extractors/zig.ts +58 -61
  269. package/src/features/ast.ts +130 -110
  270. package/src/features/audit.ts +8 -6
  271. package/src/features/branch-compare.ts +105 -79
  272. package/src/features/communities.ts +25 -10
  273. package/src/features/complexity.ts +171 -134
  274. package/src/features/dataflow.ts +165 -175
  275. package/src/features/flow.ts +129 -92
  276. package/src/features/structure-query.ts +79 -64
  277. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  278. package/src/graph/algorithms/leiden/partition.ts +359 -294
  279. package/src/graph/model.ts +6 -1
  280. package/src/infrastructure/config.ts +6 -4
  281. package/src/infrastructure/suppress.ts +47 -0
  282. package/src/mcp/server.ts +53 -37
  283. package/src/presentation/dataflow.ts +50 -44
  284. package/src/presentation/diff-impact-mermaid.ts +104 -62
  285. package/src/presentation/queries-cli/exports.ts +21 -13
  286. package/src/presentation/queries-cli/impact.ts +15 -13
  287. package/src/presentation/queries-cli/inspect.ts +100 -81
  288. package/src/presentation/queries-cli/overview.ts +26 -16
  289. package/src/presentation/queries-cli/path.ts +33 -25
  290. package/src/presentation/result-formatter.ts +19 -1
  291. package/src/presentation/viewer.ts +42 -14
  292. package/src/shared/errors.ts +6 -0
  293. package/src/shared/hierarchy.ts +50 -2
  294. package/src/shared/normalize.ts +31 -12
  295. package/src/shared/paginate.ts +0 -17
  296. package/src/types.ts +24 -4
@@ -18,29 +18,8 @@ function isTrackedExt(filePath: string): boolean {
18
18
  return EXTENSIONS.has(path.extname(filePath));
19
19
  }
20
20
 
21
- export async function watchProject(rootDir: string, opts: { engine?: string } = {}): Promise<void> {
22
- const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
23
- if (!fs.existsSync(dbPath)) {
24
- throw new DbError('No graph.db found. Run `codegraph build` first.', { file: dbPath });
25
- }
26
-
27
- const db = openDb(dbPath);
28
- initSchema(db);
29
- const engineOpts: import('../../types.js').EngineOpts = {
30
- engine: (opts.engine || 'auto') as import('../../types.js').EngineMode,
31
- dataflow: false,
32
- ast: false,
33
- };
34
- const { name: engineName, version: engineVersion } = getActiveEngine(engineOpts);
35
- info(`Watch mode using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
36
-
37
- const cache = createParseTreeCache();
38
- info(
39
- cache
40
- ? 'Incremental parsing enabled (native tree cache)'
41
- : 'Incremental parsing unavailable (full re-parse)',
42
- );
43
-
21
+ /** Prepare all SQL statements needed by the watcher's incremental rebuild. */
22
+ function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStmts {
44
23
  const stmts = {
45
24
  insertNode: db.prepare(
46
25
  'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)',
@@ -67,7 +46,6 @@ export async function watchProject(rootDir: string, opts: { engine?: string } =
67
46
  listSymbols: db.prepare("SELECT name, kind, line FROM nodes WHERE file = ? AND kind != 'file'"),
68
47
  };
69
48
 
70
- // Use named params for statements needing the same value twice
71
49
  const origDeleteEdges = db.prepare(
72
50
  `DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`,
73
51
  );
@@ -79,96 +57,224 @@ export async function watchProject(rootDir: string, opts: { engine?: string } =
79
57
  get: (f: string) => origCountEdges.get({ f }) as { c: number } | undefined,
80
58
  };
81
59
 
60
+ return stmts as IncrementalStmts;
61
+ }
62
+
63
+ /** Rebuild result shape from rebuildFile. */
64
+ interface RebuildResult {
65
+ file: string;
66
+ deleted?: boolean;
67
+ event: string;
68
+ symbolDiff: unknown;
69
+ nodesBefore: number;
70
+ nodesAfter: number;
71
+ nodesAdded: number;
72
+ nodesRemoved: number;
73
+ edgesAdded: number;
74
+ }
75
+
76
+ /** Process a batch of pending file changes: rebuild, journal, and log. */
77
+ async function processPendingFiles(
78
+ files: string[],
79
+ db: ReturnType<typeof openDb>,
80
+ rootDir: string,
81
+ stmts: IncrementalStmts,
82
+ engineOpts: import('../../types.js').EngineOpts,
83
+ cache: ReturnType<typeof createParseTreeCache>,
84
+ ): Promise<void> {
85
+ const results: RebuildResult[] = [];
86
+ for (const filePath of files) {
87
+ const result = (await rebuildFile(db, rootDir, filePath, stmts, engineOpts, cache, {
88
+ diffSymbols: diffSymbols as (old: unknown[], new_: unknown[]) => unknown,
89
+ })) as RebuildResult | null;
90
+ if (result) results.push(result);
91
+ }
92
+
93
+ if (results.length > 0) {
94
+ writeJournalAndChangeEvents(rootDir, results);
95
+ }
96
+
97
+ logRebuildResults(results);
98
+ }
99
+
100
+ /** Write journal entries and change events for processed files. */
101
+ function writeJournalAndChangeEvents(rootDir: string, updates: RebuildResult[]): void {
102
+ const entries = updates.map((r) => ({
103
+ file: r.file,
104
+ deleted: r.deleted || false,
105
+ }));
106
+ try {
107
+ appendJournalEntries(rootDir, entries);
108
+ } catch (e: unknown) {
109
+ debug(`Journal write failed (non-fatal): ${(e as Error).message}`);
110
+ }
111
+
112
+ const changeEvents = updates.map((r) =>
113
+ buildChangeEvent(r.file, r.event, r.symbolDiff, {
114
+ nodesBefore: r.nodesBefore,
115
+ nodesAfter: r.nodesAfter,
116
+ edgesAdded: r.edgesAdded,
117
+ }),
118
+ );
119
+ try {
120
+ appendChangeEvents(rootDir, changeEvents);
121
+ } catch (e: unknown) {
122
+ debug(`Change event write failed (non-fatal): ${(e as Error).message}`);
123
+ }
124
+ }
125
+
126
+ /** Log rebuild results to the user. */
127
+ function logRebuildResults(updates: RebuildResult[]): void {
128
+ for (const r of updates) {
129
+ const nodeDelta = r.nodesAdded - r.nodesRemoved;
130
+ const nodeStr = nodeDelta >= 0 ? `+${nodeDelta}` : `${nodeDelta}`;
131
+ if (r.deleted) {
132
+ info(`Removed: ${r.file} (-${r.nodesRemoved} nodes)`);
133
+ } else {
134
+ info(`Updated: ${r.file} (${nodeStr} nodes, +${r.edgesAdded} edges)`);
135
+ }
136
+ }
137
+ }
138
+
139
+ /** Recursively collect tracked source files for stat-based polling. */
140
+ function collectTrackedFiles(dir: string, result: string[]): void {
141
+ let entries: fs.Dirent[];
142
+ try {
143
+ entries = fs.readdirSync(dir, { withFileTypes: true });
144
+ } catch {
145
+ return;
146
+ }
147
+ for (const entry of entries) {
148
+ if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
149
+ const full = path.join(dir, entry.name);
150
+ if (entry.isDirectory()) {
151
+ collectTrackedFiles(full, result);
152
+ } else if (EXTENSIONS.has(path.extname(entry.name))) {
153
+ result.push(full);
154
+ }
155
+ }
156
+ }
157
+
158
+ export async function watchProject(
159
+ rootDir: string,
160
+ opts: { engine?: string; poll?: boolean; pollInterval?: number } = {},
161
+ ): Promise<void> {
162
+ const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
163
+ if (!fs.existsSync(dbPath)) {
164
+ throw new DbError('No graph.db found. Run `codegraph build` first.', { file: dbPath });
165
+ }
166
+
167
+ const db = openDb(dbPath);
168
+ initSchema(db);
169
+ const engineOpts: import('../../types.js').EngineOpts = {
170
+ engine: (opts.engine || 'auto') as import('../../types.js').EngineMode,
171
+ dataflow: false,
172
+ ast: false,
173
+ };
174
+ const { name: engineName, version: engineVersion } = getActiveEngine(engineOpts);
175
+ info(`Watch mode using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
176
+
177
+ const cache = createParseTreeCache();
178
+ info(
179
+ cache
180
+ ? 'Incremental parsing enabled (native tree cache)'
181
+ : 'Incremental parsing unavailable (full re-parse)',
182
+ );
183
+
184
+ const stmts = prepareWatcherStatements(db);
185
+
82
186
  const pending = new Set<string>();
83
187
  let timer: ReturnType<typeof setTimeout> | null = null;
84
188
  const DEBOUNCE_MS = 300;
85
189
 
86
- async function processPending(): Promise<void> {
87
- const files = [...pending];
88
- pending.clear();
89
-
90
- const results: Array<{
91
- file: string;
92
- deleted?: boolean;
93
- event: string;
94
- symbolDiff: unknown;
95
- nodesBefore: number;
96
- nodesAfter: number;
97
- nodesAdded: number;
98
- nodesRemoved: number;
99
- edgesAdded: number;
100
- }> = [];
101
- for (const filePath of files) {
102
- const result = (await rebuildFile(
103
- db,
104
- rootDir,
105
- filePath,
106
- stmts as IncrementalStmts,
107
- engineOpts,
108
- cache,
109
- {
110
- diffSymbols: diffSymbols as (old: unknown[], new_: unknown[]) => unknown,
111
- },
112
- )) as (typeof results)[number] | null;
113
- if (result) results.push(result);
114
- }
115
- const updates = results;
190
+ const usePoll = opts.poll ?? process.platform === 'win32';
191
+ const POLL_INTERVAL_MS = opts.pollInterval ?? 2000;
116
192
 
117
- // Append processed files to journal for Tier 0 detection on next build
118
- if (updates.length > 0) {
119
- const entries = updates.map((r) => ({
120
- file: r.file,
121
- deleted: r.deleted || false,
122
- }));
123
- try {
124
- appendJournalEntries(rootDir, entries);
125
- } catch (e: unknown) {
126
- debug(`Journal write failed (non-fatal): ${(e as Error).message}`);
127
- }
193
+ info(`Watching ${rootDir} for changes${usePoll ? ' (polling mode)' : ''}...`);
194
+ info('Press Ctrl+C to stop.');
195
+
196
+ let cleanup: () => void;
197
+
198
+ if (usePoll) {
199
+ // Polling mode: avoids native OS file watchers (NtNotifyChangeDirectoryFileEx)
200
+ // which can crash ReFS drivers on Windows Dev Drives.
201
+ const mtimeMap = new Map<string, number>();
128
202
 
129
- const changeEvents = updates.map((r) =>
130
- buildChangeEvent(r.file, r.event, r.symbolDiff, {
131
- nodesBefore: r.nodesBefore,
132
- nodesAfter: r.nodesAfter,
133
- edgesAdded: r.edgesAdded,
134
- }),
135
- );
203
+ // Seed initial mtimes
204
+ const initial: string[] = [];
205
+ collectTrackedFiles(rootDir, initial);
206
+ for (const f of initial) {
136
207
  try {
137
- appendChangeEvents(rootDir, changeEvents);
138
- } catch (e: unknown) {
139
- debug(`Change event write failed (non-fatal): ${(e as Error).message}`);
208
+ mtimeMap.set(f, fs.statSync(f).mtimeMs);
209
+ } catch {
210
+ /* deleted between collect and stat */
140
211
  }
141
212
  }
213
+ info(`Polling ${initial.length} tracked files every ${POLL_INTERVAL_MS}ms`);
214
+
215
+ const pollTimer = setInterval(() => {
216
+ const current: string[] = [];
217
+ collectTrackedFiles(rootDir, current);
218
+ const currentSet = new Set(current);
142
219
 
143
- for (const r of updates) {
144
- const nodeDelta = r.nodesAdded - r.nodesRemoved;
145
- const nodeStr = nodeDelta >= 0 ? `+${nodeDelta}` : `${nodeDelta}`;
146
- if (r.deleted) {
147
- info(`Removed: ${r.file} (-${r.nodesRemoved} nodes)`);
148
- } else {
149
- info(`Updated: ${r.file} (${nodeStr} nodes, +${r.edgesAdded} edges)`);
220
+ // Detect modified or new files
221
+ for (const f of current) {
222
+ try {
223
+ const mtime = fs.statSync(f).mtimeMs;
224
+ const prev = mtimeMap.get(f);
225
+ if (prev === undefined || mtime !== prev) {
226
+ mtimeMap.set(f, mtime);
227
+ pending.add(f);
228
+ }
229
+ } catch {
230
+ /* deleted between collect and stat */
231
+ }
150
232
  }
151
- }
152
- }
153
233
 
154
- info(`Watching ${rootDir} for changes...`);
155
- info('Press Ctrl+C to stop.');
234
+ // Detect deleted files
235
+ for (const f of mtimeMap.keys()) {
236
+ if (!currentSet.has(f)) {
237
+ mtimeMap.delete(f);
238
+ pending.add(f);
239
+ }
240
+ }
156
241
 
157
- const watcher = fs.watch(rootDir, { recursive: true }, (_eventType, filename) => {
158
- if (!filename) return;
159
- if (shouldIgnore(filename)) return;
160
- if (!isTrackedExt(filename)) return;
242
+ if (pending.size > 0) {
243
+ if (timer) clearTimeout(timer);
244
+ timer = setTimeout(async () => {
245
+ const files = [...pending];
246
+ pending.clear();
247
+ await processPendingFiles(files, db, rootDir, stmts, engineOpts, cache);
248
+ }, DEBOUNCE_MS);
249
+ }
250
+ }, POLL_INTERVAL_MS);
161
251
 
162
- const fullPath = path.join(rootDir, filename);
163
- pending.add(fullPath);
252
+ cleanup = () => clearInterval(pollTimer);
253
+ } else {
254
+ // Native OS watcher — efficient but can trigger ReFS crashes on Windows Dev Drives.
255
+ // Use --poll if you experience BSOD/HYPERVISOR_ERROR on ReFS volumes.
256
+ const watcher = fs.watch(rootDir, { recursive: true }, (_eventType, filename) => {
257
+ if (!filename) return;
258
+ if (shouldIgnore(filename)) return;
259
+ if (!isTrackedExt(filename)) return;
164
260
 
165
- if (timer) clearTimeout(timer);
166
- timer = setTimeout(processPending, DEBOUNCE_MS);
167
- });
261
+ const fullPath = path.join(rootDir, filename);
262
+ pending.add(fullPath);
263
+
264
+ if (timer) clearTimeout(timer);
265
+ timer = setTimeout(async () => {
266
+ const files = [...pending];
267
+ pending.clear();
268
+ await processPendingFiles(files, db, rootDir, stmts, engineOpts, cache);
269
+ }, DEBOUNCE_MS);
270
+ });
271
+
272
+ cleanup = () => watcher.close();
273
+ }
168
274
 
169
275
  process.on('SIGINT', () => {
170
276
  info('Stopping watcher...');
171
- watcher.close();
277
+ cleanup();
172
278
  // Flush any pending file paths to journal before exit
173
279
  if (pending.size > 0) {
174
280
  const entries = [...pending].map((filePath) => ({
@@ -193,39 +193,27 @@ export async function createParsers(): Promise<Map<string, Parser | null>> {
193
193
  * Call this between repeated builds in the same process (e.g. benchmarks)
194
194
  * to prevent memory accumulation that can cause segfaults.
195
195
  */
196
- export function disposeParsers(): void {
197
- if (_cachedParsers) {
198
- for (const [id, parser] of _cachedParsers) {
199
- if (parser && typeof parser.delete === 'function') {
200
- try {
201
- parser.delete();
202
- } catch (e: unknown) {
203
- debug(`Failed to dispose parser ${id}: ${(e as Error).message}`);
204
- }
205
- }
206
- }
207
- _cachedParsers = null;
208
- }
209
- for (const [id, query] of _queryCache) {
210
- if (query && typeof query.delete === 'function') {
196
+ function disposeMapEntries(entries: Iterable<[string, any]>, label: string): void {
197
+ for (const [id, item] of entries) {
198
+ if (item && typeof item.delete === 'function') {
211
199
  try {
212
- query.delete();
200
+ item.delete();
213
201
  } catch (e: unknown) {
214
- debug(`Failed to dispose query ${id}: ${(e as Error).message}`);
202
+ debug(`Failed to dispose ${label} ${id}: ${(e as Error).message}`);
215
203
  }
216
204
  }
217
205
  }
206
+ }
207
+
208
+ export function disposeParsers(): void {
209
+ if (_cachedParsers) {
210
+ disposeMapEntries(_cachedParsers, 'parser');
211
+ _cachedParsers = null;
212
+ }
213
+ disposeMapEntries(_queryCache, 'query');
218
214
  _queryCache.clear();
219
215
  if (_cachedLanguages) {
220
- for (const [id, lang] of _cachedLanguages) {
221
- if (lang && typeof (lang as any).delete === 'function') {
222
- try {
223
- (lang as any).delete();
224
- } catch (e: unknown) {
225
- debug(`Failed to dispose language ${id}: ${(e as Error).message}`);
226
- }
227
- }
228
- }
216
+ disposeMapEntries(_cachedLanguages, 'language');
229
217
  _cachedLanguages = null;
230
218
  }
231
219
  _initialized = false;
@@ -100,7 +100,7 @@ export async function buildEmbeddings(
100
100
  let overflowCount = 0;
101
101
 
102
102
  for (const [file, fileNodes] of byFile) {
103
- const fullPath = path.join(rootDir, file);
103
+ const fullPath = path.isAbsolute(file) ? file : path.join(rootDir, file);
104
104
  let lines: string[];
105
105
  try {
106
106
  lines = fs.readFileSync(fullPath, 'utf-8').split('\n');
@@ -96,13 +96,26 @@ export function getModelConfig(modelKey?: string): ModelConfig {
96
96
  }
97
97
 
98
98
  /**
99
- * Prompt the user to install a missing package interactively.
99
+ * Attempt to install a missing package.
100
+ * In TTY environments, prompts the user for confirmation first.
101
+ * In non-TTY environments (CI, piped stdin), installs automatically with a log message.
100
102
  * Returns true if the package was installed, false otherwise.
101
- * Skips the prompt entirely in non-TTY environments (CI, piped stdin).
102
103
  * @internal Not part of the public barrel.
103
104
  */
104
105
  export function promptInstall(packageName: string): Promise<boolean> {
105
- if (!process.stdin.isTTY) return Promise.resolve(false);
106
+ if (!process.stdin.isTTY) {
107
+ info(`Installing ${packageName} (optional dependency for semantic search)…`);
108
+ try {
109
+ execFileSync('npm', ['install', '--no-save', packageName], {
110
+ stdio: 'inherit',
111
+ timeout: 300_000,
112
+ });
113
+ return Promise.resolve(true);
114
+ } catch (err) {
115
+ info(`Auto-install failed: ${err instanceof Error ? err.message : String(err)}`);
116
+ return Promise.resolve(false);
117
+ }
118
+ }
106
119
 
107
120
  return new Promise((resolve) => {
108
121
  const rl = createInterface({ input: process.stdin, output: process.stderr });
@@ -128,7 +141,7 @@ export function promptInstall(packageName: string): Promise<boolean> {
128
141
  /**
129
142
  * Lazy-load @huggingface/transformers.
130
143
  * If the package is missing, prompts the user to install it interactively.
131
- * In non-TTY environments, prints an error and exits.
144
+ * In non-TTY environments, attempts automatic installation.
132
145
  * @internal Not part of the public barrel.
133
146
  */
134
147
  export async function loadTransformers(): Promise<unknown> {
@@ -25,45 +25,51 @@ export interface HybridSearchResult {
25
25
  results: HybridResult[];
26
26
  }
27
27
 
28
- export async function hybridSearchData(
29
- query: string,
30
- customDbPath: string | undefined,
31
- opts: SemanticSearchOpts = {},
32
- ): Promise<HybridSearchResult | null> {
33
- const config = opts.config || loadConfig();
34
- const searchCfg = config.search || ({} as CodegraphConfig['search']);
35
- const limit = opts.limit ?? searchCfg.topK ?? 15;
36
- const k = opts.rrfK ?? searchCfg.rrfK ?? 60;
37
- const topK = (opts.limit ?? searchCfg.topK ?? 15) * 5;
38
-
39
- const queries =
40
- typeof query === 'string'
41
- ? query
42
- .split(';')
43
- .map((q) => q.trim())
44
- .filter((q) => q.length > 0)
45
- : [query];
28
+ interface RankedItem {
29
+ key: string;
30
+ rank: number;
31
+ source: 'bm25' | 'semantic';
32
+ name: string;
33
+ kind: string;
34
+ file: string;
35
+ line: number;
36
+ endLine?: number | null;
37
+ role?: string | null;
38
+ fileHash?: string | null;
39
+ bm25Score?: number;
40
+ similarity?: number;
41
+ }
46
42
 
47
- const checkDb = openReadonlyOrFail(customDbPath) as BetterSqlite3Database;
48
- const ftsAvailable = hasFtsIndex(checkDb);
49
- checkDb.close();
50
- if (!ftsAvailable) return null;
43
+ interface FusionEntry {
44
+ name: string;
45
+ kind: string;
46
+ file: string;
47
+ line: number;
48
+ endLine: number | null;
49
+ role: string | null;
50
+ fileHash: string | null;
51
+ rrfScore: number;
52
+ bm25Score: number | null;
53
+ bm25Rank: number | null;
54
+ similarity: number | null;
55
+ semanticRank: number | null;
56
+ }
51
57
 
52
- interface RankedItem {
53
- key: string;
54
- rank: number;
55
- source: 'bm25' | 'semantic';
56
- name: string;
57
- kind: string;
58
- file: string;
59
- line: number;
60
- endLine?: number | null;
61
- role?: string | null;
62
- fileHash?: string | null;
63
- bm25Score?: number;
64
- similarity?: number;
65
- }
58
+ /** Parse a semicolon-delimited query string into individual queries. */
59
+ function parseQueries(query: string): string[] {
60
+ return query
61
+ .split(';')
62
+ .map((q) => q.trim())
63
+ .filter((q) => q.length > 0);
64
+ }
66
65
 
66
+ /** Collect BM25 and semantic ranked lists for each query. */
67
+ async function collectRankedLists(
68
+ queries: string[],
69
+ customDbPath: string | undefined,
70
+ opts: SemanticSearchOpts,
71
+ topK: number,
72
+ ): Promise<RankedItem[][]> {
67
73
  const rankedLists: RankedItem[][] = [];
68
74
 
69
75
  for (const q of queries) {
@@ -96,22 +102,13 @@ export async function hybridSearchData(
96
102
  }
97
103
  }
98
104
 
99
- interface FusionEntry {
100
- name: string;
101
- kind: string;
102
- file: string;
103
- line: number;
104
- endLine: number | null;
105
- role: string | null;
106
- fileHash: string | null;
107
- rrfScore: number;
108
- bm25Score: number | null;
109
- bm25Rank: number | null;
110
- similarity: number | null;
111
- semanticRank: number | null;
112
- }
105
+ return rankedLists;
106
+ }
113
107
 
108
+ /** Reciprocal Rank Fusion: merge ranked lists into a single scored result set. */
109
+ function fuseResults(rankedLists: RankedItem[][], k: number, limit: number): HybridResult[] {
114
110
  const fusionMap = new Map<string, FusionEntry>();
111
+
115
112
  for (const list of rankedLists) {
116
113
  for (const item of list) {
117
114
  if (!fusionMap.has(item.key)) {
@@ -146,7 +143,7 @@ export async function hybridSearchData(
146
143
  }
147
144
  }
148
145
 
149
- const results: HybridResult[] = [...fusionMap.values()]
146
+ return [...fusionMap.values()]
150
147
  .sort((a, b) => b.rrfScore - a.rrfScore)
151
148
  .slice(0, limit)
152
149
  .map((e) => ({
@@ -163,6 +160,27 @@ export async function hybridSearchData(
163
160
  similarity: e.similarity,
164
161
  semanticRank: e.semanticRank,
165
162
  }));
163
+ }
164
+
165
+ export async function hybridSearchData(
166
+ query: string,
167
+ customDbPath: string | undefined,
168
+ opts: SemanticSearchOpts = {},
169
+ ): Promise<HybridSearchResult | null> {
170
+ const config = opts.config || loadConfig();
171
+ const searchCfg = config.search || ({} as CodegraphConfig['search']);
172
+ const limit = opts.limit ?? searchCfg.topK ?? 15;
173
+ const k = opts.rrfK ?? searchCfg.rrfK ?? 60;
174
+ const topK = (opts.limit ?? searchCfg.topK ?? 15) * 5;
175
+
176
+ const checkDb = openReadonlyOrFail(customDbPath) as BetterSqlite3Database;
177
+ const ftsAvailable = hasFtsIndex(checkDb);
178
+ checkDb.close();
179
+ if (!ftsAvailable) return null;
180
+
181
+ const queries = parseQueries(query);
182
+ const rankedLists = await collectRankedLists(queries, customDbPath, opts, topK);
183
+ const results = fuseResults(rankedLists, k, limit);
166
184
 
167
185
  return { results };
168
186
  }