@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
@@ -12,10 +12,9 @@ import type { ASTNodeKind, BetterSqlite3Database, Definition, TreeSitterNode } f
12
12
 
13
13
  // ─── Constants ────────────────────────────────────────────────────────
14
14
 
15
- export const AST_NODE_KINDS: ASTNodeKind[] = ['call', 'new', 'string', 'regex', 'throw', 'await'];
15
+ export const AST_NODE_KINDS: ASTNodeKind[] = ['new', 'string', 'regex', 'throw', 'await'];
16
16
 
17
17
  const KIND_ICONS: Record<string, string> = {
18
- call: '\u0192', // ƒ
19
18
  new: '\u2295', // ⊕
20
19
  string: '"',
21
20
  regex: '/',
@@ -59,38 +58,11 @@ function findParentDef(defs: Definition[], line: number): Definition | null {
59
58
  return best;
60
59
  }
61
60
 
62
- // ─── Build ────────────────────────────────────────────────────────────
61
+ // ─── Build helpers ───────────────────────────────────────────────────
63
62
 
64
- export async function buildAstNodes(
65
- db: BetterSqlite3Database,
66
- fileSymbols: Map<string, FileSymbols>,
67
- _rootDir: string,
68
- engineOpts?: {
69
- nativeDb?: {
70
- bulkInsertAstNodes(
71
- batches: Array<{
72
- file: string;
73
- nodes: Array<{
74
- line: number;
75
- kind: string;
76
- name: string;
77
- text?: string | null;
78
- receiver?: string | null;
79
- }>;
80
- }>,
81
- ): number;
82
- };
83
- suspendJsDb?: () => void;
84
- resumeJsDb?: () => void;
85
- },
86
- ): Promise<void> {
87
- // ── Native bulk-insert fast path ──────────────────────────────────────
88
- // Uses NativeDatabase persistent connection (Phase 6.15+).
89
- // Standalone napi functions were removed in 6.17.
90
- const nativeDb = engineOpts?.nativeDb;
91
- if (nativeDb?.bulkInsertAstNodes) {
92
- let needsJsFallback = false;
93
- const batches: Array<{
63
+ interface NativeDbHandle {
64
+ bulkInsertAstNodes(
65
+ batches: Array<{
94
66
  file: string;
95
67
  nodes: Array<{
96
68
  line: number;
@@ -99,47 +71,127 @@ export async function buildAstNodes(
99
71
  text?: string | null;
100
72
  receiver?: string | null;
101
73
  }>;
102
- }> = [];
103
-
104
- for (const [relPath, symbols] of fileSymbols) {
105
- if (Array.isArray(symbols.astNodes)) {
106
- batches.push({
107
- file: relPath,
108
- nodes: symbols.astNodes.map((n) => ({
109
- line: n.line,
110
- kind: n.kind,
111
- name: n.name,
112
- text: n.text,
113
- receiver: n.receiver ?? '',
114
- })),
115
- });
116
- } else if (symbols.calls || symbols._tree) {
117
- needsJsFallback = true;
118
- break;
119
- }
120
- }
74
+ }>,
75
+ ): number;
76
+ }
121
77
 
122
- if (!needsJsFallback) {
123
- const expectedNodes = batches.reduce((s, b) => s + b.nodes.length, 0);
124
- let inserted: number;
125
- try {
126
- engineOpts?.suspendJsDb?.();
127
- inserted = nativeDb.bulkInsertAstNodes(batches);
128
- } finally {
129
- engineOpts?.resumeJsDb?.();
130
- }
131
- if (inserted === expectedNodes) {
132
- debug(`AST extraction (native bulk): ${inserted} nodes stored`);
133
- return;
134
- }
135
- debug(
136
- `AST extraction (native bulk): expected ${expectedNodes}, got ${inserted} — falling back to JS`,
137
- );
138
- // fall through to JS path
78
+ interface EngineOpts {
79
+ nativeDb?: NativeDbHandle;
80
+ suspendJsDb?: () => void;
81
+ resumeJsDb?: () => void;
82
+ }
83
+
84
+ /**
85
+ * Attempt native bulk-insert of AST nodes.
86
+ * Returns `true` if all nodes were inserted natively, `false` if JS fallback is needed.
87
+ */
88
+ function tryNativeBulkInsert(
89
+ fileSymbols: Map<string, FileSymbols>,
90
+ engineOpts: EngineOpts | undefined,
91
+ ): boolean {
92
+ const nativeDb = engineOpts?.nativeDb;
93
+ if (!nativeDb?.bulkInsertAstNodes) return false;
94
+
95
+ const batches: Array<{
96
+ file: string;
97
+ nodes: Array<{
98
+ line: number;
99
+ kind: string;
100
+ name: string;
101
+ text?: string | null;
102
+ receiver?: string | null;
103
+ }>;
104
+ }> = [];
105
+
106
+ for (const [relPath, symbols] of fileSymbols) {
107
+ if (Array.isArray(symbols.astNodes)) {
108
+ batches.push({
109
+ file: relPath,
110
+ nodes: symbols.astNodes.map((n) => ({
111
+ line: n.line,
112
+ kind: n.kind,
113
+ name: n.name,
114
+ text: n.text,
115
+ receiver: n.receiver ?? '',
116
+ })),
117
+ });
118
+ } else if (symbols.calls || symbols._tree) {
119
+ return false; // needs JS fallback
139
120
  }
140
121
  }
141
122
 
142
- // ── JS fallback path ──────────────────────────────────────────────────
123
+ const expectedNodes = batches.reduce((s, b) => s + b.nodes.length, 0);
124
+ let inserted: number;
125
+ try {
126
+ engineOpts?.suspendJsDb?.();
127
+ inserted = nativeDb.bulkInsertAstNodes(batches);
128
+ } finally {
129
+ engineOpts?.resumeJsDb?.();
130
+ }
131
+
132
+ if (inserted === expectedNodes) {
133
+ debug(`AST extraction (native bulk): ${inserted} nodes stored`);
134
+ return true;
135
+ }
136
+ debug(
137
+ `AST extraction (native bulk): expected ${expectedNodes}, got ${inserted} — falling back to JS`,
138
+ );
139
+ return false;
140
+ }
141
+
142
+ /** Collect AST rows for a single file, resolving parent node IDs. */
143
+ function collectFileAstRows(
144
+ db: BetterSqlite3Database,
145
+ relPath: string,
146
+ symbols: FileSymbols,
147
+ ): AstRow[] {
148
+ const defs = symbols.definitions || [];
149
+ const nodeIdMap = new Map<string, number>();
150
+ for (const row of bulkNodeIdsByFile(db, relPath)) {
151
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
152
+ }
153
+
154
+ if (Array.isArray(symbols.astNodes)) {
155
+ return symbols.astNodes.map((n) => {
156
+ const parentDef = findParentDef(defs, n.line);
157
+ const parentNodeId = parentDef
158
+ ? nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null
159
+ : null;
160
+ return {
161
+ file: relPath,
162
+ line: n.line,
163
+ kind: n.kind,
164
+ name: n.name,
165
+ text: n.text || null,
166
+ receiver: n.receiver || null,
167
+ parentNodeId,
168
+ };
169
+ });
170
+ }
171
+
172
+ // WASM fallback — walk tree if available
173
+ const ext = path.extname(relPath).toLowerCase();
174
+ if (WALK_EXTENSIONS.has(ext) && symbols._tree) {
175
+ const rows: AstRow[] = [];
176
+ walkAst(symbols._tree.rootNode, defs, relPath, rows, nodeIdMap);
177
+ return rows;
178
+ }
179
+
180
+ return [];
181
+ }
182
+
183
+ // ─── Build ────────────────────────────────────────────────────────────
184
+
185
+ export async function buildAstNodes(
186
+ db: BetterSqlite3Database,
187
+ fileSymbols: Map<string, FileSymbols>,
188
+ _rootDir: string,
189
+ engineOpts?: EngineOpts,
190
+ ): Promise<void> {
191
+ // Native bulk-insert fast path (Phase 6.15+)
192
+ if (tryNativeBulkInsert(fileSymbols, engineOpts)) return;
193
+
194
+ // JS fallback path
143
195
  let insertStmt: ReturnType<BetterSqlite3Database['prepare']>;
144
196
  try {
145
197
  insertStmt = db.prepare(
@@ -157,43 +209,8 @@ export async function buildAstNodes(
157
209
  });
158
210
 
159
211
  const allRows: AstRow[] = [];
160
-
161
212
  for (const [relPath, symbols] of fileSymbols) {
162
- const defs = symbols.definitions || [];
163
-
164
- const nodeIdMap = new Map<string, number>();
165
- for (const row of bulkNodeIdsByFile(db, relPath)) {
166
- nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
167
- }
168
-
169
- if (Array.isArray(symbols.astNodes)) {
170
- // Native engine provided AST nodes (may be empty for files with no AST content)
171
- for (const n of symbols.astNodes) {
172
- const parentDef = findParentDef(defs, n.line);
173
- let parentNodeId: number | null = null;
174
- if (parentDef) {
175
- parentNodeId =
176
- nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
177
- }
178
- allRows.push({
179
- file: relPath,
180
- line: n.line,
181
- kind: n.kind,
182
- name: n.name,
183
- text: n.text || null,
184
- receiver: n.receiver || null,
185
- parentNodeId,
186
- });
187
- }
188
- } else {
189
- // WASM fallback — walk tree if available
190
- const ext = path.extname(relPath).toLowerCase();
191
- if (WALK_EXTENSIONS.has(ext) && symbols._tree) {
192
- const astRows: AstRow[] = [];
193
- walkAst(symbols._tree.rootNode, defs, relPath, astRows, nodeIdMap);
194
- allRows.push(...astRows);
195
- }
196
- }
213
+ allRows.push(...collectFileAstRows(db, relPath, symbols));
197
214
  }
198
215
 
199
216
  if (allRows.length > 0) {
@@ -351,23 +368,25 @@ export function astQuery(
351
368
  if (outputResult(data, 'results', opts)) return;
352
369
 
353
370
  if (data.results.length === 0) {
354
- console.log(`No AST nodes found${pattern ? ` matching "${pattern}"` : ''}.`);
371
+ process.stdout.write(`No AST nodes found${pattern ? ` matching "${pattern}"` : ''}.\n`);
355
372
  return;
356
373
  }
357
374
 
358
375
  const kindLabel = opts.kind ? ` (kind: ${opts.kind})` : '';
359
- console.log(`\n${data.count} AST nodes${pattern ? ` matching "${pattern}"` : ''}${kindLabel}:\n`);
376
+ process.stdout.write(
377
+ `\n${data.count} AST nodes${pattern ? ` matching "${pattern}"` : ''}${kindLabel}:\n\n`,
378
+ );
360
379
 
361
380
  for (const r of data.results) {
362
381
  const icon = KIND_ICONS[r.kind] || '?';
363
382
  const parentInfo = r.parent ? ` (in ${r.parent.name})` : '';
364
- console.log(` ${icon} ${r.name} -- ${r.file}:${r.line}${parentInfo}`);
383
+ process.stdout.write(` ${icon} ${r.name} -- ${r.file}:${r.line}${parentInfo}\n`);
365
384
  }
366
385
 
367
386
  if (data._pagination?.hasMore) {
368
- console.log(
369
- `\n ... ${data._pagination.total - data._pagination.offset - data._pagination.returned} more (use --offset ${data._pagination.offset + data._pagination.limit})`,
387
+ process.stdout.write(
388
+ `\n ... ${data._pagination.total - data._pagination.offset - data._pagination.returned} more (use --offset ${data._pagination.offset + data._pagination.limit})\n`,
370
389
  );
371
390
  }
372
- console.log();
391
+ process.stdout.write('\n');
373
392
  }
@@ -4,7 +4,9 @@ import { normalizeFileFilter } from '../db/query-builder.js';
4
4
  import { bfsTransitiveCallers } from '../domain/analysis/impact.js';
5
5
  import { explainData } from '../domain/queries.js';
6
6
  import { loadConfig } from '../infrastructure/config.js';
7
+ import { debug } from '../infrastructure/logger.js';
7
8
  import { isTestFile } from '../infrastructure/test-filter.js';
9
+ import { toErrorMessage } from '../shared/errors.js';
8
10
  import type { BetterSqlite3Database, CodegraphConfig } from '../types.js';
9
11
  import { RULE_DEFS } from './manifesto.js';
10
12
 
@@ -41,8 +43,8 @@ function resolveThresholds(
41
43
  };
42
44
  }
43
45
  return resolved;
44
- } catch {
45
- // Fall back to defaults if config loading fails
46
+ } catch (e) {
47
+ debug(`resolveThresholds: config loading failed, using defaults: ${toErrorMessage(e)}`);
46
48
  const resolved: Record<string, ThresholdEntry> = {};
47
49
  for (const def of FUNCTION_RULES) {
48
50
  resolved[def.name] = {
@@ -110,8 +112,8 @@ function readPhase44(db: BetterSqlite3Database, nodeId: number): Phase44Fields {
110
112
  sideEffects: row.side_effects ?? null,
111
113
  };
112
114
  }
113
- } catch {
114
- /* columns don't exist yet */
115
+ } catch (e) {
116
+ debug(`readPhase44: columns may not exist yet: ${toErrorMessage(e)}`);
115
117
  }
116
118
  return { riskScore: null, complexityNotes: null, sideEffects: null };
117
119
  }
@@ -413,8 +415,8 @@ function buildHealth(
413
415
  commentLines: row.comment_lines || 0,
414
416
  thresholdBreaches: checkBreaches(row as unknown as Record<string, unknown>, thresholds),
415
417
  };
416
- } catch {
417
- /* table may not exist */
418
+ } catch (e) {
419
+ debug(`readHealth: function_complexity table may not exist: ${toErrorMessage(e)}`);
418
420
  return defaultHealth();
419
421
  }
420
422
  }
@@ -8,6 +8,7 @@ import { kindIcon } from '../domain/queries.js';
8
8
  import { debug } from '../infrastructure/logger.js';
9
9
  import { getNative, isNativeAvailable } from '../infrastructure/native.js';
10
10
  import { isTestFile } from '../infrastructure/test-filter.js';
11
+ import { toErrorMessage } from '../shared/errors.js';
11
12
  import type { EngineMode, NativeDatabase } from '../types.js';
12
13
 
13
14
  // ─── Git Helpers ────────────────────────────────────────────────────────
@@ -20,7 +21,8 @@ function validateGitRef(repoRoot: string, ref: string): string | null {
20
21
  stdio: ['pipe', 'pipe', 'pipe'],
21
22
  }).trim();
22
23
  return sha;
23
- } catch {
24
+ } catch (e) {
25
+ debug(`validateGitRef failed for "${ref}": ${toErrorMessage(e)}`);
24
26
  return null;
25
27
  }
26
28
  }
@@ -50,11 +52,12 @@ function removeWorktree(repoRoot: string, dir: string): void {
50
52
  encoding: 'utf-8',
51
53
  stdio: ['pipe', 'pipe', 'pipe'],
52
54
  });
53
- } catch {
55
+ } catch (e) {
56
+ debug(`removeWorktree: git worktree remove failed for ${dir}: ${toErrorMessage(e)}`);
54
57
  try {
55
58
  fs.rmSync(dir, { recursive: true, force: true });
56
- } catch {
57
- /* best-effort */
59
+ } catch (rmErr) {
60
+ debug(`removeWorktree: rmSync fallback failed for ${dir}: ${toErrorMessage(rmErr)}`);
58
61
  }
59
62
  try {
60
63
  execFileSync('git', ['worktree', 'prune'], {
@@ -62,8 +65,8 @@ function removeWorktree(repoRoot: string, dir: string): void {
62
65
  encoding: 'utf-8',
63
66
  stdio: ['pipe', 'pipe', 'pipe'],
64
67
  });
65
- } catch {
66
- /* best-effort */
68
+ } catch (pruneErr) {
69
+ debug(`removeWorktree: git worktree prune failed: ${toErrorMessage(pruneErr)}`);
67
70
  }
68
71
  }
69
72
  }
