@optave/codegraph 3.8.0 → 3.9.0

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 (310) hide show
  1. package/README.md +13 -8
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +137 -86
  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 +81 -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/branch-compare.d.ts.map +1 -1
  28. package/dist/cli/commands/branch-compare.js +4 -0
  29. package/dist/cli/commands/branch-compare.js.map +1 -1
  30. package/dist/cli/commands/diff-impact.d.ts.map +1 -1
  31. package/dist/cli/commands/diff-impact.js +2 -1
  32. package/dist/cli/commands/diff-impact.js.map +1 -1
  33. package/dist/cli/commands/info.d.ts.map +1 -1
  34. package/dist/cli/commands/info.js +3 -2
  35. package/dist/cli/commands/info.js.map +1 -1
  36. package/dist/cli/commands/watch.d.ts.map +1 -1
  37. package/dist/cli/commands/watch.js +16 -2
  38. package/dist/cli/commands/watch.js.map +1 -1
  39. package/dist/db/connection.d.ts.map +1 -1
  40. package/dist/db/connection.js +29 -26
  41. package/dist/db/connection.js.map +1 -1
  42. package/dist/db/query-builder.d.ts.map +1 -1
  43. package/dist/db/query-builder.js +16 -5
  44. package/dist/db/query-builder.js.map +1 -1
  45. package/dist/db/repository/base.d.ts +16 -0
  46. package/dist/db/repository/base.d.ts.map +1 -1
  47. package/dist/db/repository/base.js +31 -0
  48. package/dist/db/repository/base.js.map +1 -1
  49. package/dist/db/repository/native-repository.d.ts +7 -1
  50. package/dist/db/repository/native-repository.d.ts.map +1 -1
  51. package/dist/db/repository/native-repository.js +100 -1
  52. package/dist/db/repository/native-repository.js.map +1 -1
  53. package/dist/db/repository/nodes.d.ts.map +1 -1
  54. package/dist/db/repository/nodes.js +8 -4
  55. package/dist/db/repository/nodes.js.map +1 -1
  56. package/dist/db/repository/sqlite-repository.d.ts +4 -0
  57. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  58. package/dist/db/repository/sqlite-repository.js +51 -0
  59. package/dist/db/repository/sqlite-repository.js.map +1 -1
  60. package/dist/domain/analysis/brief.d.ts.map +1 -1
  61. package/dist/domain/analysis/brief.js +13 -17
  62. package/dist/domain/analysis/brief.js.map +1 -1
  63. package/dist/domain/analysis/context.d.ts.map +1 -1
  64. package/dist/domain/analysis/context.js +14 -11
  65. package/dist/domain/analysis/context.js.map +1 -1
  66. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  67. package/dist/domain/analysis/dependencies.js +64 -59
  68. package/dist/domain/analysis/dependencies.js.map +1 -1
  69. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  70. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  71. package/dist/domain/analysis/fn-impact.js +33 -31
  72. package/dist/domain/analysis/fn-impact.js.map +1 -1
  73. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  74. package/dist/domain/analysis/implementations.js +11 -19
  75. package/dist/domain/analysis/implementations.js.map +1 -1
  76. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  77. package/dist/domain/analysis/module-map.js +55 -76
  78. package/dist/domain/analysis/module-map.js.map +1 -1
  79. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  80. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  81. package/dist/domain/analysis/query-helpers.js +15 -1
  82. package/dist/domain/analysis/query-helpers.js.map +1 -1
  83. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  84. package/dist/domain/graph/builder/pipeline.js +352 -107
  85. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  86. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/build-edges.js +49 -18
  88. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  89. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  90. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  91. package/dist/domain/graph/builder/stages/finalize.js +2 -2
  92. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  93. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  94. package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
  95. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  96. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  97. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  98. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  99. package/dist/domain/graph/cycles.d.ts +6 -0
  100. package/dist/domain/graph/cycles.d.ts.map +1 -1
  101. package/dist/domain/graph/cycles.js +114 -22
  102. package/dist/domain/graph/cycles.js.map +1 -1
  103. package/dist/domain/graph/resolve.js +1 -1
  104. package/dist/domain/graph/resolve.js.map +1 -1
  105. package/dist/domain/graph/watcher.d.ts +2 -0
  106. package/dist/domain/graph/watcher.d.ts.map +1 -1
  107. package/dist/domain/graph/watcher.js +170 -75
  108. package/dist/domain/graph/watcher.js.map +1 -1
  109. package/dist/domain/parser.d.ts +3 -4
  110. package/dist/domain/parser.d.ts.map +1 -1
  111. package/dist/domain/parser.js +141 -89
  112. package/dist/domain/parser.js.map +1 -1
  113. package/dist/domain/search/generator.js +1 -1
  114. package/dist/domain/search/generator.js.map +1 -1
  115. package/dist/domain/search/models.d.ts +4 -3
  116. package/dist/domain/search/models.d.ts.map +1 -1
  117. package/dist/domain/search/models.js +23 -8
  118. package/dist/domain/search/models.js.map +1 -1
  119. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  120. package/dist/domain/search/search/hybrid.js +29 -18
  121. package/dist/domain/search/search/hybrid.js.map +1 -1
  122. package/dist/extractors/go.js +36 -33
  123. package/dist/extractors/go.js.map +1 -1
  124. package/dist/extractors/helpers.d.ts.map +1 -1
  125. package/dist/extractors/helpers.js +40 -29
  126. package/dist/extractors/helpers.js.map +1 -1
  127. package/dist/extractors/java.js +58 -46
  128. package/dist/extractors/java.js.map +1 -1
  129. package/dist/extractors/javascript.js +65 -54
  130. package/dist/extractors/javascript.js.map +1 -1
  131. package/dist/extractors/kotlin.js +84 -78
  132. package/dist/extractors/kotlin.js.map +1 -1
  133. package/dist/extractors/python.js +29 -24
  134. package/dist/extractors/python.js.map +1 -1
  135. package/dist/extractors/rust.js +41 -32
  136. package/dist/extractors/rust.js.map +1 -1
  137. package/dist/extractors/solidity.js +58 -67
  138. package/dist/extractors/solidity.js.map +1 -1
  139. package/dist/extractors/swift.js +83 -81
  140. package/dist/extractors/swift.js.map +1 -1
  141. package/dist/extractors/zig.js +58 -60
  142. package/dist/extractors/zig.js.map +1 -1
  143. package/dist/features/ast.d.ts +16 -14
  144. package/dist/features/ast.d.ts.map +1 -1
  145. package/dist/features/ast.js +83 -81
  146. package/dist/features/ast.js.map +1 -1
  147. package/dist/features/audit.d.ts.map +1 -1
  148. package/dist/features/audit.js +8 -6
  149. package/dist/features/audit.js.map +1 -1
  150. package/dist/features/branch-compare.d.ts.map +1 -1
  151. package/dist/features/branch-compare.js +69 -72
  152. package/dist/features/branch-compare.js.map +1 -1
  153. package/dist/features/communities.d.ts.map +1 -1
  154. package/dist/features/communities.js +19 -7
  155. package/dist/features/communities.js.map +1 -1
  156. package/dist/features/complexity.d.ts.map +1 -1
  157. package/dist/features/complexity.js +120 -125
  158. package/dist/features/complexity.js.map +1 -1
  159. package/dist/features/dataflow.d.ts.map +1 -1
  160. package/dist/features/dataflow.js +136 -137
  161. package/dist/features/dataflow.js.map +1 -1
  162. package/dist/features/flow.d.ts.map +1 -1
  163. package/dist/features/flow.js +84 -79
  164. package/dist/features/flow.js.map +1 -1
  165. package/dist/features/structure-query.d.ts.map +1 -1
  166. package/dist/features/structure-query.js +69 -65
  167. package/dist/features/structure-query.js.map +1 -1
  168. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  169. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  170. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  171. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  172. package/dist/graph/algorithms/leiden/partition.js +288 -266
  173. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  174. package/dist/graph/model.d.ts.map +1 -1
  175. package/dist/graph/model.js +5 -1
  176. package/dist/graph/model.js.map +1 -1
  177. package/dist/infrastructure/config.d.ts.map +1 -1
  178. package/dist/infrastructure/config.js +6 -4
  179. package/dist/infrastructure/config.js.map +1 -1
  180. package/dist/infrastructure/suppress.d.ts +25 -0
  181. package/dist/infrastructure/suppress.d.ts.map +1 -0
  182. package/dist/infrastructure/suppress.js +43 -0
  183. package/dist/infrastructure/suppress.js.map +1 -0
  184. package/dist/mcp/server.d.ts.map +1 -1
  185. package/dist/mcp/server.js +29 -24
  186. package/dist/mcp/server.js.map +1 -1
  187. package/dist/presentation/dataflow.d.ts.map +1 -1
  188. package/dist/presentation/dataflow.js +47 -38
  189. package/dist/presentation/dataflow.js.map +1 -1
  190. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  191. package/dist/presentation/diff-impact-mermaid.js +60 -51
  192. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  193. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  194. package/dist/presentation/queries-cli/exports.js +20 -14
  195. package/dist/presentation/queries-cli/exports.js.map +1 -1
  196. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  197. package/dist/presentation/queries-cli/impact.js +15 -13
  198. package/dist/presentation/queries-cli/impact.js.map +1 -1
  199. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  200. package/dist/presentation/queries-cli/inspect.js +101 -79
  201. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  202. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  203. package/dist/presentation/queries-cli/overview.js +25 -16
  204. package/dist/presentation/queries-cli/overview.js.map +1 -1
  205. package/dist/presentation/queries-cli/path.js +26 -20
  206. package/dist/presentation/queries-cli/path.js.map +1 -1
  207. package/dist/presentation/result-formatter.d.ts +10 -0
  208. package/dist/presentation/result-formatter.d.ts.map +1 -1
  209. package/dist/presentation/result-formatter.js +16 -1
  210. package/dist/presentation/result-formatter.js.map +1 -1
  211. package/dist/presentation/viewer.d.ts.map +1 -1
  212. package/dist/presentation/viewer.js +18 -12
  213. package/dist/presentation/viewer.js.map +1 -1
  214. package/dist/shared/errors.d.ts +5 -0
  215. package/dist/shared/errors.d.ts.map +1 -1
  216. package/dist/shared/errors.js +5 -0
  217. package/dist/shared/errors.js.map +1 -1
  218. package/dist/shared/hierarchy.d.ts +8 -2
  219. package/dist/shared/hierarchy.d.ts.map +1 -1
  220. package/dist/shared/hierarchy.js +42 -1
  221. package/dist/shared/hierarchy.js.map +1 -1
  222. package/dist/shared/normalize.d.ts +6 -1
  223. package/dist/shared/normalize.d.ts.map +1 -1
  224. package/dist/shared/normalize.js +20 -12
  225. package/dist/shared/normalize.js.map +1 -1
  226. package/dist/shared/paginate.d.ts +0 -9
  227. package/dist/shared/paginate.d.ts.map +1 -1
  228. package/dist/shared/paginate.js +0 -15
  229. package/dist/shared/paginate.js.map +1 -1
  230. package/dist/types.d.ts +12 -5
  231. package/dist/types.d.ts.map +1 -1
  232. package/grammars/tree-sitter-erlang.wasm +0 -0
  233. package/grammars/tree-sitter-gleam.wasm +0 -0
  234. package/package.json +9 -9
  235. package/src/ast-analysis/engine.ts +176 -104
  236. package/src/ast-analysis/metrics.ts +33 -11
  237. package/src/ast-analysis/shared.ts +33 -24
  238. package/src/ast-analysis/visitor-utils.ts +52 -32
  239. package/src/ast-analysis/visitor.ts +132 -71
  240. package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
  241. package/src/ast-analysis/visitors/complexity-visitor.ts +89 -40
  242. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  243. package/src/cli/commands/branch-compare.ts +4 -0
  244. package/src/cli/commands/diff-impact.ts +2 -1
  245. package/src/cli/commands/info.ts +3 -2
  246. package/src/cli/commands/watch.ts +16 -2
  247. package/src/db/connection.ts +29 -28
  248. package/src/db/query-builder.ts +15 -3
  249. package/src/db/repository/base.ts +34 -0
  250. package/src/db/repository/native-repository.ts +104 -1
  251. package/src/db/repository/nodes.ts +13 -8
  252. package/src/db/repository/sqlite-repository.ts +55 -0
  253. package/src/domain/analysis/brief.ts +15 -25
  254. package/src/domain/analysis/context.ts +17 -10
  255. package/src/domain/analysis/dependencies.ts +77 -81
  256. package/src/domain/analysis/fn-impact.ts +36 -43
  257. package/src/domain/analysis/implementations.ts +11 -17
  258. package/src/domain/analysis/module-map.ts +58 -92
  259. package/src/domain/analysis/query-helpers.ts +18 -1
  260. package/src/domain/graph/builder/pipeline.ts +409 -99
  261. package/src/domain/graph/builder/stages/build-edges.ts +45 -19
  262. package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
  263. package/src/domain/graph/builder/stages/finalize.ts +2 -2
  264. package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
  265. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  266. package/src/domain/graph/cycles.ts +110 -23
  267. package/src/domain/graph/resolve.ts +1 -1
  268. package/src/domain/graph/watcher.ts +202 -96
  269. package/src/domain/parser.ts +143 -89
  270. package/src/domain/search/generator.ts +1 -1
  271. package/src/domain/search/models.ts +26 -7
  272. package/src/domain/search/search/hybrid.ts +69 -51
  273. package/src/extractors/go.ts +43 -33
  274. package/src/extractors/helpers.ts +37 -23
  275. package/src/extractors/java.ts +66 -47
  276. package/src/extractors/javascript.ts +66 -54
  277. package/src/extractors/kotlin.ts +84 -77
  278. package/src/extractors/python.ts +31 -25
  279. package/src/extractors/rust.ts +37 -29
  280. package/src/extractors/solidity.ts +57 -61
  281. package/src/extractors/swift.ts +81 -80
  282. package/src/extractors/zig.ts +58 -61
  283. package/src/features/ast.ts +130 -110
  284. package/src/features/audit.ts +8 -6
  285. package/src/features/branch-compare.ts +105 -79
  286. package/src/features/communities.ts +25 -10
  287. package/src/features/complexity.ts +171 -134
  288. package/src/features/dataflow.ts +165 -175
  289. package/src/features/flow.ts +129 -92
  290. package/src/features/structure-query.ts +79 -64
  291. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  292. package/src/graph/algorithms/leiden/partition.ts +359 -294
  293. package/src/graph/model.ts +6 -1
  294. package/src/infrastructure/config.ts +6 -4
  295. package/src/infrastructure/suppress.ts +47 -0
  296. package/src/mcp/server.ts +53 -37
  297. package/src/presentation/dataflow.ts +50 -44
  298. package/src/presentation/diff-impact-mermaid.ts +104 -62
  299. package/src/presentation/queries-cli/exports.ts +21 -13
  300. package/src/presentation/queries-cli/impact.ts +15 -13
  301. package/src/presentation/queries-cli/inspect.ts +100 -81
  302. package/src/presentation/queries-cli/overview.ts +26 -16
  303. package/src/presentation/queries-cli/path.ts +33 -25
  304. package/src/presentation/result-formatter.ts +19 -1
  305. package/src/presentation/viewer.ts +42 -14
  306. package/src/shared/errors.ts +6 -0
  307. package/src/shared/hierarchy.ts +50 -2
  308. package/src/shared/normalize.ts +31 -12
  309. package/src/shared/paginate.ts +0 -17
  310. package/src/types.ts +26 -5
