@optave/codegraph 3.4.1 → 3.6.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 (349) hide show
  1. package/README.md +50 -28
  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/rules/javascript.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/javascript.js +1 -0
  7. package/dist/ast-analysis/rules/javascript.js.map +1 -1
  8. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.js +116 -35
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  11. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  12. package/dist/ast-analysis/visitors/complexity-visitor.js +11 -13
  13. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  14. package/dist/db/better-sqlite3.d.ts +3 -0
  15. package/dist/db/better-sqlite3.d.ts.map +1 -0
  16. package/dist/db/better-sqlite3.js +19 -0
  17. package/dist/db/better-sqlite3.js.map +1 -0
  18. package/dist/db/connection.d.ts +25 -4
  19. package/dist/db/connection.d.ts.map +1 -1
  20. package/dist/db/connection.js +125 -23
  21. package/dist/db/connection.js.map +1 -1
  22. package/dist/db/index.d.ts +2 -2
  23. package/dist/db/index.d.ts.map +1 -1
  24. package/dist/db/index.js +1 -1
  25. package/dist/db/index.js.map +1 -1
  26. package/dist/db/migrations.d.ts.map +1 -1
  27. package/dist/db/migrations.js +40 -32
  28. package/dist/db/migrations.js.map +1 -1
  29. package/dist/db/query-builder.d.ts +5 -5
  30. package/dist/db/query-builder.d.ts.map +1 -1
  31. package/dist/db/query-builder.js +20 -4
  32. package/dist/db/query-builder.js.map +1 -1
  33. package/dist/db/repository/index.d.ts +1 -0
  34. package/dist/db/repository/index.d.ts.map +1 -1
  35. package/dist/db/repository/index.js +1 -0
  36. package/dist/db/repository/index.js.map +1 -1
  37. package/dist/db/repository/native-repository.d.ts +58 -0
  38. package/dist/db/repository/native-repository.d.ts.map +1 -0
  39. package/dist/db/repository/native-repository.js +261 -0
  40. package/dist/db/repository/native-repository.js.map +1 -0
  41. package/dist/db/repository/nodes.d.ts +4 -4
  42. package/dist/db/repository/nodes.d.ts.map +1 -1
  43. package/dist/db/repository/nodes.js +6 -6
  44. package/dist/db/repository/nodes.js.map +1 -1
  45. package/dist/domain/analysis/context.d.ts.map +1 -1
  46. package/dist/domain/analysis/context.js +51 -66
  47. package/dist/domain/analysis/context.js.map +1 -1
  48. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  49. package/dist/domain/analysis/dependencies.js +62 -70
  50. package/dist/domain/analysis/dependencies.js.map +1 -1
  51. package/dist/domain/analysis/diff-impact.d.ts +9 -7
  52. package/dist/domain/analysis/diff-impact.d.ts.map +1 -1
  53. package/dist/domain/analysis/exports.d.ts.map +1 -1
  54. package/dist/domain/analysis/exports.js +29 -33
  55. package/dist/domain/analysis/exports.js.map +1 -1
  56. package/dist/domain/analysis/fn-impact.d.ts +15 -17
  57. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  58. package/dist/domain/analysis/fn-impact.js +35 -65
  59. package/dist/domain/analysis/fn-impact.js.map +1 -1
  60. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  61. package/dist/domain/analysis/module-map.js +91 -6
  62. package/dist/domain/analysis/module-map.js.map +1 -1
  63. package/dist/domain/analysis/query-helpers.d.ts +20 -0
  64. package/dist/domain/analysis/query-helpers.d.ts.map +1 -0
  65. package/dist/domain/analysis/query-helpers.js +27 -0
  66. package/dist/domain/analysis/query-helpers.js.map +1 -0
  67. package/dist/domain/graph/builder/context.d.ts +2 -1
  68. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  69. package/dist/domain/graph/builder/context.js +1 -0
  70. package/dist/domain/graph/builder/context.js.map +1 -1
  71. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  72. package/dist/domain/graph/builder/helpers.js +15 -9
  73. package/dist/domain/graph/builder/helpers.js.map +1 -1
  74. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  75. package/dist/domain/graph/builder/incremental.js +3 -2
  76. package/dist/domain/graph/builder/incremental.js.map +1 -1
  77. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  78. package/dist/domain/graph/builder/pipeline.js +95 -7
  79. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  80. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  81. package/dist/domain/graph/builder/stages/build-edges.js +101 -57
  82. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  83. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  84. package/dist/domain/graph/builder/stages/build-structure.js +33 -3
  85. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  86. package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/collect-files.js +70 -6
  88. package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
  89. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  90. package/dist/domain/graph/builder/stages/detect-changes.js +36 -14
  91. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  92. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  93. package/dist/domain/graph/builder/stages/finalize.js +130 -88
  94. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  95. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  96. package/dist/domain/graph/builder/stages/insert-nodes.js +124 -16
  97. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  98. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  99. package/dist/domain/graph/builder/stages/resolve-imports.js +3 -2
  100. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  101. package/dist/domain/graph/resolve.d.ts +0 -4
  102. package/dist/domain/graph/resolve.d.ts.map +1 -1
  103. package/dist/domain/graph/resolve.js +32 -48
  104. package/dist/domain/graph/resolve.js.map +1 -1
  105. package/dist/domain/graph/watcher.d.ts.map +1 -1
  106. package/dist/domain/graph/watcher.js +12 -12
  107. package/dist/domain/graph/watcher.js.map +1 -1
  108. package/dist/domain/parser.d.ts +1 -1
  109. package/dist/domain/parser.d.ts.map +1 -1
  110. package/dist/domain/parser.js +165 -101
  111. package/dist/domain/parser.js.map +1 -1
  112. package/dist/domain/search/search/cli-formatter.d.ts.map +1 -1
  113. package/dist/domain/search/search/cli-formatter.js +88 -83
  114. package/dist/domain/search/search/cli-formatter.js.map +1 -1
  115. package/dist/extractors/bash.d.ts +6 -0
  116. package/dist/extractors/bash.d.ts.map +1 -0
  117. package/dist/extractors/bash.js +91 -0
  118. package/dist/extractors/bash.js.map +1 -0
  119. package/dist/extractors/c.d.ts +6 -0
  120. package/dist/extractors/c.d.ts.map +1 -0
  121. package/dist/extractors/c.js +204 -0
  122. package/dist/extractors/c.js.map +1 -0
  123. package/dist/extractors/cpp.d.ts +6 -0
  124. package/dist/extractors/cpp.d.ts.map +1 -0
  125. package/dist/extractors/cpp.js +283 -0
  126. package/dist/extractors/cpp.js.map +1 -0
  127. package/dist/extractors/csharp.d.ts.map +1 -1
  128. package/dist/extractors/csharp.js +42 -54
  129. package/dist/extractors/csharp.js.map +1 -1
  130. package/dist/extractors/go.d.ts.map +1 -1
  131. package/dist/extractors/go.js +126 -130
  132. package/dist/extractors/go.js.map +1 -1
  133. package/dist/extractors/hcl.js +6 -6
  134. package/dist/extractors/hcl.js.map +1 -1
  135. package/dist/extractors/helpers.d.ts +32 -1
  136. package/dist/extractors/helpers.d.ts.map +1 -1
  137. package/dist/extractors/helpers.js +74 -0
  138. package/dist/extractors/helpers.js.map +1 -1
  139. package/dist/extractors/index.d.ts +6 -0
  140. package/dist/extractors/index.d.ts.map +1 -1
  141. package/dist/extractors/index.js +6 -0
  142. package/dist/extractors/index.js.map +1 -1
  143. package/dist/extractors/java.d.ts.map +1 -1
  144. package/dist/extractors/java.js +32 -47
  145. package/dist/extractors/java.js.map +1 -1
  146. package/dist/extractors/javascript.d.ts.map +1 -1
  147. package/dist/extractors/javascript.js +359 -330
  148. package/dist/extractors/javascript.js.map +1 -1
  149. package/dist/extractors/kotlin.d.ts +6 -0
  150. package/dist/extractors/kotlin.d.ts.map +1 -0
  151. package/dist/extractors/kotlin.js +275 -0
  152. package/dist/extractors/kotlin.js.map +1 -0
  153. package/dist/extractors/php.d.ts.map +1 -1
  154. package/dist/extractors/php.js +39 -44
  155. package/dist/extractors/php.js.map +1 -1
  156. package/dist/extractors/python.d.ts.map +1 -1
  157. package/dist/extractors/python.js +75 -93
  158. package/dist/extractors/python.js.map +1 -1
  159. package/dist/extractors/ruby.js +6 -13
  160. package/dist/extractors/ruby.js.map +1 -1
  161. package/dist/extractors/rust.d.ts.map +1 -1
  162. package/dist/extractors/rust.js +58 -82
  163. package/dist/extractors/rust.js.map +1 -1
  164. package/dist/extractors/scala.d.ts +6 -0
  165. package/dist/extractors/scala.d.ts.map +1 -0
  166. package/dist/extractors/scala.js +269 -0
  167. package/dist/extractors/scala.js.map +1 -0
  168. package/dist/extractors/swift.d.ts +6 -0
  169. package/dist/extractors/swift.d.ts.map +1 -0
  170. package/dist/extractors/swift.js +275 -0
  171. package/dist/extractors/swift.js.map +1 -0
  172. package/dist/features/ast.d.ts +16 -1
  173. package/dist/features/ast.d.ts.map +1 -1
  174. package/dist/features/ast.js +45 -23
  175. package/dist/features/ast.js.map +1 -1
  176. package/dist/features/audit.d.ts.map +1 -1
  177. package/dist/features/audit.js +17 -21
  178. package/dist/features/audit.js.map +1 -1
  179. package/dist/features/branch-compare.d.ts.map +1 -1
  180. package/dist/features/branch-compare.js +50 -4
  181. package/dist/features/branch-compare.js.map +1 -1
  182. package/dist/features/cfg.d.ts +7 -1
  183. package/dist/features/cfg.d.ts.map +1 -1
  184. package/dist/features/cfg.js +118 -62
  185. package/dist/features/cfg.js.map +1 -1
  186. package/dist/features/check.d.ts.map +1 -1
  187. package/dist/features/check.js +79 -62
  188. package/dist/features/check.js.map +1 -1
  189. package/dist/features/complexity-query.d.ts.map +1 -1
  190. package/dist/features/complexity-query.js +142 -137
  191. package/dist/features/complexity-query.js.map +1 -1
  192. package/dist/features/complexity.d.ts +7 -1
  193. package/dist/features/complexity.d.ts.map +1 -1
  194. package/dist/features/complexity.js +62 -1
  195. package/dist/features/complexity.js.map +1 -1
  196. package/dist/features/dataflow.d.ts +7 -1
  197. package/dist/features/dataflow.d.ts.map +1 -1
  198. package/dist/features/dataflow.js +356 -188
  199. package/dist/features/dataflow.js.map +1 -1
  200. package/dist/features/graph-enrichment.d.ts.map +1 -1
  201. package/dist/features/graph-enrichment.js +117 -104
  202. package/dist/features/graph-enrichment.js.map +1 -1
  203. package/dist/features/sequence.d.ts.map +1 -1
  204. package/dist/features/sequence.js +25 -4
  205. package/dist/features/sequence.js.map +1 -1
  206. package/dist/features/snapshot.d.ts.map +1 -1
  207. package/dist/features/snapshot.js +2 -1
  208. package/dist/features/snapshot.js.map +1 -1
  209. package/dist/features/structure-query.d.ts.map +1 -1
  210. package/dist/features/structure-query.js +29 -4
  211. package/dist/features/structure-query.js.map +1 -1
  212. package/dist/features/structure.d.ts.map +1 -1
  213. package/dist/features/structure.js +35 -15
  214. package/dist/features/structure.js.map +1 -1
  215. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  216. package/dist/graph/algorithms/leiden/adapter.js +88 -73
  217. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  218. package/dist/graph/algorithms/leiden/index.js +43 -28
  219. package/dist/graph/algorithms/leiden/index.js.map +1 -1
  220. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  221. package/dist/graph/algorithms/leiden/optimiser.js +90 -104
  222. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  223. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  224. package/dist/graph/algorithms/leiden/partition.js +89 -106
  225. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  226. package/dist/graph/model.d.ts +2 -0
  227. package/dist/graph/model.d.ts.map +1 -1
  228. package/dist/graph/model.js +20 -8
  229. package/dist/graph/model.js.map +1 -1
  230. package/dist/infrastructure/config.d.ts +0 -8
  231. package/dist/infrastructure/config.d.ts.map +1 -1
  232. package/dist/infrastructure/config.js +73 -62
  233. package/dist/infrastructure/config.js.map +1 -1
  234. package/dist/infrastructure/registry.d.ts +0 -8
  235. package/dist/infrastructure/registry.d.ts.map +1 -1
  236. package/dist/infrastructure/registry.js +12 -14
  237. package/dist/infrastructure/registry.js.map +1 -1
  238. package/dist/mcp/server.d.ts.map +1 -1
  239. package/dist/mcp/server.js +47 -45
  240. package/dist/mcp/server.js.map +1 -1
  241. package/dist/presentation/audit.d.ts.map +1 -1
  242. package/dist/presentation/audit.js +61 -57
  243. package/dist/presentation/audit.js.map +1 -1
  244. package/dist/presentation/branch-compare.d.ts.map +1 -1
  245. package/dist/presentation/branch-compare.js +56 -38
  246. package/dist/presentation/branch-compare.js.map +1 -1
  247. package/dist/presentation/check.d.ts.map +1 -1
  248. package/dist/presentation/check.js +30 -32
  249. package/dist/presentation/check.js.map +1 -1
  250. package/dist/presentation/colors.d.ts.map +1 -1
  251. package/dist/presentation/colors.js +2 -0
  252. package/dist/presentation/colors.js.map +1 -1
  253. package/dist/presentation/complexity.d.ts.map +1 -1
  254. package/dist/presentation/complexity.js +25 -19
  255. package/dist/presentation/complexity.js.map +1 -1
  256. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  257. package/dist/presentation/queries-cli/exports.js +15 -15
  258. package/dist/presentation/queries-cli/exports.js.map +1 -1
  259. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  260. package/dist/presentation/queries-cli/impact.js +29 -19
  261. package/dist/presentation/queries-cli/impact.js.map +1 -1
  262. package/dist/types.d.ts +406 -3
  263. package/dist/types.d.ts.map +1 -1
  264. package/grammars/tree-sitter-bash.wasm +0 -0
  265. package/grammars/tree-sitter-c.wasm +0 -0
  266. package/grammars/tree-sitter-cpp.wasm +0 -0
  267. package/grammars/tree-sitter-kotlin.wasm +0 -0
  268. package/grammars/tree-sitter-scala.wasm +0 -0
  269. package/grammars/tree-sitter-swift.wasm +0 -0
  270. package/package.json +67 -11
  271. package/src/ast-analysis/engine.ts +147 -138
  272. package/src/ast-analysis/rules/javascript.ts +1 -0
  273. package/src/ast-analysis/visitors/ast-store-visitor.ts +116 -34
  274. package/src/ast-analysis/visitors/complexity-visitor.ts +11 -11
  275. package/src/db/better-sqlite3.ts +20 -0
  276. package/src/db/connection.ts +148 -26
  277. package/src/db/index.ts +4 -1
  278. package/src/db/migrations.ts +38 -32
  279. package/src/db/query-builder.ts +30 -5
  280. package/src/db/repository/index.ts +1 -0
  281. package/src/db/repository/native-repository.ts +361 -0
  282. package/src/db/repository/nodes.ts +7 -3
  283. package/src/domain/analysis/context.ts +73 -75
  284. package/src/domain/analysis/dependencies.ts +78 -68
  285. package/src/domain/analysis/exports.ts +45 -34
  286. package/src/domain/analysis/fn-impact.ts +67 -64
  287. package/src/domain/analysis/module-map.ts +103 -8
  288. package/src/domain/analysis/query-helpers.ts +35 -0
  289. package/src/domain/graph/builder/context.ts +2 -0
  290. package/src/domain/graph/builder/helpers.ts +12 -6
  291. package/src/domain/graph/builder/incremental.ts +3 -2
  292. package/src/domain/graph/builder/pipeline.ts +98 -6
  293. package/src/domain/graph/builder/stages/build-edges.ts +116 -83
  294. package/src/domain/graph/builder/stages/build-structure.ts +46 -8
  295. package/src/domain/graph/builder/stages/collect-files.ts +83 -6
  296. package/src/domain/graph/builder/stages/detect-changes.ts +44 -21
  297. package/src/domain/graph/builder/stages/finalize.ts +172 -109
  298. package/src/domain/graph/builder/stages/insert-nodes.ts +147 -17
  299. package/src/domain/graph/builder/stages/resolve-imports.ts +3 -2
  300. package/src/domain/graph/resolve.ts +34 -46
  301. package/src/domain/graph/watcher.ts +12 -14
  302. package/src/domain/parser.ts +169 -97
  303. package/src/domain/search/search/cli-formatter.ts +121 -94
  304. package/src/extractors/bash.ts +97 -0
  305. package/src/extractors/c.ts +212 -0
  306. package/src/extractors/cpp.ts +298 -0
  307. package/src/extractors/csharp.ts +53 -56
  308. package/src/extractors/go.ts +152 -134
  309. package/src/extractors/hcl.ts +6 -6
  310. package/src/extractors/helpers.ts +93 -1
  311. package/src/extractors/index.ts +6 -0
  312. package/src/extractors/java.ts +43 -48
  313. package/src/extractors/javascript.ts +382 -317
  314. package/src/extractors/kotlin.ts +293 -0
  315. package/src/extractors/php.ts +46 -40
  316. package/src/extractors/python.ts +81 -104
  317. package/src/extractors/ruby.ts +6 -13
  318. package/src/extractors/rust.ts +65 -84
  319. package/src/extractors/scala.ts +285 -0
  320. package/src/extractors/swift.ts +293 -0
  321. package/src/features/ast.ts +74 -24
  322. package/src/features/audit.ts +24 -20
  323. package/src/features/branch-compare.ts +54 -5
  324. package/src/features/cfg.ts +158 -65
  325. package/src/features/check.ts +90 -74
  326. package/src/features/complexity-query.ts +181 -163
  327. package/src/features/complexity.ts +64 -1
  328. package/src/features/dataflow.ts +462 -217
  329. package/src/features/graph-enrichment.ts +161 -117
  330. package/src/features/sequence.ts +27 -4
  331. package/src/features/snapshot.ts +2 -1
  332. package/src/features/structure-query.ts +43 -4
  333. package/src/features/structure.ts +50 -22
  334. package/src/graph/algorithms/leiden/adapter.ts +126 -71
  335. package/src/graph/algorithms/leiden/index.ts +67 -28
  336. package/src/graph/algorithms/leiden/optimiser.ts +114 -105
  337. package/src/graph/algorithms/leiden/partition.ts +131 -98
  338. package/src/graph/model.ts +19 -7
  339. package/src/infrastructure/config.ts +60 -58
  340. package/src/infrastructure/registry.ts +17 -14
  341. package/src/mcp/server.ts +48 -47
  342. package/src/presentation/audit.ts +72 -67
  343. package/src/presentation/branch-compare.ts +54 -50
  344. package/src/presentation/check.ts +34 -34
  345. package/src/presentation/colors.ts +2 -0
  346. package/src/presentation/complexity.ts +39 -33
  347. package/src/presentation/queries-cli/exports.ts +17 -17
  348. package/src/presentation/queries-cli/impact.ts +30 -22
  349. package/src/types.ts +458 -3
@@ -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
  }