@@ -117,7 +120,7 @@ function loadSymbolsFromDb(
117
120
  const native = getNative();
118
121
  nativeDb = native.NativeDatabase.openReadonly(dbPath);
119
122
  } catch (e) {
120
- debug(`loadSymbolsFromDb: native path failed: ${(e as Error).message}`);
123
+ debug(`loadSymbolsFromDb: native path failed: ${toErrorMessage(e)}`);
121
124
  }
122
125
  }
123
126
 
@@ -205,8 +208,8 @@ function loadSymbolsFromDb(
205
208
  if (nativeDb) {
206
209
  try {
207
210
  nativeDb.close();
208
- } catch {
209
- /* already closed */
211
+ } catch (e) {
212
+ debug(`loadSymbolsFromDb: nativeDb close failed: ${toErrorMessage(e)}`);
210
213
  }
211
214
  }
212
215
  }
@@ -360,6 +363,38 @@ interface BranchCompareResult {
360
363
  summary?: BranchCompareSummary;
361
364
  }
362
365
 
366
+ function attachImpactToSymbols(
367
+ symbols: SymbolInfo[],
368
+ dbPath: string,
369
+ _baseSymbols: Map<string, SymbolInfo>,
370
+ maxDepth: number,
371
+ noTests: boolean,
372
+ ): void {
373
+ for (const sym of symbols) {
374
+ const symCallers = loadCallersFromDb(dbPath, sym.id ? [sym.id] : [], maxDepth, noTests);
375
+ (sym as SymbolInfo & { impact?: CallerInfo[] }).impact = symCallers;
376
+ }
377
+ }
378
+
379
+ function attachImpactToChanged(
380
+ changed: ChangedSymbol[],
381
+ dbPath: string,
382
+ baseSymbols: Map<string, SymbolInfo>,
383
+ maxDepth: number,
384
+ noTests: boolean,
385
+ ): void {
386
+ for (const sym of changed) {
387
+ const baseSym = baseSymbols.get(makeSymbolKey(sym.kind, sym.file, sym.name));
388
+ const symCallers = loadCallersFromDb(
389
+ dbPath,
390
+ baseSym?.id ? [baseSym.id] : [],
391
+ maxDepth,
392
+ noTests,
393
+ );
394
+ sym.impact = symCallers;
395
+ }
396
+ }
397
+
363
398
  export async function branchCompareData(
364
399
  baseRef: string,
365
400
  targetRef: string,
@@ -376,7 +411,8 @@ export async function branchCompareData(
376
411
  encoding: 'utf-8',
377
412
  stdio: ['pipe', 'pipe', 'pipe'],
378
413
  });
