@optave/codegraph 3.5.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/README.md +47 -21
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +119 -127
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +14 -1
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/complexity-visitor.js +11 -13
  10. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  11. package/dist/db/connection.d.ts +12 -2
  12. package/dist/db/connection.d.ts.map +1 -1
  13. package/dist/db/connection.js +81 -53
  14. package/dist/db/connection.js.map +1 -1
  15. package/dist/db/index.d.ts +1 -1
  16. package/dist/db/index.d.ts.map +1 -1
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/index.js.map +1 -1
  19. package/dist/db/migrations.d.ts.map +1 -1
  20. package/dist/db/migrations.js +38 -32
  21. package/dist/db/migrations.js.map +1 -1
  22. package/dist/domain/analysis/context.d.ts.map +1 -1
  23. package/dist/domain/analysis/context.js +51 -66
  24. package/dist/domain/analysis/context.js.map +1 -1
  25. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  26. package/dist/domain/analysis/dependencies.js +62 -70
  27. package/dist/domain/analysis/dependencies.js.map +1 -1
  28. package/dist/domain/analysis/diff-impact.d.ts +9 -7
  29. package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
  30. package/dist/domain/analysis/exports.d.ts.map +1 -1
  31. package/dist/domain/analysis/exports.js +29 -33
  32. package/dist/domain/analysis/exports.js.map +1 -1
  33. package/dist/domain/analysis/fn-impact.d.ts +15 -17
  34. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  35. package/dist/domain/analysis/fn-impact.js +35 -65
  36. package/dist/domain/analysis/fn-impact.js.map +1 -1
  37. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  38. package/dist/domain/analysis/module-map.js +91 -6
  39. package/dist/domain/analysis/module-map.js.map +1 -1
  40. package/dist/domain/analysis/query-helpers.d.ts +20 -0
  41. package/dist/domain/analysis/query-helpers.d.ts.map +1 -0
  42. package/dist/domain/analysis/query-helpers.js +27 -0
  43. package/dist/domain/analysis/query-helpers.js.map +1 -0
  44. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  45. package/dist/domain/graph/builder/helpers.js +15 -9
  46. package/dist/domain/graph/builder/helpers.js.map +1 -1
  47. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/incremental.js +3 -2
  49. package/dist/domain/graph/builder/incremental.js.map +1 -1
  50. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  51. package/dist/domain/graph/builder/pipeline.js +69 -3
  52. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  53. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  54. package/dist/domain/graph/builder/stages/build-edges.js +7 -51
  55. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  56. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/build-structure.js +7 -5
  58. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/collect-files.js +2 -2
  60. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  61. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  63. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/finalize.js +124 -105
  66. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  68. package/dist/domain/graph/builder/stages/insert-nodes.js +28 -15
  69. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  70. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  71. package/dist/domain/graph/builder/stages/resolve-imports.js +3 -2
  72. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  73. package/dist/domain/graph/resolve.d.ts +0 -4
  74. package/dist/domain/graph/resolve.d.ts.map +1 -1
  75. package/dist/domain/graph/resolve.js +32 -48
  76. package/dist/domain/graph/resolve.js.map +1 -1
  77. package/dist/domain/graph/watcher.d.ts.map +1 -1
  78. package/dist/domain/graph/watcher.js +12 -12
  79. package/dist/domain/graph/watcher.js.map +1 -1
  80. package/dist/domain/parser.d.ts +1 -1
  81. package/dist/domain/parser.d.ts.map +1 -1
  82. package/dist/domain/parser.js +206 -101
  83. package/dist/domain/parser.js.map +1 -1
  84. package/dist/domain/search/search/cli-formatter.d.ts.map +1 -1
  85. package/dist/domain/search/search/cli-formatter.js +88 -83
  86. package/dist/domain/search/search/cli-formatter.js.map +1 -1
  87. package/dist/extractors/bash.d.ts +6 -0
  88. package/dist/extractors/bash.d.ts.map +1 -0
  89. package/dist/extractors/bash.js +91 -0
  90. package/dist/extractors/bash.js.map +1 -0
  91. package/dist/extractors/c.d.ts +6 -0
  92. package/dist/extractors/c.d.ts.map +1 -0
  93. package/dist/extractors/c.js +204 -0
  94. package/dist/extractors/c.js.map +1 -0
  95. package/dist/extractors/cpp.d.ts +6 -0
  96. package/dist/extractors/cpp.d.ts.map +1 -0
  97. package/dist/extractors/cpp.js +283 -0
  98. package/dist/extractors/cpp.js.map +1 -0
  99. package/dist/extractors/csharp.d.ts.map +1 -1
  100. package/dist/extractors/csharp.js +42 -54
  101. package/dist/extractors/csharp.js.map +1 -1
  102. package/dist/extractors/dart.d.ts +6 -0
  103. package/dist/extractors/dart.d.ts.map +1 -0
  104. package/dist/extractors/dart.js +277 -0
  105. package/dist/extractors/dart.js.map +1 -0
  106. package/dist/extractors/elixir.d.ts +9 -0
  107. package/dist/extractors/elixir.d.ts.map +1 -0
  108. package/dist/extractors/elixir.js +223 -0
  109. package/dist/extractors/elixir.js.map +1 -0
  110. package/dist/extractors/go.d.ts.map +1 -1
  111. package/dist/extractors/go.js +126 -130
  112. package/dist/extractors/go.js.map +1 -1
  113. package/dist/extractors/haskell.d.ts +8 -0
  114. package/dist/extractors/haskell.d.ts.map +1 -0
  115. package/dist/extractors/haskell.js +217 -0
  116. package/dist/extractors/haskell.js.map +1 -0
  117. package/dist/extractors/hcl.js +6 -6
  118. package/dist/extractors/hcl.js.map +1 -1
  119. package/dist/extractors/helpers.d.ts +32 -1
  120. package/dist/extractors/helpers.d.ts.map +1 -1
  121. package/dist/extractors/helpers.js +74 -0
  122. package/dist/extractors/helpers.js.map +1 -1
  123. package/dist/extractors/index.d.ts +12 -0
  124. package/dist/extractors/index.d.ts.map +1 -1
  125. package/dist/extractors/index.js +12 -0
  126. package/dist/extractors/index.js.map +1 -1
  127. package/dist/extractors/java.d.ts.map +1 -1
  128. package/dist/extractors/java.js +32 -47
  129. package/dist/extractors/java.js.map +1 -1
  130. package/dist/extractors/javascript.d.ts.map +1 -1
  131. package/dist/extractors/javascript.js +306 -292
  132. package/dist/extractors/javascript.js.map +1 -1
  133. package/dist/extractors/kotlin.d.ts +6 -0
  134. package/dist/extractors/kotlin.d.ts.map +1 -0
  135. package/dist/extractors/kotlin.js +275 -0
  136. package/dist/extractors/kotlin.js.map +1 -0
  137. package/dist/extractors/lua.d.ts +6 -0
  138. package/dist/extractors/lua.d.ts.map +1 -0
  139. package/dist/extractors/lua.js +162 -0
  140. package/dist/extractors/lua.js.map +1 -0
  141. package/dist/extractors/ocaml.d.ts +6 -0
  142. package/dist/extractors/ocaml.d.ts.map +1 -0
  143. package/dist/extractors/ocaml.js +236 -0
  144. package/dist/extractors/ocaml.js.map +1 -0
  145. package/dist/extractors/php.d.ts.map +1 -1
  146. package/dist/extractors/php.js +39 -44
  147. package/dist/extractors/php.js.map +1 -1
  148. package/dist/extractors/python.d.ts.map +1 -1
  149. package/dist/extractors/python.js +75 -93
  150. package/dist/extractors/python.js.map +1 -1
  151. package/dist/extractors/ruby.js +6 -13
  152. package/dist/extractors/ruby.js.map +1 -1
  153. package/dist/extractors/rust.d.ts.map +1 -1
  154. package/dist/extractors/rust.js +58 -83
  155. package/dist/extractors/rust.js.map +1 -1
  156. package/dist/extractors/scala.d.ts +6 -0
  157. package/dist/extractors/scala.d.ts.map +1 -0
  158. package/dist/extractors/scala.js +269 -0
  159. package/dist/extractors/scala.js.map +1 -0
  160. package/dist/extractors/swift.d.ts +6 -0
  161. package/dist/extractors/swift.d.ts.map +1 -0
  162. package/dist/extractors/swift.js +275 -0
  163. package/dist/extractors/swift.js.map +1 -0
  164. package/dist/extractors/zig.d.ts +9 -0
  165. package/dist/extractors/zig.d.ts.map +1 -0
  166. package/dist/extractors/zig.js +276 -0
  167. package/dist/extractors/zig.js.map +1 -0
  168. package/dist/features/ast.d.ts +2 -0
  169. package/dist/features/ast.d.ts.map +1 -1
  170. package/dist/features/ast.js +9 -24
  171. package/dist/features/ast.js.map +1 -1
  172. package/dist/features/audit.d.ts.map +1 -1
  173. package/dist/features/audit.js +17 -21
  174. package/dist/features/audit.js.map +1 -1
  175. package/dist/features/branch-compare.d.ts.map +1 -1
  176. package/dist/features/branch-compare.js +47 -3
  177. package/dist/features/branch-compare.js.map +1 -1
  178. package/dist/features/cfg.d.ts +7 -1
  179. package/dist/features/cfg.d.ts.map +1 -1
  180. package/dist/features/cfg.js +72 -61
  181. package/dist/features/cfg.js.map +1 -1
  182. package/dist/features/check.d.ts.map +1 -1
  183. package/dist/features/check.js +79 -62
  184. package/dist/features/check.js.map +1 -1
  185. package/dist/features/complexity-query.d.ts.map +1 -1
  186. package/dist/features/complexity-query.js +142 -137
  187. package/dist/features/complexity-query.js.map +1 -1
  188. package/dist/features/complexity.d.ts +7 -1
  189. package/dist/features/complexity.d.ts.map +1 -1
  190. package/dist/features/complexity.js +62 -1
  191. package/dist/features/complexity.js.map +1 -1
  192. package/dist/features/dataflow.d.ts +7 -1
  193. package/dist/features/dataflow.d.ts.map +1 -1
  194. package/dist/features/dataflow.js +356 -188
  195. package/dist/features/dataflow.js.map +1 -1
  196. package/dist/features/graph-enrichment.d.ts.map +1 -1
  197. package/dist/features/graph-enrichment.js +117 -104
  198. package/dist/features/graph-enrichment.js.map +1 -1
  199. package/dist/features/sequence.d.ts.map +1 -1
  200. package/dist/features/sequence.js +25 -4
  201. package/dist/features/sequence.js.map +1 -1
  202. package/dist/features/structure-query.d.ts.map +1 -1
  203. package/dist/features/structure-query.js +29 -4
  204. package/dist/features/structure-query.js.map +1 -1
  205. package/dist/features/structure.d.ts.map +1 -1
  206. package/dist/features/structure.js +35 -15
  207. package/dist/features/structure.js.map +1 -1
  208. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  209. package/dist/graph/algorithms/leiden/adapter.js +88 -73
  210. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  211. package/dist/graph/algorithms/leiden/index.js +43 -28
  212. package/dist/graph/algorithms/leiden/index.js.map +1 -1
  213. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  214. package/dist/graph/algorithms/leiden/optimiser.js +90 -104
  215. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  216. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  217. package/dist/graph/algorithms/leiden/partition.js +89 -106
  218. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  219. package/dist/graph/model.d.ts +2 -0
  220. package/dist/graph/model.d.ts.map +1 -1
  221. package/dist/graph/model.js +20 -8
  222. package/dist/graph/model.js.map +1 -1
  223. package/dist/infrastructure/config.d.ts +0 -8
  224. package/dist/infrastructure/config.d.ts.map +1 -1
  225. package/dist/infrastructure/config.js +73 -62
  226. package/dist/infrastructure/config.js.map +1 -1
  227. package/dist/infrastructure/registry.d.ts +0 -8
  228. package/dist/infrastructure/registry.d.ts.map +1 -1
  229. package/dist/infrastructure/registry.js +12 -14
  230. package/dist/infrastructure/registry.js.map +1 -1
  231. package/dist/mcp/server.d.ts.map +1 -1
  232. package/dist/mcp/server.js +45 -36
  233. package/dist/mcp/server.js.map +1 -1
  234. package/dist/presentation/audit.d.ts.map +1 -1
  235. package/dist/presentation/audit.js +61 -57
  236. package/dist/presentation/audit.js.map +1 -1
  237. package/dist/presentation/branch-compare.d.ts.map +1 -1
  238. package/dist/presentation/branch-compare.js +56 -38
  239. package/dist/presentation/branch-compare.js.map +1 -1
  240. package/dist/presentation/check.d.ts.map +1 -1
  241. package/dist/presentation/check.js +30 -32
  242. package/dist/presentation/check.js.map +1 -1
  243. package/dist/presentation/colors.d.ts.map +1 -1
  244. package/dist/presentation/colors.js +2 -0
  245. package/dist/presentation/colors.js.map +1 -1
  246. package/dist/presentation/complexity.d.ts.map +1 -1
  247. package/dist/presentation/complexity.js +25 -19
  248. package/dist/presentation/complexity.js.map +1 -1
  249. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  250. package/dist/presentation/queries-cli/exports.js +15 -15
  251. package/dist/presentation/queries-cli/exports.js.map +1 -1
  252. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  253. package/dist/presentation/queries-cli/impact.js +29 -19
  254. package/dist/presentation/queries-cli/impact.js.map +1 -1
  255. package/dist/types.d.ts +182 -7
  256. package/dist/types.d.ts.map +1 -1
  257. package/grammars/tree-sitter-bash.wasm +0 -0
  258. package/grammars/tree-sitter-c.wasm +0 -0
  259. package/grammars/tree-sitter-cpp.wasm +0 -0
  260. package/grammars/tree-sitter-dart.wasm +0 -0
  261. package/grammars/tree-sitter-elixir.wasm +0 -0
  262. package/grammars/tree-sitter-haskell.wasm +0 -0
  263. package/grammars/tree-sitter-kotlin.wasm +0 -0
  264. package/grammars/tree-sitter-lua.wasm +0 -0
  265. package/grammars/tree-sitter-ocaml.wasm +0 -0
  266. package/grammars/tree-sitter-scala.wasm +0 -0
  267. package/grammars/tree-sitter-swift.wasm +0 -0
  268. package/grammars/tree-sitter-zig.wasm +0 -0
  269. package/package.json +19 -7
  270. package/src/ast-analysis/engine.ts +147 -138
  271. package/src/ast-analysis/visitors/ast-store-visitor.ts +15 -2
  272. package/src/ast-analysis/visitors/complexity-visitor.ts +11 -11
  273. package/src/db/connection.ts +90 -59
  274. package/src/db/index.ts +1 -0
  275. package/src/db/migrations.ts +36 -32
  276. package/src/domain/analysis/context.ts +73 -75
  277. package/src/domain/analysis/dependencies.ts +78 -68
  278. package/src/domain/analysis/exports.ts +45 -34
  279. package/src/domain/analysis/fn-impact.ts +67 -64
  280. package/src/domain/analysis/module-map.ts +103 -8
  281. package/src/domain/analysis/query-helpers.ts +35 -0
  282. package/src/domain/graph/builder/helpers.ts +12 -6
  283. package/src/domain/graph/builder/incremental.ts +3 -2
  284. package/src/domain/graph/builder/pipeline.ts +71 -3
  285. package/src/domain/graph/builder/stages/build-edges.ts +10 -75
  286. package/src/domain/graph/builder/stages/build-structure.ts +9 -7
  287. package/src/domain/graph/builder/stages/collect-files.ts +2 -2
  288. package/src/domain/graph/builder/stages/detect-changes.ts +7 -2
  289. package/src/domain/graph/builder/stages/finalize.ts +159 -125
  290. package/src/domain/graph/builder/stages/insert-nodes.ts +32 -21
  291. package/src/domain/graph/builder/stages/resolve-imports.ts +3 -2
  292. package/src/domain/graph/resolve.ts +34 -46
  293. package/src/domain/graph/watcher.ts +12 -14
  294. package/src/domain/parser.ts +222 -97
  295. package/src/domain/search/search/cli-formatter.ts +121 -94
  296. package/src/extractors/bash.ts +97 -0
  297. package/src/extractors/c.ts +212 -0
  298. package/src/extractors/cpp.ts +298 -0
  299. package/src/extractors/csharp.ts +53 -56
  300. package/src/extractors/dart.ts +304 -0
  301. package/src/extractors/elixir.ts +251 -0
  302. package/src/extractors/go.ts +152 -134
  303. package/src/extractors/haskell.ts +235 -0
  304. package/src/extractors/hcl.ts +6 -6
  305. package/src/extractors/helpers.ts +93 -1
  306. package/src/extractors/index.ts +12 -0
  307. package/src/extractors/java.ts +43 -48
  308. package/src/extractors/javascript.ts +328 -281
  309. package/src/extractors/kotlin.ts +293 -0
  310. package/src/extractors/lua.ts +169 -0
  311. package/src/extractors/ocaml.ts +259 -0
  312. package/src/extractors/php.ts +46 -40
  313. package/src/extractors/python.ts +81 -104
  314. package/src/extractors/ruby.ts +6 -13
  315. package/src/extractors/rust.ts +65 -85
  316. package/src/extractors/scala.ts +285 -0
  317. package/src/extractors/swift.ts +293 -0
  318. package/src/extractors/zig.ts +294 -0
  319. package/src/features/ast.ts +10 -25
  320. package/src/features/audit.ts +24 -20
  321. package/src/features/branch-compare.ts +51 -4
  322. package/src/features/cfg.ts +113 -65
  323. package/src/features/check.ts +90 -74
  324. package/src/features/complexity-query.ts +181 -163
  325. package/src/features/complexity.ts +64 -1
  326. package/src/features/dataflow.ts +462 -217
  327. package/src/features/graph-enrichment.ts +161 -117
  328. package/src/features/sequence.ts +27 -4
  329. package/src/features/structure-query.ts +43 -4
  330. package/src/features/structure.ts +50 -22
  331. package/src/graph/algorithms/leiden/adapter.ts +126 -71
  332. package/src/graph/algorithms/leiden/index.ts +67 -28
  333. package/src/graph/algorithms/leiden/optimiser.ts +114 -105
  334. package/src/graph/algorithms/leiden/partition.ts +131 -98
  335. package/src/graph/model.ts +19 -7
  336. package/src/infrastructure/config.ts +60 -58
  337. package/src/infrastructure/registry.ts +17 -14
  338. package/src/mcp/server.ts +46 -37
  339. package/src/presentation/audit.ts +72 -67
  340. package/src/presentation/branch-compare.ts +54 -50
  341. package/src/presentation/check.ts +34 -34
  342. package/src/presentation/colors.ts +2 -0
  343. package/src/presentation/complexity.ts +39 -33
  344. package/src/presentation/queries-cli/exports.ts +17 -17
  345. package/src/presentation/queries-cli/impact.ts +30 -22
  346. package/src/types.ts +195 -7
