@optave/codegraph 3.7.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 (401) hide show
  1. package/README.md +28 -16
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +195 -30
  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/rules/javascript.d.ts.map +1 -1
  10. package/dist/ast-analysis/rules/javascript.js +0 -1
  11. package/dist/ast-analysis/rules/javascript.js.map +1 -1
  12. package/dist/ast-analysis/shared.d.ts.map +1 -1
  13. package/dist/ast-analysis/shared.js +24 -19
  14. package/dist/ast-analysis/shared.js.map +1 -1
  15. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitor-utils.js +55 -39
  17. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  18. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  19. package/dist/ast-analysis/visitor.js +91 -70
  20. package/dist/ast-analysis/visitor.js.map +1 -1
  21. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  22. package/dist/ast-analysis/visitors/ast-store-visitor.js +52 -129
  23. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  24. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  25. package/dist/ast-analysis/visitors/complexity-visitor.js +32 -39
  26. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  27. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  28. package/dist/ast-analysis/visitors/dataflow-visitor.js +57 -38
  29. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  30. package/dist/cli/commands/ast.js +2 -2
  31. package/dist/cli/commands/ast.js.map +1 -1
  32. package/dist/cli/commands/watch.d.ts.map +1 -1
  33. package/dist/cli/commands/watch.js +16 -2
  34. package/dist/cli/commands/watch.js.map +1 -1
  35. package/dist/db/connection.d.ts.map +1 -1
  36. package/dist/db/connection.js +29 -26
  37. package/dist/db/connection.js.map +1 -1
  38. package/dist/db/query-builder.d.ts.map +1 -1
  39. package/dist/db/query-builder.js +16 -5
  40. package/dist/db/query-builder.js.map +1 -1
  41. package/dist/db/repository/base.d.ts +10 -0
  42. package/dist/db/repository/base.d.ts.map +1 -1
  43. package/dist/db/repository/base.js +17 -0
  44. package/dist/db/repository/base.js.map +1 -1
  45. package/dist/db/repository/native-repository.d.ts +6 -1
  46. package/dist/db/repository/native-repository.d.ts.map +1 -1
  47. package/dist/db/repository/native-repository.js +77 -1
  48. package/dist/db/repository/native-repository.js.map +1 -1
  49. package/dist/db/repository/nodes.d.ts.map +1 -1
  50. package/dist/db/repository/nodes.js +8 -4
  51. package/dist/db/repository/nodes.js.map +1 -1
  52. package/dist/db/repository/sqlite-repository.d.ts +3 -0
  53. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  54. package/dist/db/repository/sqlite-repository.js +26 -0
  55. package/dist/db/repository/sqlite-repository.js.map +1 -1
  56. package/dist/domain/analysis/brief.d.ts.map +1 -1
  57. package/dist/domain/analysis/brief.js +13 -17
  58. package/dist/domain/analysis/brief.js.map +1 -1
  59. package/dist/domain/analysis/context.d.ts.map +1 -1
  60. package/dist/domain/analysis/context.js +14 -11
  61. package/dist/domain/analysis/context.js.map +1 -1
  62. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  63. package/dist/domain/analysis/dependencies.js +53 -52
  64. package/dist/domain/analysis/dependencies.js.map +1 -1
  65. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  66. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  67. package/dist/domain/analysis/fn-impact.js +33 -31
  68. package/dist/domain/analysis/fn-impact.js.map +1 -1
  69. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  70. package/dist/domain/analysis/implementations.js +11 -19
  71. package/dist/domain/analysis/implementations.js.map +1 -1
  72. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  73. package/dist/domain/analysis/module-map.js +55 -76
  74. package/dist/domain/analysis/module-map.js.map +1 -1
  75. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  76. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  77. package/dist/domain/analysis/query-helpers.js +15 -1
  78. package/dist/domain/analysis/query-helpers.js.map +1 -1
  79. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  80. package/dist/domain/graph/builder/pipeline.js +315 -43
  81. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  82. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  83. package/dist/domain/graph/builder/stages/build-edges.js +106 -1
  84. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  85. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  86. package/dist/domain/graph/builder/stages/collect-files.js +17 -5
  87. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  88. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  89. package/dist/domain/graph/builder/stages/detect-changes.js +99 -51
  90. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  91. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  92. package/dist/domain/graph/builder/stages/finalize.js +34 -7
  93. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  94. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  95. package/dist/domain/graph/builder/stages/insert-nodes.js +50 -26
  96. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  97. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  98. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  99. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  100. package/dist/domain/graph/cycles.d.ts +6 -0
  101. package/dist/domain/graph/cycles.d.ts.map +1 -1
  102. package/dist/domain/graph/cycles.js +114 -22
  103. package/dist/domain/graph/cycles.js.map +1 -1
  104. package/dist/domain/graph/resolve.js +1 -1
  105. package/dist/domain/graph/resolve.js.map +1 -1
  106. package/dist/domain/graph/watcher.d.ts +2 -0
  107. package/dist/domain/graph/watcher.d.ts.map +1 -1
  108. package/dist/domain/graph/watcher.js +170 -75
  109. package/dist/domain/graph/watcher.js.map +1 -1
  110. package/dist/domain/parser.d.ts +1 -6
  111. package/dist/domain/parser.d.ts.map +1 -1
  112. package/dist/domain/parser.js +101 -32
  113. package/dist/domain/parser.js.map +1 -1
  114. package/dist/domain/search/generator.js +1 -1
  115. package/dist/domain/search/generator.js.map +1 -1
  116. package/dist/domain/search/models.d.ts +4 -3
  117. package/dist/domain/search/models.d.ts.map +1 -1
  118. package/dist/domain/search/models.js +18 -5
  119. package/dist/domain/search/models.js.map +1 -1
  120. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  121. package/dist/domain/search/search/hybrid.js +29 -18
  122. package/dist/domain/search/search/hybrid.js.map +1 -1
  123. package/dist/extractors/clojure.d.ts +12 -0
  124. package/dist/extractors/clojure.d.ts.map +1 -0
  125. package/dist/extractors/clojure.js +245 -0
  126. package/dist/extractors/clojure.js.map +1 -0
  127. package/dist/extractors/cuda.d.ts +11 -0
  128. package/dist/extractors/cuda.d.ts.map +1 -0
  129. package/dist/extractors/cuda.js +302 -0
  130. package/dist/extractors/cuda.js.map +1 -0
  131. package/dist/extractors/erlang.d.ts +14 -0
  132. package/dist/extractors/erlang.d.ts.map +1 -0
  133. package/dist/extractors/erlang.js +239 -0
  134. package/dist/extractors/erlang.js.map +1 -0
  135. package/dist/extractors/fsharp.d.ts +13 -0
  136. package/dist/extractors/fsharp.d.ts.map +1 -0
  137. package/dist/extractors/fsharp.js +218 -0
  138. package/dist/extractors/fsharp.js.map +1 -0
  139. package/dist/extractors/gleam.d.ts +14 -0
  140. package/dist/extractors/gleam.d.ts.map +1 -0
  141. package/dist/extractors/gleam.js +229 -0
  142. package/dist/extractors/gleam.js.map +1 -0
  143. package/dist/extractors/go.js +36 -33
  144. package/dist/extractors/go.js.map +1 -1
  145. package/dist/extractors/groovy.d.ts +10 -0
  146. package/dist/extractors/groovy.d.ts.map +1 -0
  147. package/dist/extractors/groovy.js +304 -0
  148. package/dist/extractors/groovy.js.map +1 -0
  149. package/dist/extractors/helpers.d.ts.map +1 -1
  150. package/dist/extractors/helpers.js +40 -29
  151. package/dist/extractors/helpers.js.map +1 -1
  152. package/dist/extractors/index.d.ts +11 -0
  153. package/dist/extractors/index.d.ts.map +1 -1
  154. package/dist/extractors/index.js +11 -0
  155. package/dist/extractors/index.js.map +1 -1
  156. package/dist/extractors/java.js +58 -46
  157. package/dist/extractors/java.js.map +1 -1
  158. package/dist/extractors/javascript.js +46 -45
  159. package/dist/extractors/javascript.js.map +1 -1
  160. package/dist/extractors/julia.d.ts +16 -0
  161. package/dist/extractors/julia.d.ts.map +1 -0
  162. package/dist/extractors/julia.js +287 -0
  163. package/dist/extractors/julia.js.map +1 -0
  164. package/dist/extractors/kotlin.js +84 -78
  165. package/dist/extractors/kotlin.js.map +1 -1
  166. package/dist/extractors/objc.d.ts +9 -0
  167. package/dist/extractors/objc.d.ts.map +1 -0
  168. package/dist/extractors/objc.js +406 -0
  169. package/dist/extractors/objc.js.map +1 -0
  170. package/dist/extractors/ocaml.js +74 -0
  171. package/dist/extractors/ocaml.js.map +1 -1
  172. package/dist/extractors/python.js +29 -24
  173. package/dist/extractors/python.js.map +1 -1
  174. package/dist/extractors/r.d.ts +13 -0
  175. package/dist/extractors/r.d.ts.map +1 -0
  176. package/dist/extractors/r.js +251 -0
  177. package/dist/extractors/r.js.map +1 -0
  178. package/dist/extractors/rust.js +41 -32
  179. package/dist/extractors/rust.js.map +1 -1
  180. package/dist/extractors/solidity.d.ts +9 -0
  181. package/dist/extractors/solidity.d.ts.map +1 -0
  182. package/dist/extractors/solidity.js +365 -0
  183. package/dist/extractors/solidity.js.map +1 -0
  184. package/dist/extractors/swift.js +83 -81
  185. package/dist/extractors/swift.js.map +1 -1
  186. package/dist/extractors/verilog.d.ts +9 -0
  187. package/dist/extractors/verilog.d.ts.map +1 -0
  188. package/dist/extractors/verilog.js +286 -0
  189. package/dist/extractors/verilog.js.map +1 -0
  190. package/dist/extractors/zig.js +58 -60
  191. package/dist/extractors/zig.js.map +1 -1
  192. package/dist/features/ast.d.ts +16 -14
  193. package/dist/features/ast.d.ts.map +1 -1
  194. package/dist/features/ast.js +84 -83
  195. package/dist/features/ast.js.map +1 -1
  196. package/dist/features/audit.d.ts.map +1 -1
  197. package/dist/features/audit.js +8 -6
  198. package/dist/features/audit.js.map +1 -1
  199. package/dist/features/branch-compare.d.ts.map +1 -1
  200. package/dist/features/branch-compare.js +69 -72
  201. package/dist/features/branch-compare.js.map +1 -1
  202. package/dist/features/communities.d.ts.map +1 -1
  203. package/dist/features/communities.js +19 -7
  204. package/dist/features/communities.js.map +1 -1
  205. package/dist/features/complexity.d.ts.map +1 -1
  206. package/dist/features/complexity.js +120 -125
  207. package/dist/features/complexity.js.map +1 -1
  208. package/dist/features/dataflow.d.ts.map +1 -1
  209. package/dist/features/dataflow.js +136 -137
  210. package/dist/features/dataflow.js.map +1 -1
  211. package/dist/features/flow.d.ts.map +1 -1
  212. package/dist/features/flow.js +84 -79
  213. package/dist/features/flow.js.map +1 -1
  214. package/dist/features/structure-query.d.ts.map +1 -1
  215. package/dist/features/structure-query.js +69 -65
  216. package/dist/features/structure-query.js.map +1 -1
  217. package/dist/graph/algorithms/bfs.d.ts +2 -0
  218. package/dist/graph/algorithms/bfs.d.ts.map +1 -1
  219. package/dist/graph/algorithms/bfs.js +27 -0
  220. package/dist/graph/algorithms/bfs.js.map +1 -1
  221. package/dist/graph/algorithms/centrality.d.ts +2 -0
  222. package/dist/graph/algorithms/centrality.d.ts.map +1 -1
  223. package/dist/graph/algorithms/centrality.js +28 -0
  224. package/dist/graph/algorithms/centrality.js.map +1 -1
  225. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  226. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  227. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  228. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  229. package/dist/graph/algorithms/leiden/partition.js +288 -266
  230. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  231. package/dist/graph/algorithms/louvain.d.ts +3 -4
  232. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  233. package/dist/graph/algorithms/louvain.js +29 -0
  234. package/dist/graph/algorithms/louvain.js.map +1 -1
  235. package/dist/graph/algorithms/shortest-path.d.ts +2 -0
  236. package/dist/graph/algorithms/shortest-path.d.ts.map +1 -1
  237. package/dist/graph/algorithms/shortest-path.js +18 -1
  238. package/dist/graph/algorithms/shortest-path.js.map +1 -1
  239. package/dist/graph/model.d.ts.map +1 -1
  240. package/dist/graph/model.js +5 -1
  241. package/dist/graph/model.js.map +1 -1
  242. package/dist/infrastructure/config.d.ts.map +1 -1
  243. package/dist/infrastructure/config.js +6 -4
  244. package/dist/infrastructure/config.js.map +1 -1
  245. package/dist/infrastructure/suppress.d.ts +25 -0
  246. package/dist/infrastructure/suppress.d.ts.map +1 -0
  247. package/dist/infrastructure/suppress.js +43 -0
  248. package/dist/infrastructure/suppress.js.map +1 -0
  249. package/dist/mcp/server.d.ts.map +1 -1
  250. package/dist/mcp/server.js +29 -24
  251. package/dist/mcp/server.js.map +1 -1
  252. package/dist/presentation/dataflow.d.ts.map +1 -1
  253. package/dist/presentation/dataflow.js +47 -38
  254. package/dist/presentation/dataflow.js.map +1 -1
  255. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  256. package/dist/presentation/diff-impact-mermaid.js +60 -51
  257. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  258. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  259. package/dist/presentation/queries-cli/exports.js +20 -14
  260. package/dist/presentation/queries-cli/exports.js.map +1 -1
  261. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  262. package/dist/presentation/queries-cli/impact.js +15 -13
  263. package/dist/presentation/queries-cli/impact.js.map +1 -1
  264. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  265. package/dist/presentation/queries-cli/inspect.js +101 -79
  266. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  267. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  268. package/dist/presentation/queries-cli/overview.js +25 -16
  269. package/dist/presentation/queries-cli/overview.js.map +1 -1
  270. package/dist/presentation/queries-cli/path.js +26 -20
  271. package/dist/presentation/queries-cli/path.js.map +1 -1
  272. package/dist/presentation/result-formatter.d.ts +10 -0
  273. package/dist/presentation/result-formatter.d.ts.map +1 -1
  274. package/dist/presentation/result-formatter.js +16 -1
  275. package/dist/presentation/result-formatter.js.map +1 -1
  276. package/dist/presentation/viewer.d.ts.map +1 -1
  277. package/dist/presentation/viewer.js +18 -12
  278. package/dist/presentation/viewer.js.map +1 -1
  279. package/dist/shared/errors.d.ts +5 -0
  280. package/dist/shared/errors.d.ts.map +1 -1
  281. package/dist/shared/errors.js +5 -0
  282. package/dist/shared/errors.js.map +1 -1
  283. package/dist/shared/hierarchy.d.ts +8 -2
  284. package/dist/shared/hierarchy.d.ts.map +1 -1
  285. package/dist/shared/hierarchy.js +42 -1
  286. package/dist/shared/hierarchy.js.map +1 -1
  287. package/dist/shared/normalize.d.ts +6 -1
  288. package/dist/shared/normalize.d.ts.map +1 -1
  289. package/dist/shared/normalize.js +20 -12
  290. package/dist/shared/normalize.js.map +1 -1
  291. package/dist/shared/paginate.d.ts +0 -9
  292. package/dist/shared/paginate.d.ts.map +1 -1
  293. package/dist/shared/paginate.js +0 -15
  294. package/dist/shared/paginate.js.map +1 -1
  295. package/dist/types.d.ts +129 -3
  296. package/dist/types.d.ts.map +1 -1
  297. package/grammars/tree-sitter-clojure.wasm +0 -0
  298. package/grammars/tree-sitter-cuda.wasm +0 -0
  299. package/grammars/tree-sitter-erlang.wasm +0 -0
  300. package/grammars/tree-sitter-fsharp.wasm +0 -0
  301. package/grammars/tree-sitter-gleam.wasm +0 -0
  302. package/grammars/tree-sitter-groovy.wasm +0 -0
  303. package/grammars/tree-sitter-julia.wasm +0 -0
  304. package/grammars/tree-sitter-objc.wasm +0 -0
  305. package/grammars/tree-sitter-ocaml_interface.wasm +0 -0
  306. package/grammars/tree-sitter-r.wasm +0 -0
  307. package/grammars/tree-sitter-solidity.wasm +0 -0
  308. package/grammars/tree-sitter-verilog.wasm +0 -0
  309. package/package.json +18 -7
  310. package/src/ast-analysis/engine.ts +245 -42
  311. package/src/ast-analysis/metrics.ts +33 -11
  312. package/src/ast-analysis/rules/javascript.ts +0 -1
  313. package/src/ast-analysis/shared.ts +33 -24
  314. package/src/ast-analysis/visitor-utils.ts +52 -32
  315. package/src/ast-analysis/visitor.ts +132 -71
  316. package/src/ast-analysis/visitors/ast-store-visitor.ts +49 -119
  317. package/src/ast-analysis/visitors/complexity-visitor.ts +35 -40
  318. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  319. package/src/cli/commands/ast.ts +2 -2
  320. package/src/cli/commands/watch.ts +16 -2
  321. package/src/db/connection.ts +29 -28
  322. package/src/db/query-builder.ts +15 -3
  323. package/src/db/repository/base.ts +20 -0
  324. package/src/db/repository/native-repository.ts +79 -1
  325. package/src/db/repository/nodes.ts +13 -8
  326. package/src/db/repository/sqlite-repository.ts +29 -0
  327. package/src/domain/analysis/brief.ts +15 -25
  328. package/src/domain/analysis/context.ts +17 -10
  329. package/src/domain/analysis/dependencies.ts +67 -76
  330. package/src/domain/analysis/fn-impact.ts +36 -43
  331. package/src/domain/analysis/implementations.ts +11 -17
  332. package/src/domain/analysis/module-map.ts +58 -92
  333. package/src/domain/analysis/query-helpers.ts +18 -1
  334. package/src/domain/graph/builder/pipeline.ts +366 -41
  335. package/src/domain/graph/builder/stages/build-edges.ts +162 -1
  336. package/src/domain/graph/builder/stages/collect-files.ts +18 -7
  337. package/src/domain/graph/builder/stages/detect-changes.ts +110 -56
  338. package/src/domain/graph/builder/stages/finalize.ts +41 -11
  339. package/src/domain/graph/builder/stages/insert-nodes.ts +75 -39
  340. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  341. package/src/domain/graph/cycles.ts +110 -23
  342. package/src/domain/graph/resolve.ts +1 -1
  343. package/src/domain/graph/watcher.ts +202 -96
  344. package/src/domain/parser.ts +122 -28
  345. package/src/domain/search/generator.ts +1 -1
  346. package/src/domain/search/models.ts +17 -4
  347. package/src/domain/search/search/hybrid.ts +69 -51
  348. package/src/extractors/clojure.ts +273 -0
  349. package/src/extractors/cuda.ts +316 -0
  350. package/src/extractors/erlang.ts +252 -0
  351. package/src/extractors/fsharp.ts +253 -0
  352. package/src/extractors/gleam.ts +246 -0
  353. package/src/extractors/go.ts +43 -33
  354. package/src/extractors/groovy.ts +332 -0
  355. package/src/extractors/helpers.ts +37 -23
  356. package/src/extractors/index.ts +11 -0
  357. package/src/extractors/java.ts +66 -47
  358. package/src/extractors/javascript.ts +45 -46
  359. package/src/extractors/julia.ts +318 -0
  360. package/src/extractors/kotlin.ts +84 -77
  361. package/src/extractors/objc.ts +431 -0
  362. package/src/extractors/ocaml.ts +78 -0
  363. package/src/extractors/python.ts +31 -25
  364. package/src/extractors/r.ts +253 -0
  365. package/src/extractors/rust.ts +37 -29
  366. package/src/extractors/solidity.ts +394 -0
  367. package/src/extractors/swift.ts +81 -80
  368. package/src/extractors/verilog.ts +315 -0
  369. package/src/extractors/zig.ts +58 -61
  370. package/src/features/ast.ts +131 -112
  371. package/src/features/audit.ts +8 -6
  372. package/src/features/branch-compare.ts +105 -79
  373. package/src/features/communities.ts +25 -10
  374. package/src/features/complexity.ts +171 -134
  375. package/src/features/dataflow.ts +165 -175
  376. package/src/features/flow.ts +129 -92
  377. package/src/features/structure-query.ts +79 -64
  378. package/src/graph/algorithms/bfs.ts +34 -0
  379. package/src/graph/algorithms/centrality.ts +30 -0
  380. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  381. package/src/graph/algorithms/leiden/partition.ts +359 -294
  382. package/src/graph/algorithms/louvain.ts +31 -4
  383. package/src/graph/algorithms/shortest-path.ts +20 -1
  384. package/src/graph/model.ts +6 -1
  385. package/src/infrastructure/config.ts +6 -4
  386. package/src/infrastructure/suppress.ts +47 -0
  387. package/src/mcp/server.ts +53 -37
  388. package/src/presentation/dataflow.ts +50 -44
  389. package/src/presentation/diff-impact-mermaid.ts +104 -62
  390. package/src/presentation/queries-cli/exports.ts +21 -13
  391. package/src/presentation/queries-cli/impact.ts +15 -13
  392. package/src/presentation/queries-cli/inspect.ts +100 -81
  393. package/src/presentation/queries-cli/overview.ts +26 -16
  394. package/src/presentation/queries-cli/path.ts +33 -25
  395. package/src/presentation/result-formatter.ts +19 -1
  396. package/src/presentation/viewer.ts +42 -14
  397. package/src/shared/errors.ts +6 -0
  398. package/src/shared/hierarchy.ts +50 -2
  399. package/src/shared/normalize.ts +31 -12
  400. package/src/shared/paginate.ts +0 -17
  401. package/src/types.ts +138 -3
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Community detection via vendored Leiden algorithm.
2
+ * Community detection via native Rust Louvain or vendored Leiden algorithm.
3
3
  * Maintains backward-compatible API: { assignments: Map<string, number>, modularity: number }