@@ -1,9 +1,14 @@
1
- import { tarjan } from '../../graph/algorithms/tarjan.js';
2
- import { buildDependencyGraph } from '../../graph/builders/dependency.js';
3
- import { CodeGraph } from '../../graph/model.js';
1
+ import { getCallableNodes, getCallEdges, getFileNodesAll, getImportEdges } from '../../db/index.js';
4
2
  import { loadNative } from '../../infrastructure/native.js';
3
+ import { isTestFile } from '../../infrastructure/test-filter.js';
5
4
  import type { BetterSqlite3Database } from '../../types.js';
6
5
 
6
+ /**
7
+ * Find cycles using Tarjan's SCC algorithm.
8
+ *
9
+ * Builds a label-based adjacency list directly from DB rows — no intermediate
10
+ * CodeGraph construction. This is O(V + E) with minimal memory overhead.
11
+ */
7
12
  export function findCycles(
8
13
  db: BetterSqlite3Database,
9
14
  opts: { fileLevel?: boolean; noTests?: boolean } = {},
@@ -11,40 +16,122 @@ export function findCycles(
11
16
  const fileLevel = opts.fileLevel !== false;
12
17
  const noTests = opts.noTests || false;
13
18
 
14
- const graph = buildDependencyGraph(db, { fileLevel, noTests });
19
+ const edges: Array<{ source: string; target: string }> = [];
20
+ const seen = new Set<string>();
15
21
 
16
- const idToLabel = new Map<string, string>();
17
- for (const [id, attrs] of graph.nodes()) {
18
- if (fileLevel) {
19
- idToLabel.set(id, attrs.file as string);
20
- } else {
21
- idToLabel.set(id, `${attrs.label}|${attrs.file}`);
22
+ if (fileLevel) {
23
+ let nodes = getFileNodesAll(db);
24
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
25
+ const nodeIds = new Set<number>();
26
+ const idToFile = new Map<number, string>();
27
+ for (const n of nodes) {
28
+ nodeIds.add(n.id);
29
+ idToFile.set(n.id, n.file);
30
+ }
31
+ for (const e of getImportEdges(db)) {
32
+ if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
33
+ if (e.source_id === e.target_id) continue;
34
+ const src = idToFile.get(e.source_id)!;
35
+ const tgt = idToFile.get(e.target_id)!;
36
+ const key = `${src}\0${tgt}`;
37
+ if (seen.has(key)) continue;
38
+ seen.add(key);
39
+ edges.push({ source: src, target: tgt });
40
+ }
41
+ } else {
42
+ let nodes = getCallableNodes(db);
43
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
44
+ const nodeIds = new Set<number>();
45
+ const idToLabel = new Map<number, string>();
46
+ for (const n of nodes) {
47
+ nodeIds.add(n.id);
48
+ idToLabel.set(n.id, `${n.name}|${n.file}`);
49
+ }
50
+ for (const e of getCallEdges(db)) {
51
+ if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
52
+ if (e.source_id === e.target_id) continue;
53
+ const src = idToLabel.get(e.source_id)!;
54
+ const tgt = idToLabel.get(e.target_id)!;
55
+ const key = `${src}\0${tgt}`;
56
+ if (seen.has(key)) continue;
57
+ seen.add(key);
58
+ edges.push({ source: src, target: tgt });
22
59
  }
23
60
  }
24
61
 
25
- const edges = graph.toEdgeArray().map((e) => ({
26
- source: idToLabel.get(e.source) ?? e.source,
27
- target: idToLabel.get(e.target) ?? e.target,
28
- }));
29
-
30
62
  const native = loadNative();
31
63
  if (native) {
32
64
  return native.detectCycles(edges) as string[][];
33
65
  }
34
66
 
35
- const labelGraph = new CodeGraph();
36
- for (const { source, target } of edges) {
37
- labelGraph.addEdge(source, target);
38
- }
39
- return tarjan(labelGraph);
67
+ return tarjanFromEdges(edges);
40
68
  }
41
69
 
42
70
  export function findCyclesJS(edges: Array<{ source: string; target: string }>): string[][] {
43
- const graph = new CodeGraph();
71
+ return tarjanFromEdges(edges);
72
+ }
73
+
74
+ /**
75
+ * Run Tarjan's SCC on a flat edge list. Returns SCCs with length > 1 (cycles).
76
+ * Uses a simple adjacency-list Map instead of a full CodeGraph.
77
+ */
78
+ function tarjanFromEdges(edges: Array<{ source: string; target: string }>): string[][] {
79
+ const adj = new Map<string, string[]>();
80
+ const allNodes = new Set<string>();
44
81
  for (const { source, target } of edges) {
45
- graph.addEdge(source, target);
82
+ allNodes.add(source);
83
+ allNodes.add(target);
84
+ let list = adj.get(source);
85
+ if (!list) {
86
+ list = [];
87
+ adj.set(source, list);
88
+ }
89
+ list.push(target);
46
90
  }
47
- return tarjan(graph);
91
+
92
+ let index = 0;
93
+ const stack: string[] = [];
94
+ const onStack = new Set<string>();
95
+ const indices = new Map<string, number>();
96
+ const lowlinks = new Map<string, number>();
97
+ const sccs: string[][] = [];
98
+
99
+ function strongconnect(v: string): void {
100
+ indices.set(v, index);
101
+ lowlinks.set(v, index);
102
+ index++;
103
+ stack.push(v);
104
+ onStack.add(v);
105
+
106
+ const successors = adj.get(v);
107
+ if (successors) {
108
+ for (const w of successors) {
109
+ if (!indices.has(w)) {
110
+ strongconnect(w);
111
+ lowlinks.set(v, Math.min(lowlinks.get(v)!, lowlinks.get(w)!));
112
+ } else if (onStack.has(w)) {
113
+ lowlinks.set(v, Math.min(lowlinks.get(v)!, indices.get(w)!));
114
+ }
115
+ }
116
+ }
117
+
118
+ if (lowlinks.get(v) === indices.get(v)) {
119
+ const scc: string[] = [];
120
+ let w: string | undefined;
121
+ do {
122
+ w = stack.pop()!;
123
+ onStack.delete(w);
124
+ scc.push(w);
125
+ } while (w !== v);
126
+ if (scc.length > 1) sccs.push(scc);
127
+ }
128
+ }
129
+
130
+ for (const id of allNodes) {
131
+ if (!indices.has(id)) strongconnect(id);
132
+ }
133
+
134
+ return sccs;
48
135
  }
49
136
 
50
137
  export function formatCycles(cycles: string[][]): string {
@@ -565,7 +565,7 @@ export function resolveImportsBatch(
565
565
  // Native resolver's .js → .ts remap fails on unnormalized paths —
566
566
  // apply JS-side fallback (same fix as resolveImportPath).
567
567
  const resolved = remapJsToTs(normalized, rootDir);
568
- map.set(`${r.fromFile}|${r.importSource}`, resolved);
568
+ map.set(`${normalizePath(r.fromFile)}|${r.importSource}`, resolved);
569
569
  }
570
570
  return map;
571
571
  } catch (e) {
@@ -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) => ({