@@ -50,6 +50,111 @@ function taAdd(a: Float64Array, i: number, v: number): void {
50
50
  a[i] = taGet(a, i) + v;
51
51
  }
52
52
 
53
+ /**
54
+ * Populate edge arrays for a directed graph. Each edge is stored once in
55
+ * outEdges[from] and inEdges[to]. Self-loops are tracked in both the selfLoop
56
+ * array and the adjacency lists (partition.ts accounts for this).
57
+ */
58
+ function populateDirectedEdges(
59
+ graph: CodeGraph,
60
+ idToIndex: Map<string, number>,
61
+ linkWeight: (attrs: EdgeAttrs) => number,
62
+ selfLoop: Float64Array,
63
+ outEdges: EdgeEntry[][],
64
+ inEdges: InEdgeEntry[][],
65
+ strengthOut: Float64Array,
66
+ strengthIn: Float64Array,
67
+ ): void {
68
+ for (const [src, tgt, attrs] of graph.edges()) {
69
+ const from = idToIndex.get(src);
70
+ const to = idToIndex.get(tgt);
71
+ if (from == null || to == null) continue;
72
+ const w: number = +linkWeight(attrs) || 0;
73
+ if (from === to) {
74
+ taAdd(selfLoop, from, w);
75
+ // Self-loop is intentionally kept in outEdges/inEdges as well.
76
+ // partition.ts's moveNodeToCommunity (directed path) accounts for this
77
+ // by subtracting selfLoopWeight once from outToOld+inFromOld to avoid
78
+ // triple-counting (see partition.ts moveNodeToCommunity directed block).
79
+ }
80
+ (outEdges[from] as EdgeEntry[]).push({ to, w });
81
+ (inEdges[to] as InEdgeEntry[]).push({ from, w });
82
+ taAdd(strengthOut, from, w);
83
+ taAdd(strengthIn, to, w);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Populate edge arrays for an undirected graph. Reciprocal pairs are
89
+ * symmetrized and averaged to produce a single weight per undirected edge.
90
+ * Self-loops use single-w convention (matching modularity.ts formulas).
91
+ */
92
+ function populateUndirectedEdges(
93
+ graph: CodeGraph,
94
+ idToIndex: Map<string, number>,
95
+ linkWeight: (attrs: EdgeAttrs) => number,
96
+ n: number,
97
+ selfLoop: Float64Array,
98
+ outEdges: EdgeEntry[][],
99
+ inEdges: InEdgeEntry[][],
100
+ strengthOut: Float64Array,
101
+ strengthIn: Float64Array,
102
+ ): void {
103
+ const pairAgg = new Map<string, { sum: number; seenAB: number; seenBA: number }>();
104
+
105
+ for (const [src, tgt, attrs] of graph.edges()) {
106
+ const a = idToIndex.get(src);
107
+ const b = idToIndex.get(tgt);
108
+ if (a == null || b == null) continue;
109
+ const w: number = +linkWeight(attrs) || 0;
110
+ if (a === b) {
111
+ taAdd(selfLoop, a, w);
112
+ continue;
113
+ }
114
+ const i = a < b ? a : b;
115
+ const j = a < b ? b : a;
116
+ const key = `${i}:${j}`;
117
+ let rec = pairAgg.get(key);
118
+ if (!rec) {
119
+ rec = { sum: 0, seenAB: 0, seenBA: 0 };
120
+ pairAgg.set(key, rec);
121
+ }
122
+ rec.sum += w;
123
+ if (a === i) rec.seenAB = 1;
124
+ else rec.seenBA = 1;
125
+ }
126
+
127
+ for (const [key, rec] of pairAgg.entries()) {
128
+ const parts = key.split(':');
129
+ const i = +(parts[0] as string);
130
+ const j = +(parts[1] as string);
131
+ const dirCount: number = (rec.seenAB ? 1 : 0) + (rec.seenBA ? 1 : 0);
132
+ const w: number = dirCount > 0 ? rec.sum / dirCount : 0;
133
+ if (w === 0) continue;
134
+ (outEdges[i] as EdgeEntry[]).push({ to: j, w });
135
+ (outEdges[j] as EdgeEntry[]).push({ to: i, w });
136
+ (inEdges[i] as InEdgeEntry[]).push({ from: j, w });
137
+ (inEdges[j] as InEdgeEntry[]).push({ from: i, w });
138
+ taAdd(strengthOut, i, w);
139
+ taAdd(strengthOut, j, w);
140
+ taAdd(strengthIn, i, w);
141
+ taAdd(strengthIn, j, w);
142
+ }
143
+
144
+ // Add self-loops into adjacency and strengths.
145
+ // Note: uses single-w convention (not standard 2w) — the modularity formulas in
146
+ // modularity.ts are written to match this convention, keeping the system self-consistent.
147
+ for (let v = 0; v < n; v++) {
148
+ const w: number = taGet(selfLoop, v);
149
+ if (w !== 0) {
150
+ (outEdges[v] as EdgeEntry[]).push({ to: v, w });
151
+ (inEdges[v] as InEdgeEntry[]).push({ from: v, w });
152
+ taAdd(strengthOut, v, w);
153
+ taAdd(strengthIn, v, w);
154
+ }
155
+ }
156
+ }
157
+
53
158
  export function makeGraphAdapter(graph: CodeGraph, opts: GraphAdapterOptions = {}): GraphAdapter {
54
159
  const linkWeight: (attrs: EdgeAttrs) => number =
55
160
  opts.linkWeight || ((attrs) => (attrs && typeof attrs.weight === 'number' ? attrs.weight : 1));
@@ -92,78 +197,28 @@ export function makeGraphAdapter(graph: CodeGraph, opts: GraphAdapterOptions = {
92
197
 
93
198
  // Populate from graph
94
199
  if (directed) {
95
- for (const [src, tgt, attrs] of graph.edges()) {
96
- const from = idToIndex.get(src);
97
- const to = idToIndex.get(tgt);
98
- if (from == null || to == null) continue;
99
- const w: number = +linkWeight(attrs) || 0;
100
- if (from === to) {
101
- taAdd(selfLoop, from, w);
102
- // Self-loop is intentionally kept in outEdges/inEdges as well.
103
- // partition.ts's moveNodeToCommunity (directed path) accounts for this
104
- // by subtracting selfLoopWeight once from outToOld+inFromOld to avoid
105
- // triple-counting (see partition.ts moveNodeToCommunity directed block).
106
- }
107
- (outEdges[from] as EdgeEntry[]).push({ to, w });
108
- (inEdges[to] as InEdgeEntry[]).push({ from, w });
109
- taAdd(strengthOut, from, w);
110
- taAdd(strengthIn, to, w);
111
- }
200
+ populateDirectedEdges(
201
+ graph,
202
+ idToIndex,
203
+ linkWeight,
204
+ selfLoop,
205
+ outEdges,
206
+ inEdges,
207
+ strengthOut,
208
+ strengthIn,
209
+ );
112
210
  } else {
113
- // Undirected: symmetrize and average reciprocal pairs
114
- const pairAgg = new Map<string, { sum: number; seenAB: number; seenBA: number }>();
115
-
116
- for (const [src, tgt, attrs] of graph.edges()) {
117
- const a = idToIndex.get(src);
118
- const b = idToIndex.get(tgt);
119
- if (a == null || b == null) continue;
120
- const w: number = +linkWeight(attrs) || 0;
121
- if (a === b) {
122
- taAdd(selfLoop, a, w);
123
- continue;
124
- }
125
- const i = a < b ? a : b;
126
- const j = a < b ? b : a;
127
- const key = `${i}:${j}`;
128
- let rec = pairAgg.get(key);
129
- if (!rec) {
130
- rec = { sum: 0, seenAB: 0, seenBA: 0 };
131
- pairAgg.set(key, rec);
132
- }
133
- rec.sum += w;
134
- if (a === i) rec.seenAB = 1;
135
- else rec.seenBA = 1;
136
- }
137
-
138
- for (const [key, rec] of pairAgg.entries()) {
139
- const parts = key.split(':');
140
- const i = +(parts[0] as string);
141
- const j = +(parts[1] as string);
142
- const dirCount: number = (rec.seenAB ? 1 : 0) + (rec.seenBA ? 1 : 0);
143
- const w: number = dirCount > 0 ? rec.sum / dirCount : 0;
144
- if (w === 0) continue;
145
- (outEdges[i] as EdgeEntry[]).push({ to: j, w });
146
- (outEdges[j] as EdgeEntry[]).push({ to: i, w });
147
- (inEdges[i] as InEdgeEntry[]).push({ from: j, w });
148
- (inEdges[j] as InEdgeEntry[]).push({ from: i, w });
149
- taAdd(strengthOut, i, w);
150
- taAdd(strengthOut, j, w);
151
- taAdd(strengthIn, i, w);
152
- taAdd(strengthIn, j, w);
153
- }
154
-
155
- // Add self-loops into adjacency and strengths.
156
- // Note: uses single-w convention (not standard 2w) — the modularity formulas in
157
- // modularity.ts are written to match this convention, keeping the system self-consistent.
158
- for (let v = 0; v < n; v++) {
159
- const w: number = taGet(selfLoop, v);
160
- if (w !== 0) {
161
- (outEdges[v] as EdgeEntry[]).push({ to: v, w });
162
- (inEdges[v] as InEdgeEntry[]).push({ from: v, w });
163
- taAdd(strengthOut, v, w);
164
- taAdd(strengthIn, v, w);
165
- }
166
- }
211
+ populateUndirectedEdges(
212
+ graph,
213
+ idToIndex,
214
+ linkWeight,
215
+ n,
216
+ selfLoop,
217
+ outEdges,
218
+ inEdges,
219
+ strengthOut,
220
+ strengthIn,
221
+ );
167
222
  }
168
223
 
169
224
  // Node sizes
@@ -119,34 +119,17 @@ interface OriginalPartition {
119
119
  getInEdgeWeightFromCommunity(c: number): number;
120
120
  }
121
121
 
122
- function buildOriginalPartition(g: GraphAdapter, communityMap: Int32Array): OriginalPartition {
123
- const n: number = g.n;
124
- let maxC: number = 0;
125
- for (let i = 0; i < n; i++) {
126
- const ci = iget(communityMap, i);
127
- if (ci > maxC) maxC = ci;
128
- }
129
- const cc: number = maxC + 1;
130
-
131
- const nodeCommunity = communityMap;
132
- const internalWeight = new Float64Array(cc);
133
- const totalStr = new Float64Array(cc);
134
- const totalOutStr = new Float64Array(cc);
135
- const totalInStr = new Float64Array(cc);
136
- const totalSize = new Float64Array(cc);
137
-
138
- for (let i = 0; i < n; i++) {
139
- const c: number = iget(communityMap, i);
140
- totalSize[c] = fget(totalSize, c) + fget(g.size, i);
141
- if (g.directed) {
142
- totalOutStr[c] = fget(totalOutStr, c) + fget(g.strengthOut, i);
143
- totalInStr[c] = fget(totalInStr, c) + fget(g.strengthIn, i);
144
- } else {
145
- totalStr[c] = fget(totalStr, c) + fget(g.strengthOut, i);
146
- }
147
- if (fget(g.selfLoop, i)) internalWeight[c] = fget(internalWeight, c) + fget(g.selfLoop, i);
148
- }
149
-
122
+ /**
123
+ * Accumulate intra-community edge weights for quality evaluation.
124
+ * For directed graphs, counts all intra-community non-self edges.
125
+ * For undirected, counts each edge once (j > i) to avoid double-counting.
126
+ */
127
+ function accumulateInternalEdgeWeights(
128
+ g: GraphAdapter,
129
+ communityMap: Int32Array,
130
+ n: number,
131
+ internalWeight: Float64Array,
132
+ ): void {
150
133
  if (g.directed) {
151
134
  for (let i = 0; i < n; i++) {
152
135
  const ci: number = iget(communityMap, i);
@@ -168,6 +151,62 @@ function buildOriginalPartition(g: GraphAdapter, communityMap: Int32Array): Orig
168
151
  }
169
152
  }
170
153
  }
154
+ }
155
+
156
+ /**
157
+ * Accumulate per-community node-level aggregates (size, strength) from
158
+ * the graph adapter and community mapping.
159
+ */
160
+ function accumulateNodeAggregates(
161
+ g: GraphAdapter,
162
+ communityMap: Int32Array,
163
+ n: number,
164
+ totalSize: Float64Array,
165
+ totalStr: Float64Array,
166
+ totalOutStr: Float64Array,
167
+ totalInStr: Float64Array,
168
+ internalWeight: Float64Array,
169
+ ): void {
170
+ for (let i = 0; i < n; i++) {
171
+ const c: number = iget(communityMap, i);
172
+ totalSize[c] = fget(totalSize, c) + fget(g.size, i);
173
+ if (g.directed) {
174
+ totalOutStr[c] = fget(totalOutStr, c) + fget(g.strengthOut, i);
175
+ totalInStr[c] = fget(totalInStr, c) + fget(g.strengthIn, i);
176
+ } else {
177
+ totalStr[c] = fget(totalStr, c) + fget(g.strengthOut, i);
178
+ }
179
+ if (fget(g.selfLoop, i)) internalWeight[c] = fget(internalWeight, c) + fget(g.selfLoop, i);
180
+ }
181
+ }
182
+
183
+ function buildOriginalPartition(g: GraphAdapter, communityMap: Int32Array): OriginalPartition {
184
+ const n: number = g.n;
185
+ let maxC: number = 0;
186
+ for (let i = 0; i < n; i++) {
187
+ const ci = iget(communityMap, i);
188
+ if (ci > maxC) maxC = ci;
189
+ }
190
+ const cc: number = maxC + 1;
191
+
192
+ const nodeCommunity = communityMap;
193
+ const internalWeight = new Float64Array(cc);
194
+ const totalStr = new Float64Array(cc);
195
+ const totalOutStr = new Float64Array(cc);
196
+ const totalInStr = new Float64Array(cc);
197
+ const totalSize = new Float64Array(cc);
198
+
199
+ accumulateNodeAggregates(
200
+ g,
201
+ communityMap,
202
+ n,
203
+ totalSize,
204
+ totalStr,
205
+ totalOutStr,
206
+ totalInStr,
207
+ internalWeight,
208
+ );
209
+ accumulateInternalEdgeWeights(g, communityMap, n, internalWeight);
171
210
 
172
211
  return {
173
212
  communityCount: cc,
@@ -129,83 +129,15 @@ export function runLouvainUndirectedModularity(
129
129
  const nodeIndex: number = order[idx]!;
130
130
  if (level === 0 && fixedNodeMask && fixedNodeMask[nodeIndex]) continue;
131
131
  const candidateCount: number = partition.accumulateNeighborCommunityEdgeWeights(nodeIndex);
132
- let bestCommunityId: number = partition.nodeCommunity[nodeIndex]!;
133
- let bestGain: number = 0;
134
- const maxCommunitySize: number = options.maxCommunitySize;
135
- if (strategyCode === CandidateStrategy.All) {
136
- for (let communityId = 0; communityId < partition.communityCount; communityId++) {
137
- if (communityId === partition.nodeCommunity[nodeIndex]!) continue;
138
- if (
139
- maxCommunitySize < Infinity &&
140
- partition.getCommunityTotalSize(communityId) + graphAdapter.size[nodeIndex]! >
141
- maxCommunitySize
142
- )
143
- continue;
144
- const gain: number = computeQualityGain(partition, nodeIndex, communityId, options);
145
- if (gain > bestGain) {
146
- bestGain = gain;
147
- bestCommunityId = communityId;
148
- }
149
- }
150
- } else if (strategyCode === CandidateStrategy.RandomAny) {
151
- const tries: number = Math.min(10, Math.max(1, partition.communityCount));
152
- for (let trialIndex = 0; trialIndex < tries; trialIndex++) {
153
- const communityId: number = (random() * partition.communityCount) | 0;
154
- if (communityId === partition.nodeCommunity[nodeIndex]!) continue;
155
- if (
156
- maxCommunitySize < Infinity &&
157
- partition.getCommunityTotalSize(communityId) + graphAdapter.size[nodeIndex]! >
158
- maxCommunitySize
159
- )
160
- continue;
161
- const gain: number = computeQualityGain(partition, nodeIndex, communityId, options);
162
- if (gain > bestGain) {
163
- bestGain = gain;
164
- bestCommunityId = communityId;
165
- }
166
- }
167
- } else if (strategyCode === CandidateStrategy.RandomNeighbor) {
168
- const tries: number = Math.min(10, Math.max(1, candidateCount));
169
- for (let trialIndex = 0; trialIndex < tries; trialIndex++) {
170
- const communityId: number = partition.getCandidateCommunityAt(
171
- (random() * candidateCount) | 0,
172
- );
173
- if (communityId === partition.nodeCommunity[nodeIndex]!) continue;
174
- if (
175
- maxCommunitySize < Infinity &&
176
- partition.getCommunityTotalSize(communityId) + graphAdapter.size[nodeIndex]! >
177
- maxCommunitySize
178
- )
179
- continue;
180
- const gain: number = computeQualityGain(partition, nodeIndex, communityId, options);
181
- if (gain > bestGain) {
182
- bestGain = gain;
183
- bestCommunityId = communityId;
184
- }
185
- }
186
- } else {
187
- for (let trialIndex = 0; trialIndex < candidateCount; trialIndex++) {
188
- const communityId: number = partition.getCandidateCommunityAt(trialIndex);
189
- if (maxCommunitySize < Infinity) {
190
- const nextSize: number =
191
- partition.getCommunityTotalSize(communityId) + graphAdapter.size[nodeIndex]!;
192
- if (nextSize > maxCommunitySize) continue;
193
- }
194
- const gain: number = computeQualityGain(partition, nodeIndex, communityId, options);
195
- if (gain > bestGain) {
196
- bestGain = gain;
197
- bestCommunityId = communityId;
198
- }
199
- }
200
- }
201
- if (options.allowNewCommunity) {
202
- const newCommunityId: number = partition.communityCount;
203
- const gain: number = computeQualityGain(partition, nodeIndex, newCommunityId, options);
204
- if (gain > bestGain) {
205
- bestGain = gain;
206
- bestCommunityId = newCommunityId;
207
- }
208
- }
132
+ const { bestCommunityId, bestGain } = findBestCommunityMove(
133
+ partition,
134
+ graphAdapter,
135
+ nodeIndex,
136
+ candidateCount,
137
+ strategyCode,
138
+ options,
139
+ random,
140
+ );
209
141
  if (bestCommunityId !== partition.nodeCommunity[nodeIndex]! && bestGain > GAIN_EPSILON) {
210
142
  partition.moveNodeToCommunity(nodeIndex, bestCommunityId);
211
143
  improved = true;
@@ -267,6 +199,109 @@ export function runLouvainUndirectedModularity(
267
199
  };
268
200
  }
269
201
 
202
+ /**
203
+ * Evaluate all candidate communities for a node and return the best move.
204
+ * Encapsulates the four candidate-selection strategies (All, RandomAny,
205
+ * RandomNeighbor, Neighbors) and the optional new-community probe.
206
+ */
207
+ function findBestCommunityMove(
208
+ partition: Partition,
209
+ graphAdapter: GraphAdapter,
210
+ nodeIndex: number,
211
+ candidateCount: number,
212
+ strategyCode: CandidateStrategyCode,
213
+ options: NormalizedOptions,
214
+ random: () => number,
215
+ ): { bestCommunityId: number; bestGain: number } {
216
+ let bestCommunityId: number = partition.nodeCommunity[nodeIndex]!;
217
+ let bestGain: number = 0;
218
+ const maxCommunitySize: number = options.maxCommunitySize;
219
+
220
+ const evaluateCandidate = (communityId: number): void => {
221
+ if (communityId === partition.nodeCommunity[nodeIndex]!) return;
222
+ if (
223
+ maxCommunitySize < Infinity &&
224
+ partition.getCommunityTotalSize(communityId) + graphAdapter.size[nodeIndex]! >
225
+ maxCommunitySize
226
+ )
227
+ return;
228
+ const gain: number = computeQualityGain(partition, nodeIndex, communityId, options);
229
+ if (gain > bestGain) {
230
+ bestGain = gain;
231
+ bestCommunityId = communityId;
232
+ }
233
+ };
234
+
235
+ if (strategyCode === CandidateStrategy.All) {
236
+ for (let communityId = 0; communityId < partition.communityCount; communityId++) {
237
+ evaluateCandidate(communityId);
238
+ }
239
+ } else if (strategyCode === CandidateStrategy.RandomAny) {
240
+ const tries: number = Math.min(10, Math.max(1, partition.communityCount));
241
+ for (let trialIndex = 0; trialIndex < tries; trialIndex++) {
242
+ evaluateCandidate((random() * partition.communityCount) | 0);
243
+ }
244
+ } else if (strategyCode === CandidateStrategy.RandomNeighbor) {
245
+ const tries: number = Math.min(10, Math.max(1, candidateCount));
246
+ for (let trialIndex = 0; trialIndex < tries; trialIndex++) {
247
+ evaluateCandidate(partition.getCandidateCommunityAt((random() * candidateCount) | 0));
248
+ }
249
+ } else {
250
+ for (let trialIndex = 0; trialIndex < candidateCount; trialIndex++) {
251
+ evaluateCandidate(partition.getCandidateCommunityAt(trialIndex));
252
+ }
253
+ }
254
+
255
+ if (options.allowNewCommunity) {
256
+ const newCommunityId: number = partition.communityCount;
257
+ const gain: number = computeQualityGain(partition, nodeIndex, newCommunityId, options);
258
+ if (gain > bestGain) {
259
+ bestGain = gain;
260
+ bestCommunityId = newCommunityId;
261
+ }
262
+ }
263
+
264
+ return { bestCommunityId, bestGain };
265
+ }
266
+
267
+ /**
268
+ * Run a BFS on the subgraph induced by `inCommunity` starting from `start`.
269
+ * Returns the list of visited nodes. Works on both directed (weak connectivity
270
+ * via both outEdges and inEdges) and undirected graphs.
271
+ */
272
+ function bfsComponent(
273
+ g: GraphAdapter,
274
+ start: number,
275
+ inCommunity: Uint8Array,
276
+ visited: Uint8Array,
277
+ ): number[] {
278
+ const queue: number[] = [start];
279
+ visited[start] = 1;
280
+ let head: number = 0;
281
+ while (head < queue.length) {
282
+ const v: number = queue[head++]!;
283
+ const out: EdgeEntry[] = g.outEdges[v]!;
284
+ for (let k = 0; k < out.length; k++) {
285
+ const w: number = out[k]!.to;
286
+ if (inCommunity[w] && !visited[w]) {
287
+ visited[w] = 1;
288
+ queue.push(w);
289
+ }
290
+ }
291
+ if (g.directed) {
292
+ const inc: InEdgeEntry[] = g.inEdges[v]!;
293
+ for (let k = 0; k < inc.length; k++) {
294
+ const w: number = inc[k]!.from;
295
+ if (inCommunity[w] && !visited[w]) {
296
+ visited[w] = 1;
297
+ queue.push(w);
298
+ }
299
+ }
300
+ }
301
+ }
302
+ return queue;
303
+ }
304
+
270
305
  // Build a coarse graph where each community becomes a single node.
271
306
  // Self-loops (g.selfLoop[]) don't need separate handling here because they
272
307
  // are already present in g.outEdges (directed path keeps them in both arrays).
@@ -450,38 +485,12 @@ function splitDisconnectedCommunities(g: GraphAdapter, partition: Partition): vo
450
485
  if (visited[start]) continue;
451
486
  componentCount++;
452
487
 
453
- // BFS within the community subgraph.
454
- // For directed graphs, traverse both outEdges and inEdges to check
455
- // weak connectivity (reachability ignoring edge direction).
456
- const queue: number[] = [start];
457
- visited[start] = 1;
458
- let head: number = 0;
459
- while (head < queue.length) {
460
- const v: number = queue[head++]!;
461
- const out: EdgeEntry[] = g.outEdges[v]!;
462
- for (let k = 0; k < out.length; k++) {
463
- const w: number = out[k]!.to;
464
- if (inCommunity[w] && !visited[w]) {
465
- visited[w] = 1;
466
- queue.push(w);
467
- }
468
- }
469
- if (g.directed) {
470
- const inc: InEdgeEntry[] = g.inEdges[v]!;
471
- for (let k = 0; k < inc.length; k++) {
472
- const w: number = inc[k]!.from;
473
- if (inCommunity[w] && !visited[w]) {
474
- visited[w] = 1;
475
- queue.push(w);
476
- }
477
- }
478
- }
479
- }
488
+ const component: number[] = bfsComponent(g, start, inCommunity, visited);
480
489
 
481
490
  if (componentCount > 1) {
482
491
  // Secondary component — assign new community ID directly.
483
492
  const newC: number = nextC++;
484
- for (let q = 0; q < queue.length; q++) nc[queue[q]!] = newC;
493
+ for (let q = 0; q < component.length; q++) nc[component[q]!] = newC;
485
494
  didSplit = true;
486
495
  }
487
496
  }