4
4
  *
5
- * Note: Always runs in undirected mode (`directed: false`) regardless of
6
- * the input graph's directedness. For direction-aware community detection,
7
- * use `detectClusters` from `./leiden/index.js` directly.
5
+ * Native path: classic Louvain (Rust, undirected modularity optimization).
6
+ * JS fallback: Leiden algorithm via `detectClusters` (always undirected, `directed: false`).
8
7
  */
8
+
9
+ import { warn } from '../../infrastructure/logger.js';
10
+ import { loadNative } from '../../infrastructure/native.js';
9
11
  import type { CodeGraph } from '../model.js';
10
12
  import type { DetectClustersResult } from './leiden/index.js';
11
13
  import { detectClusters } from './leiden/index.js';
@@ -28,6 +30,31 @@ export function louvainCommunities(graph: CodeGraph, opts: LouvainOptions = {}):
28
30
  }
29
31
 
30
32
  const resolution: number = opts.resolution ?? 1.0;
33
+
34
+ const native = loadNative();
35
+ if (native?.louvainCommunities) {
36
+ // maxLevels, maxLocalPasses, and refinementTheta are Leiden-specific tuning knobs
37
+ // not supported by the Rust Louvain implementation. Warn callers who set them.
38
+ if (opts.maxLevels != null || opts.maxLocalPasses != null || opts.refinementTheta != null) {
39
+ warn(
40
+ 'louvainCommunities: maxLevels/maxLocalPasses/refinementTheta are ignored by the native Rust path',
41
+ );
42
+ }
43
+ const edges = graph.toEdgeArray();
44
+ const nodeIds = graph.nodeIds();
45
+ const result = native.louvainCommunities(edges, nodeIds, resolution, 42);
46
+ const assignments = new Map<string, number>();
47
+ for (const entry of result.assignments) {
48
+ assignments.set(entry.node, entry.community);
49
+ }
50
+ return { assignments, modularity: result.modularity };
51
+ }
52
+
53
+ return louvainJS(graph, opts, resolution);
54
+ }
55
+
56
+ /** JS fallback using the vendored Leiden algorithm. */
57
+ function louvainJS(graph: CodeGraph, opts: LouvainOptions, resolution: number): LouvainResult {
31
58
  const result: DetectClustersResult = detectClusters(graph, {
32
59
  resolution,
33
60
  randomSeed: 42,
@@ -1,8 +1,11 @@
1
+ import { loadNative } from '../../infrastructure/native.js';
1
2
  import type { CodeGraph } from '../model.js';
2
3
 
3
4
  /**
4
5
  * BFS-based shortest path on a CodeGraph.
5
6
  *
7
+ * Tries the native Rust implementation first, falls back to JS.
8
+ *
6
9
  * @returns Path from fromId to toId (inclusive), or null if unreachable
7
10
  */
8
11
  export function shortestPath(graph: CodeGraph, fromId: string, toId: string): string[] | null {
@@ -12,6 +15,23 @@ export function shortestPath(graph: CodeGraph, fromId: string, toId: string): st
12
15
  if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
13
16
  if (from === to) return [from];
14
17
 
18
+ const native = loadNative();
19
+ if (native?.shortestPath) {
20
+ let edges = graph.toEdgeArray();
21
+ if (!graph.directed) {
22
+ // Undirected: toEdgeArray() deduplicates to one canonical direction;
23
+ // mirror each edge so the Rust BFS can traverse in both directions.
24
+ edges = [...edges, ...edges.map((e) => ({ source: e.target, target: e.source }))];
25
+ }
26
+ const result = native.shortestPath(edges, from, to);
27
+ return result.length > 0 ? result : null;
28
+ }
29
+
30
+ return shortestPathJS(graph, from, to);
31
+ }
32
+
33
+ /** Pure JS fallback for shortest path. */
34
+ function shortestPathJS(graph: CodeGraph, from: string, to: string): string[] | null {
15
35
  const parent = new Map<string, string | null>();
16
36
  parent.set(from, null);
17
37
  const queue = [from];
@@ -23,7 +43,6 @@ export function shortestPath(graph: CodeGraph, fromId: string, toId: string): st
23
43
  if (parent.has(neighbor)) continue;
24
44
  parent.set(neighbor, current);
25
45
  if (neighbor === to) {
26
- // Reconstruct path
27
46
  const path: string[] = [];
28
47
  let node: string | null = to;
29
48
  while (node !== null) {
@@ -17,6 +17,11 @@ export interface CodeGraphOpts {
17
17
  directed?: boolean;
18
18
  }
19
19
 
20
+ /** Canonical key for an undirected edge — smallest ID first, null-byte separated. */
21
+ function undirectedEdgeKey(a: string, b: string): string {
22
+ return a < b ? `${a}\0${b}` : `${b}\0${a}`;
23
+ }
24
+
20
25
  export class CodeGraph {
21
26
  private _directed: boolean;
22
27
  private _nodes: Map<string, NodeAttrs>;
@@ -121,7 +126,7 @@ export class CodeGraph {
121
126
  const seen = new Set<string>();
122
127
  for (const [src, targets] of this._successors) {
123
128
  for (const [tgt, attrs] of targets) {
124
- const key = src < tgt ? `${src}\0${tgt}` : `${tgt}\0${src}`;
129
+ const key = undirectedEdgeKey(src, tgt);
125
130
  if (seen.has(key)) continue;
126
131
  seen.add(key);
127
132
  yield [src, tgt, attrs];
@@ -178,7 +178,7 @@ export function loadConfig(cwd?: string): CodegraphConfig {
178
178
  _configCache.set(cwd, structuredClone(result));
179
179
  return result;
180
180
  } catch (err: unknown) {
181
- debug(`Failed to parse config ${filePath}: ${(err as Error).message}`);
181
+ debug(`Failed to parse config ${filePath}: ${toErrorMessage(err)}`);
182
182
  }
183
183
  }
184
184
  }
@@ -229,7 +229,7 @@ export function resolveSecrets(config: CodegraphConfig): CodegraphConfig {
229
229
  (config.llm as Record<string, unknown>).apiKey = result;
230
230
  }
231
231
  } catch (err: unknown) {
232
- warn(`apiKeyCommand failed: ${(err as Error).message}`);
232
+ warn(`apiKeyCommand failed: ${toErrorMessage(err)}`);
233
233
  }
234
234
  return config;
235
235
  }
@@ -252,7 +252,8 @@ function expandWorkspaceGlob(pattern: string, rootDir: string): string[] {
252
252
  .filter((e) => e.isDirectory())
253
253
  .map((e) => path.join(baseDir, e.name))
254
254
  .filter((d) => fs.existsSync(path.join(d, 'package.json')));
255
- } catch {
255
+ } catch (e) {
256
+ debug(`expandGlobDirs: failed to read ${baseDir}: ${toErrorMessage(e)}`);
256
257
  return [];
257
258
  }
258
259
  }
@@ -265,7 +266,8 @@ function readPackageName(pkgDir: string): string | null {
265
266
  const raw = fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf-8');
266
267
  const pkg = JSON.parse(raw);
267
268
  return pkg.name || null;
268
- } catch {
269
+ } catch (e) {
270
+ debug(`readPackageName: failed for ${pkgDir}: ${toErrorMessage(e)}`);
269
271
  return null;
270
272
  }
271
273
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Catch-suppression helpers for intentional error swallowing.
3
+ *
4
+ * Lives in `infrastructure/` (not `shared/`) because it depends on the
5
+ * structured logger — keeping `shared/errors.ts` dependency-free.
6
+ */
7
+
8
+ import { toErrorMessage } from '../shared/errors.js';
9
+ import { debug } from './logger.js';
10
+
11
+ /**
12
+ * Run `fn` and return its result. If it throws, log a debug message and
13
+ * return `fallback` instead. Use this for intentional catch suppression
14
+ * where the error is expected and non-fatal (e.g. optional file reads,
15
+ * graceful feature probes, cleanup that may fail).
16
+ *
17
+ * @example
18
+ * const version = suppressError(() => readPkgVersion(), 'read package version', '');
19
+ */
20
+ export function suppressError<T>(fn: () => T, context: string, fallback: T): T {
21
+ try {
22
+ return fn();
23
+ } catch (e: unknown) {
24
+ debug(`${context}: ${toErrorMessage(e)}`);
25
+ return fallback;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Async variant of {@link suppressError}. Awaits `fn()` and returns `fallback`
31
+ * on rejection, logging the error via `debug()`.
32
+ *
33
+ * @example
34
+ * const data = await suppressErrorAsync(() => fetchOptionalData(), 'fetch data', null);
35
+ */
36
+ export async function suppressErrorAsync<T>(
37
+ fn: () => Promise<T>,
38
+ context: string,
39
+ fallback: T,
40
+ ): Promise<T> {
41
+ try {
42
+ return await fn();
43
+ } catch (e: unknown) {
44
+ debug(`${context}: ${toErrorMessage(e)}`);
45
+ return fallback;
46
+ }
47
+ }
package/src/mcp/server.ts CHANGED
@@ -13,7 +13,8 @@ const { version: PKG_VERSION } = require('../../package.json') as { version: str
13
13
  import { getDatabase } from '../db/better-sqlite3.js';
14
14
  import { findDbPath } from '../db/index.js';
15
15
  import { loadConfig } from '../infrastructure/config.js';
16
- import { CodegraphError, ConfigError } from '../shared/errors.js';
16
+ import { debug } from '../infrastructure/logger.js';
17
+ import { CodegraphError, ConfigError, toErrorMessage } from '../shared/errors.js';
17
18
  import { MCP_MAX_LIMIT } from '../shared/paginate.js';
18
19
  import type { CodegraphConfig, MCPServerOptions } from '../types.js';
19
20
  import { initMcpDefaults } from './middleware.js';
@@ -56,7 +57,8 @@ async function loadMCPSdk(): Promise<{
56
57
  ListToolsRequestSchema: types.ListToolsRequestSchema,
57
58
  CallToolRequestSchema: types.CallToolRequestSchema,
58
59
  };
59
- } catch {
60
+ } catch (e) {
61
+ debug(`MCP SDK import failed: ${toErrorMessage(e)}`);
60
62
  throw new ConfigError(
61
63
  'MCP server requires @modelcontextprotocol/sdk.\nInstall it with: npm install @modelcontextprotocol/sdk',
62
64
  );
@@ -123,8 +125,10 @@ function registerShutdownHandlers(): void {
123
125
  const shutdown = async () => {
124
126
  try {
125
127
  await _activeServer?.close();
126
- } catch (_shutdownErr: unknown) {
127
- // Ignore close errors during shutdown — the transport may already be gone.
128
+ } catch (shutdownErr: unknown) {
129
+ debug(
130
+ `MCP shutdown close failed (transport may already be gone): ${toErrorMessage(shutdownErr)}`,
131
+ );
128
132
  }
129
133
  process.exit(0);
130
134
  };
@@ -154,36 +158,13 @@ function registerShutdownHandlers(): void {
154
158
  process.on('unhandledRejection', silentReject);
155
159
  }
156
160
 
157
- export async function startMCPServer(
158
- customDbPath?: string,
159
- options: MCPServerOptionsInternal = {},
160
- ): Promise<void> {
161
- const { allowedRepos } = options;
162
- const multiRepo = options.multiRepo || !!allowedRepos;
163
-
164
- // Apply config-based MCP page-size overrides
165
- const config = options.config || loadConfig();
166
- initMcpDefaults(config.mcp?.defaults ? { ...config.mcp.defaults } : undefined);
167
-
168
- const { Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema } =
169
- await loadMCPSdk();
170
-
171
- // Connect transport FIRST so the server can receive the client's
172
- // `initialize` request while heavy modules (queries, better-sqlite3)
173
- // are still loading. These are lazy-loaded on the first tool call
174
- // and cached for subsequent calls.
175
- const { getQueries } = createLazyLoaders();
176
-
177
- const server = new (Server as any)(
178
- { name: 'codegraph', version: PKG_VERSION },
179
- { capabilities: { tools: {} } },
180
- );
181
-
182
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
183
- tools: buildToolList(multiRepo),
184
- }));
185
-
186
- server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
161
+ function createCallToolHandler(
162
+ multiRepo: boolean,
163
+ customDbPath: string | undefined,
164
+ allowedRepos: string[] | undefined,
165
+ getQueries: () => Promise<unknown>,
166
+ ) {
167
+ return async (request: any) => {
187
168
  const { name, arguments: args } = request.params;
188
169
  try {
189
170
  validateMultiRepoAccess(multiRepo, name, args);
@@ -212,10 +193,45 @@ export async function startMCPServer(
212
193
  const text =
213
194
  err instanceof CodegraphError
214
195
  ? `[${code}] ${err.message}`
215
- : `Error: ${(err as Error).message}`;
196
+ : `Error: ${toErrorMessage(err)}`;
216
197
  return { content: [{ type: 'text', text }], isError: true };
217
198
  }
218
- });
199
+ };
200
+ }
201
+
202
+ export async function startMCPServer(
203
+ customDbPath?: string,
204
+ options: MCPServerOptionsInternal = {},
205
+ ): Promise<void> {
206
+ const { allowedRepos } = options;
207
+ const multiRepo = options.multiRepo || !!allowedRepos;
208
+
209
+ // Apply config-based MCP page-size overrides
210
+ const config = options.config || loadConfig();
211
+ initMcpDefaults(config.mcp?.defaults ? { ...config.mcp.defaults } : undefined);
212
+
213
+ const { Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema } =
214
+ await loadMCPSdk();
215
+
216
+ // Connect transport FIRST so the server can receive the client's
217
+ // `initialize` request while heavy modules (queries, better-sqlite3)
218
+ // are still loading. These are lazy-loaded on the first tool call
219
+ // and cached for subsequent calls.
220
+ const { getQueries } = createLazyLoaders();
221
+
222
+ const server = new (Server as any)(
223
+ { name: 'codegraph', version: PKG_VERSION },
224
+ { capabilities: { tools: {} } },
225
+ );
226
+
227
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
228
+ tools: buildToolList(multiRepo),
229
+ }));
230
+
231
+ server.setRequestHandler(
232
+ CallToolRequestSchema,
233
+ createCallToolHandler(multiRepo, customDbPath, allowedRepos, getQueries),
234
+ );
219
235
 
220
236
  const transport = new (StdioServerTransport as any)();
221
237
 
@@ -235,7 +251,7 @@ export async function startMCPServer(
235
251
  process.exit(0);
236
252
  }
237
253
  process.stderr.write(
238
- `MCP transport connect failed: ${(err as Error).stack ?? (err as Error).message}\n`,
254
+ `MCP transport connect failed: ${err instanceof Error ? (err.stack ?? err.message) : toErrorMessage(err)}\n`,
239
255
  );
240
256
  process.exit(1);
241
257
  }
@@ -58,6 +58,53 @@ interface DataflowImpactEntry {
58
58
  levels: Record<string, Array<{ name: string; file: string; line: number }>>;
59
59
  }
60
60
 
61
+ function printDataflowFlows(r: DataflowResultEntry): void {
62
+ if (r.flowsTo.length > 0) {
63
+ console.log('\n Data flows TO:');
64
+ for (const f of r.flowsTo) {
65
+ const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
66
+ console.log(` \u2192 ${f.target} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
67
+ }
68
+ }
69
+ if (r.flowsFrom.length > 0) {
70
+ console.log('\n Data flows FROM:');
71
+ for (const f of r.flowsFrom) {
72
+ const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
73
+ console.log(` \u2190 ${f.source} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
74
+ }
75
+ }
76
+ }
77
+
78
+ function printDataflowReturns(r: DataflowResultEntry): void {
79
+ if (r.returns.length > 0) {
80
+ console.log('\n Return value consumed by:');
81
+ for (const c of r.returns) {
82
+ console.log(` \u2192 ${c.consumer} (${c.file}:${c.line}) ${c.expression}`);
83
+ }
84
+ }
85
+ if (r.returnedBy.length > 0) {
86
+ console.log('\n Uses return value of:');
87
+ for (const p of r.returnedBy) {
88
+ console.log(` \u2190 ${p.producer} (${p.file}:${p.line}) ${p.expression}`);
89
+ }
90
+ }
91
+ }
92
+
93
+ function printDataflowMutations(r: DataflowResultEntry): void {
94
+ if (r.mutates.length > 0) {
95
+ console.log('\n Mutates:');
96
+ for (const m of r.mutates) {
97
+ console.log(` \u270E ${m.expression} (line ${m.line})`);
98
+ }
99
+ }
100
+ if (r.mutatedBy.length > 0) {
101
+ console.log('\n Mutated by:');
102
+ for (const m of r.mutatedBy) {
103
+ console.log(` \u270E ${m.source} \u2014 ${m.expression} (line ${m.line})`);
104
+ }
105
+ }
106
+ }
107
+
61
108
  export function dataflow(
62
109
  name: string,
63
110
  customDbPath: string | undefined,
@@ -87,50 +134,9 @@ export function dataflow(
87
134
  for (const r of data.results) {
88
135
  console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
89
136
  console.log('\u2500'.repeat(60));
90
-
91
- if (r.flowsTo.length > 0) {
92
- console.log('\n Data flows TO:');
93
- for (const f of r.flowsTo) {
94
- const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
95
- console.log(` \u2192 ${f.target} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
96
- }
97
- }
98
-
99
- if (r.flowsFrom.length > 0) {
100
- console.log('\n Data flows FROM:');
101
- for (const f of r.flowsFrom) {
102
- const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
103
- console.log(` \u2190 ${f.source} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
104
- }
105
- }
106
-
107
- if (r.returns.length > 0) {
108
- console.log('\n Return value consumed by:');
109
- for (const c of r.returns) {
110
- console.log(` \u2192 ${c.consumer} (${c.file}:${c.line}) ${c.expression}`);
111
- }
112
- }
113
-
114
- if (r.returnedBy.length > 0) {
115
- console.log('\n Uses return value of:');
116
- for (const p of r.returnedBy) {
117
- console.log(` \u2190 ${p.producer} (${p.file}:${p.line}) ${p.expression}`);
118
- }
119
- }
120
-
121
- if (r.mutates.length > 0) {
122
- console.log('\n Mutates:');
123
- for (const m of r.mutates) {
124
- console.log(` \u270E ${m.expression} (line ${m.line})`);
125
- }
126
- }
127
-
128
- if (r.mutatedBy.length > 0) {
129
- console.log('\n Mutated by:');
130
- for (const m of r.mutatedBy) {
131
- console.log(` \u270E ${m.source} \u2014 ${m.expression} (line ${m.line})`);
132
- }
133
- }
137
+ printDataflowFlows(r);
138
+ printDataflowReturns(r);
139
+ printDataflowMutations(r);
134
140
  }
135
141
  }
136
142
 
@@ -1,56 +1,48 @@
1
1
  import { diffImpactData } from '../domain/analysis/diff-impact.js';
2
2
 
3
- export function diffImpactMermaid(
4
- customDbPath: string,
5
- opts: {
6
- noTests?: boolean;
7
- depth?: number;
8
- staged?: boolean;
9
- ref?: string;
10
- includeImplementors?: boolean;
11
- limit?: number;
12
- offset?: number;
13
- config?: any;
14
- } = {},
15
- ): string {
16
- const data: any = diffImpactData(customDbPath, opts);
17
- if ('error' in data) return data.error as string;
18
- if (data.changedFiles === 0 || data.affectedFunctions.length === 0) {
19
- return 'flowchart TB\n none["No impacted functions detected"]';
20
- }
3
+ interface MermaidNodeRegistry {
4
+ nodeIdMap: Map<string, string>;
5
+ nodeLabels: Map<string, string>;
6
+ counter: number;
7
+ }
21
8
 
22
- const newFileSet = new Set(data.newFiles || []);
23
- const lines = ['flowchart TB'];
9
+ interface ImpactEdgeSets {
10
+ allEdges: Set<string>;
11
+ edgeFromNodes: Set<string>;
12
+ edgeToNodes: Set<string>;
13
+ changedKeys: Set<string>;
14
+ }
24
15
 
25
- // Assign stable Mermaid node IDs
26
- let nodeCounter = 0;
27
- const nodeIdMap = new Map<string, string>();
28
- const nodeLabels = new Map<string, string>();
29
- function nodeId(key: string, label?: string): string {
30
- if (!nodeIdMap.has(key)) {
31
- nodeIdMap.set(key, `n${nodeCounter++}`);
32
- if (label) nodeLabels.set(key, label);
33
- }
34
- return nodeIdMap.get(key)!;
16
+ function createNodeRegistry(): MermaidNodeRegistry {
17
+ return { nodeIdMap: new Map(), nodeLabels: new Map(), counter: 0 };
18
+ }
19
+
20
+ function registerNode(reg: MermaidNodeRegistry, key: string, label?: string): string {
21
+ if (!reg.nodeIdMap.has(key)) {
22
+ reg.nodeIdMap.set(key, `n${reg.counter++}`);
23
+ if (label) reg.nodeLabels.set(key, label);
35
24
  }
25
+ return reg.nodeIdMap.get(key)!;
26
+ }
36
27
 
37
- // Register all nodes (changed functions + their callers)
38
- for (const fn of data.affectedFunctions) {
39
- nodeId(`${fn.file}::${fn.name}:${fn.line}`, fn.name);
28
+ function registerAllNodes(reg: MermaidNodeRegistry, affectedFunctions: any[]): void {
29
+ for (const fn of affectedFunctions) {
30
+ registerNode(reg, `${fn.file}::${fn.name}:${fn.line}`, fn.name);
40
31
  for (const callers of Object.values(fn.levels || {})) {
41
32
  for (const c of callers as Array<{ name: string; file: string; line: number }>) {
42
- nodeId(`${c.file}::${c.name}:${c.line}`, c.name);
33
+ registerNode(reg, `${c.file}::${c.name}:${c.line}`, c.name);
43
34
  }
44
35
  }
45
36
  }
37
+ }
46
38
 
47
- // Collect all edges and determine blast radius
39
+ function collectEdges(affectedFunctions: any[]): ImpactEdgeSets {
48
40
  const allEdges = new Set<string>();
49
41
  const edgeFromNodes = new Set<string>();
50
42
  const edgeToNodes = new Set<string>();
51
43
  const changedKeys = new Set<string>();
52
44
 
53
- for (const fn of data.affectedFunctions) {
45
+ for (const fn of affectedFunctions) {
54
46
  changedKeys.add(`${fn.file}::${fn.name}:${fn.line}`);
55
47
  for (const edge of fn.edges || []) {
56
48
  const edgeKey = `${edge.from}|${edge.to}`;
@@ -62,30 +54,42 @@ export function diffImpactMermaid(
62
54
  }
63
55
  }
64
56
 
65
- // Blast radius: caller nodes that are never a source (leaf nodes of the impact tree)
57
+ return { allEdges, edgeFromNodes, edgeToNodes, changedKeys };
58
+ }
59
+
60
+ function classifyCallerNodes(edges: ImpactEdgeSets): {
61
+ blastRadiusKeys: Set<string>;
62
+ intermediateKeys: Set<string>;
63
+ } {
66
64
  const blastRadiusKeys = new Set<string>();
67
- for (const key of edgeToNodes) {
68
- if (!edgeFromNodes.has(key) && !changedKeys.has(key)) {
65
+ for (const key of edges.edgeToNodes) {
66
+ if (!edges.edgeFromNodes.has(key) && !edges.changedKeys.has(key)) {
69
67
  blastRadiusKeys.add(key);
70
68
  }
71
69
  }
72
70
 
73
- // Intermediate callers: not changed, not blast radius
74
71
  const intermediateKeys = new Set<string>();
75
- for (const key of edgeToNodes) {
76
- if (!changedKeys.has(key) && !blastRadiusKeys.has(key)) {
72
+ for (const key of edges.edgeToNodes) {
73
+ if (!edges.changedKeys.has(key) && !blastRadiusKeys.has(key)) {
77
74
  intermediateKeys.add(key);
78
75
  }
79
76
  }
80
77
 
81
- // Group changed functions by file
82
- const fileGroups = new Map<string, typeof data.affectedFunctions>();
83
- for (const fn of data.affectedFunctions) {
78
+ return { blastRadiusKeys, intermediateKeys };
79
+ }
80
+
81
+ function emitFileSubgraphs(
82
+ lines: string[],
83
+ affectedFunctions: any[],
84
+ newFileSet: Set<string>,
85
+ reg: MermaidNodeRegistry,
86
+ ): number {
87
+ const fileGroups = new Map<string, any[]>();
88
+ for (const fn of affectedFunctions) {
84
89
  if (!fileGroups.has(fn.file)) fileGroups.set(fn.file, []);
85
90
  fileGroups.get(fn.file)!.push(fn);
86
91
  }
87
92
 
88
- // Emit changed-file subgraphs
89
93
  let sgCounter = 0;
90
94
  for (const [file, fns] of fileGroups) {
91
95
  const isNew = newFileSet.has(file);
@@ -94,33 +98,71 @@ export function diffImpactMermaid(
94
98
  lines.push(` subgraph ${sgId}["${file} **(${tag})**"]`);
95
99
  for (const fn of fns) {
96
100
  const key = `${fn.file}::${fn.name}:${fn.line}`;
97
- lines.push(` ${nodeIdMap.get(key)}["${fn.name}"]`);
101
+ lines.push(` ${reg.nodeIdMap.get(key)}["${fn.name}"]`);
98
102
  }
99
103
  lines.push(' end');
100
104
  const style = isNew ? 'fill:#e8f5e9,stroke:#4caf50' : 'fill:#fff3e0,stroke:#ff9800';
101
105
  lines.push(` style ${sgId} ${style}`);
102
106
  }
103
107
 
104
- // Emit intermediate caller nodes (outside subgraphs)
105
- for (const key of intermediateKeys) {
106
- lines.push(` ${nodeIdMap.get(key)}["${nodeLabels.get(key)}"]`);
108
+ return sgCounter;
109
+ }
110
+
111
+ function emitBlastRadiusSubgraph(
112
+ lines: string[],
113
+ blastRadiusKeys: Set<string>,
114
+ reg: MermaidNodeRegistry,
115
+ sgCounter: number,
116
+ ): void {
117
+ if (blastRadiusKeys.size === 0) return;
118
+ const sgId = `sg${sgCounter}`;
119
+ lines.push(` subgraph ${sgId}["Callers **(blast radius)**"]`);
120
+ for (const key of blastRadiusKeys) {
121
+ lines.push(` ${reg.nodeIdMap.get(key)}["${reg.nodeLabels.get(key)}"]`);
107
122
  }
123
+ lines.push(' end');
124
+ lines.push(` style ${sgId} fill:#f3e5f5,stroke:#9c27b0`);
125
+ }
108
126
 
109
- // Emit blast radius subgraph
110
- if (blastRadiusKeys.size > 0) {
111
- const sgId = `sg${sgCounter++}`;
112
- lines.push(` subgraph ${sgId}["Callers **(blast radius)**"]`);
113
- for (const key of blastRadiusKeys) {
114
- lines.push(` ${nodeIdMap.get(key)}["${nodeLabels.get(key)}"]`);
115
- }
116
- lines.push(' end');
117
- lines.push(` style ${sgId} fill:#f3e5f5,stroke:#9c27b0`);
127
+ export function diffImpactMermaid(
128
+ customDbPath: string,
129
+ opts: {
130
+ noTests?: boolean;
131
+ depth?: number;
132
+ staged?: boolean;
133
+ ref?: string;
134
+ includeImplementors?: boolean;
135
+ limit?: number;
136
+ offset?: number;
137
+ config?: any;
138
+ } = {},
139
+ ): string {
140
+ const data: any = diffImpactData(customDbPath, opts);
141
+ if ('error' in data) return data.error as string;
142
+ if (data.changedFiles === 0 || data.affectedFunctions.length === 0) {
143
+ return 'flowchart TB\n none["No impacted functions detected"]';
118
144
  }
119
145
 
120
- // Emit edges (impact flows from changed fn toward callers)
121
- for (const edgeKey of allEdges) {
146
+ const newFileSet = new Set<string>(data.newFiles || []);
147
+ const lines = ['flowchart TB'];
148
+
149
+ const reg = createNodeRegistry();
150
+ registerAllNodes(reg, data.affectedFunctions);
151
+
152
+ const edges = collectEdges(data.affectedFunctions);
153
+ const { blastRadiusKeys, intermediateKeys } = classifyCallerNodes(edges);
154
+
155
+ const sgCounter = emitFileSubgraphs(lines, data.affectedFunctions, newFileSet, reg);
156
+
157
+ for (const key of intermediateKeys) {
158
+ lines.push(` ${reg.nodeIdMap.get(key)}["${reg.nodeLabels.get(key)}"]`);
159
+ }
160
+
161
+ emitBlastRadiusSubgraph(lines, blastRadiusKeys, reg, sgCounter);
162
+
163
+ for (const edgeKey of edges.allEdges) {
122
164
  const [from, to] = edgeKey.split('|') as [string, string];
123
- lines.push(` ${nodeIdMap.get(from)} --> ${nodeIdMap.get(to)}`);
165
+ lines.push(` ${reg.nodeIdMap.get(from)} --> ${reg.nodeIdMap.get(to)}`);
124
166
  }
125
167
 
126
168
  return lines.join('\n');