379
- } catch {
414
+ } catch (e) {
415
+ debug(`branchCompareData: git check failed: ${toErrorMessage(e)}`);
380
416
  return { error: 'Not a git repository' };
381
417
  }
382
418
 
@@ -440,20 +476,8 @@ export async function branchCompareData(
440
476
  const removedImpact = loadCallersFromDb(baseDbPath, removedIds, maxDepth, noTests);
441
477
  const changedImpact = loadCallersFromDb(baseDbPath, changedIds, maxDepth, noTests);
442
478
 
443
- for (const sym of removed) {
444
- const symCallers = loadCallersFromDb(baseDbPath, sym.id ? [sym.id] : [], maxDepth, noTests);
445
- (sym as SymbolInfo & { impact?: CallerInfo[] }).impact = symCallers;
446
- }
447
- for (const sym of changed) {
448
- const baseSym = baseSymbols.get(makeSymbolKey(sym.kind, sym.file, sym.name));
449
- const symCallers = loadCallersFromDb(
450
- baseDbPath,
451
- baseSym?.id ? [baseSym.id] : [],
452
- maxDepth,
453
- noTests,
454
- );
455
- sym.impact = symCallers;
456
- }
479
+ attachImpactToSymbols(removed, baseDbPath, baseSymbols, maxDepth, noTests);
480
+ attachImpactToChanged(changed, baseDbPath, baseSymbols, maxDepth, noTests);
457
481
 
458
482
  const allImpacted = new Set<string>();
459
483
  for (const c of removedImpact) allImpacted.add(`${c.file}:${c.name}`);
@@ -489,20 +513,66 @@ export async function branchCompareData(
489
513
  },
490
514
  };
491
515
  } catch (err) {
492
- return { error: (err as Error).message };
516
+ return { error: toErrorMessage(err) };
493
517
  } finally {
494
518
  removeWorktree(repoRoot, baseDir);
495
519
  removeWorktree(repoRoot, targetDir);
496
520
  try {
497
521
  fs.rmSync(tmpBase, { recursive: true, force: true });
498
- } catch {
499
- /* best-effort */
522
+ } catch (cleanupErr) {
523
+ debug(`branchCompareData: temp cleanup failed: ${toErrorMessage(cleanupErr)}`);
500
524
  }
501
525
  }
502
526
  }
503
527
 
504
528
  // ─── Mermaid Output ─────────────────────────────────────────────────────
505
529
 
530
+ interface MermaidNodeIdState {
531
+ counter: number;
532
+ map: Map<string, string>;
533
+ }
534
+
535
+ function mermaidNodeId(state: MermaidNodeIdState, key: string): string {
536
+ if (!state.map.has(key)) {
537
+ state.map.set(key, `n${state.counter++}`);
538
+ }
539
+ return state.map.get(key)!;
540
+ }
541
+
542
+ function addMermaidSubgraph(
543
+ lines: string[],
544
+ state: MermaidNodeIdState,
545
+ prefix: string,
546
+ label: string,
547
+ symbols: Array<{ kind: string; file: string; name: string }>,
548
+ fillColor: string,
549
+ strokeColor: string,
550
+ ): void {
551
+ if (symbols.length === 0) return;
552
+ lines.push(` subgraph sg_${prefix}["${label}"]`);
553
+ for (const sym of symbols) {
554
+ const key = `${prefix}::${sym.kind}::${sym.file}::${sym.name}`;
555
+ const nid = mermaidNodeId(state, key);
556
+ lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
557
+ }
558
+ lines.push(' end');
559
+ lines.push(` style sg_${prefix} fill:${fillColor},stroke:${strokeColor}`);
560
+ }
561
+
562
+ function collectImpactedCallers(
563
+ impactSources: Array<{ impact?: CallerInfo[] }>,
564
+ ): Map<string, CallerInfo> {
565
+ const allImpacted = new Map<string, CallerInfo>();
566
+ for (const sym of impactSources) {
567
+ if (!sym.impact) continue;
568
+ for (const c of sym.impact) {
569
+ const key = `impact::${c.kind}::${c.file}::${c.name}`;
570
+ if (!allImpacted.has(key)) allImpacted.set(key, c);
571
+ }
572
+ }
573
+ return allImpacted;
574
+ }
575
+
506
576
  export function branchCompareMermaid(data: BranchCompareResult): string {
507
577
  if (data.error) return data.error;
508
578
  if (
@@ -514,63 +584,19 @@ export function branchCompareMermaid(data: BranchCompareResult): string {
514
584
  }
515
585
 
516
586
  const lines = ['flowchart TB'];
517
- let nodeCounter = 0;
518
- const nodeIdMap = new Map<string, string>();
519
-
520
- function nodeId(key: string): string {
521
- if (!nodeIdMap.has(key)) {
522
- nodeIdMap.set(key, `n${nodeCounter++}`);
523
- }
524
- return nodeIdMap.get(key)!;
525
- }
526
-
527
- if (data.added && data.added.length > 0) {
528
- lines.push(' subgraph sg_added["Added"]');
529
- for (const sym of data.added) {
530
- const key = `added::${sym.kind}::${sym.file}::${sym.name}`;
531
- const nid = nodeId(key);
532
- lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
533
- }
534
- lines.push(' end');
535
- lines.push(' style sg_added fill:#e8f5e9,stroke:#4caf50');
536
- }
537
-
538
- if (data.removed && data.removed.length > 0) {
539
- lines.push(' subgraph sg_removed["Removed"]');
540
- for (const sym of data.removed) {
541
- const key = `removed::${sym.kind}::${sym.file}::${sym.name}`;
542
- const nid = nodeId(key);
543
- lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
544
- }
545
- lines.push(' end');
546
- lines.push(' style sg_removed fill:#ffebee,stroke:#f44336');
547
- }
587
+ const state: MermaidNodeIdState = { counter: 0, map: new Map() };
548
588
 
549
- if (data.changed && data.changed.length > 0) {
550
- lines.push(' subgraph sg_changed["Changed"]');
551
- for (const sym of data.changed) {
552
- const key = `changed::${sym.kind}::${sym.file}::${sym.name}`;
553
- const nid = nodeId(key);
554
- lines.push(` ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
555
- }
556
- lines.push(' end');
557
- lines.push(' style sg_changed fill:#fff3e0,stroke:#ff9800');
558
- }
589
+ addMermaidSubgraph(lines, state, 'added', 'Added', data.added || [], '#e8f5e9', '#4caf50');
590
+ addMermaidSubgraph(lines, state, 'removed', 'Removed', data.removed || [], '#ffebee', '#f44336');
591
+ addMermaidSubgraph(lines, state, 'changed', 'Changed', data.changed || [], '#fff3e0', '#ff9800');
559
592
 
560
- const allImpacted = new Map<string, CallerInfo>();
561
593
  const impactSources = [...(data.removed || []), ...(data.changed || [])];
562
- for (const sym of impactSources) {
563
- if (!sym.impact) continue;
564
- for (const c of sym.impact) {
565
- const key = `impact::${c.kind}::${c.file}::${c.name}`;
566
- if (!allImpacted.has(key)) allImpacted.set(key, c);
567
- }
568
- }
594
+ const allImpacted = collectImpactedCallers(impactSources);
569
595
 
570
596
  if (allImpacted.size > 0) {
571
597
  lines.push(' subgraph sg_impact["Impacted Callers"]');
572
598
  for (const [key, c] of allImpacted) {
573
- const nid = nodeId(key);
599
+ const nid = mermaidNodeId(state, key);
574
600
  lines.push(` ${nid}["[${kindIcon(c.kind)}] ${c.name}"]`);
575
601
  }
576
602
  lines.push(' end');
@@ -583,8 +609,8 @@ export function branchCompareMermaid(data: BranchCompareResult): string {
583
609
  const symKey = `${prefix}::${sym.kind}::${sym.file}::${sym.name}`;
584
610
  for (const c of sym.impact) {
585
611
  const callerKey = `impact::${c.kind}::${c.file}::${c.name}`;
586
- if (nodeIdMap.has(symKey) && nodeIdMap.has(callerKey)) {
587
- lines.push(` ${nodeIdMap.get(symKey)} -.-> ${nodeIdMap.get(callerKey)}`);
612
+ if (state.map.has(symKey) && state.map.has(callerKey)) {
613
+ lines.push(` ${state.map.get(symKey)} -.-> ${state.map.get(callerKey)}`);
588
614
  }
589
615
  }
590
616
  }
@@ -86,10 +86,7 @@ interface DriftResult {
86
86
  driftScore: number;
87
87
  }
88
88
 
89
- function analyzeDrift(
90
- communities: CommunityObject[],
91
- communityDirs: Map<number, Set<string>>,
92
- ): DriftResult {
89
+ function buildDirToCommunities(communityDirs: Map<number, Set<string>>): Map<string, Set<number>> {
93
90
  const dirToCommunities = new Map<string, Set<number>>();
94
91
  for (const [cid, dirs] of communityDirs) {
95
92
  for (const dir of dirs) {
@@ -97,20 +94,28 @@ function analyzeDrift(
97
94
  dirToCommunities.get(dir)!.add(cid);
98
95
  }
99
96
  }
97
+ return dirToCommunities;
98
+ }
100
99
 
101
- const splitCandidates: DriftResult['splitCandidates'] = [];
100
+ function findSplitCandidates(
101
+ dirToCommunities: Map<string, Set<number>>,
102
+ ): DriftResult['splitCandidates'] {
103
+ const candidates: DriftResult['splitCandidates'] = [];
102
104
  for (const [dir, cids] of dirToCommunities) {
103
105
  if (cids.size >= 2) {
104
- splitCandidates.push({ directory: dir, communityCount: cids.size });
106
+ candidates.push({ directory: dir, communityCount: cids.size });
105
107
  }
106
108
  }
107
- splitCandidates.sort((a, b) => b.communityCount - a.communityCount);
109
+ candidates.sort((a, b) => b.communityCount - a.communityCount);
110
+ return candidates;
111
+ }
108
112
 
109
- const mergeCandidates: DriftResult['mergeCandidates'] = [];
113
+ function findMergeCandidates(communities: CommunityObject[]): DriftResult['mergeCandidates'] {
114
+ const candidates: DriftResult['mergeCandidates'] = [];
110
115
  for (const c of communities) {
111
116
  const dirCount = Object.keys(c.directories).length;
112
117
  if (dirCount >= 2) {
113
- mergeCandidates.push({
118
+ candidates.push({
114
119
  communityId: c.id,
115
120
  size: c.size,
116
121
  directoryCount: dirCount,
@@ -118,7 +123,17 @@ function analyzeDrift(
118
123
  });
119
124
  }
120
125
  }
121
- mergeCandidates.sort((a, b) => b.directoryCount - a.directoryCount);
126
+ candidates.sort((a, b) => b.directoryCount - a.directoryCount);
127
+ return candidates;
128
+ }
129
+
130
+ function analyzeDrift(
131
+ communities: CommunityObject[],
132
+ communityDirs: Map<number, Set<string>>,
133
+ ): DriftResult {
134
+ const dirToCommunities = buildDirToCommunities(communityDirs);
135
+ const splitCandidates = findSplitCandidates(dirToCommunities);
136
+ const mergeCandidates = findMergeCandidates(communities);
122
137
 
123
138
  const totalDirs = dirToCommunities.size;
124
139
  const splitRatio = totalDirs > 0 ? splitCandidates.length / totalDirs : 0;