@optave/codegraph 3.10.0 → 3.11.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 (312) hide show
  1. package/README.md +40 -33
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +91 -60
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/index.js +77 -0
  7. package/dist/ast-analysis/rules/index.js.map +1 -1
  8. package/dist/ast-analysis/visitor-utils.d.ts +3 -0
  9. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitor-utils.js +83 -49
  11. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  12. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  14. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  15. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  17. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  18. package/dist/cli/commands/audit.js +1 -1
  19. package/dist/cli/commands/audit.js.map +1 -1
  20. package/dist/cli/commands/build.d.ts.map +1 -1
  21. package/dist/cli/commands/build.js +2 -0
  22. package/dist/cli/commands/build.js.map +1 -1
  23. package/dist/cli/commands/check.js +1 -1
  24. package/dist/cli/commands/check.js.map +1 -1
  25. package/dist/cli/commands/children.js +1 -1
  26. package/dist/cli/commands/children.js.map +1 -1
  27. package/dist/cli/commands/diff-impact.js +1 -1
  28. package/dist/cli/commands/diff-impact.js.map +1 -1
  29. package/dist/cli/commands/embed.d.ts.map +1 -1
  30. package/dist/cli/commands/embed.js +49 -4
  31. package/dist/cli/commands/embed.js.map +1 -1
  32. package/dist/cli/commands/roles.js +1 -1
  33. package/dist/cli/commands/roles.js.map +1 -1
  34. package/dist/cli/commands/structure.js +1 -1
  35. package/dist/cli/commands/structure.js.map +1 -1
  36. package/dist/cli/shared/options.js +1 -1
  37. package/dist/cli/shared/options.js.map +1 -1
  38. package/dist/db/connection.d.ts.map +1 -1
  39. package/dist/db/connection.js +8 -0
  40. package/dist/db/connection.js.map +1 -1
  41. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  42. package/dist/domain/analysis/dependencies.js +106 -80
  43. package/dist/domain/analysis/dependencies.js.map +1 -1
  44. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  45. package/dist/domain/analysis/fn-impact.js +77 -52
  46. package/dist/domain/analysis/fn-impact.js.map +1 -1
  47. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  48. package/dist/domain/analysis/module-map.js +132 -121
  49. package/dist/domain/analysis/module-map.js.map +1 -1
  50. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  51. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  52. package/dist/domain/graph/builder/helpers.js +47 -33
  53. package/dist/domain/graph/builder/helpers.js.map +1 -1
  54. package/dist/domain/graph/builder/incremental.d.ts +6 -6
  55. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/incremental.js +148 -99
  57. package/dist/domain/graph/builder/incremental.js.map +1 -1
  58. package/dist/domain/graph/builder/pipeline.d.ts +1 -0
  59. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  60. package/dist/domain/graph/builder/pipeline.js +23 -637
  61. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  62. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  63. package/dist/domain/graph/builder/stages/build-edges.js +141 -98
  64. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  65. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  66. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  67. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  68. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  69. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  70. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  71. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  72. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  73. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  74. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  75. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  76. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  77. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  78. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  79. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  80. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  81. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  82. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  83. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  84. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  85. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  86. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
  88. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  89. package/dist/domain/graph/cycles.d.ts +6 -4
  90. package/dist/domain/graph/cycles.d.ts.map +1 -1
  91. package/dist/domain/graph/cycles.js +50 -55
  92. package/dist/domain/graph/cycles.js.map +1 -1
  93. package/dist/domain/graph/journal.d.ts.map +1 -1
  94. package/dist/domain/graph/journal.js +89 -70
  95. package/dist/domain/graph/journal.js.map +1 -1
  96. package/dist/domain/graph/watcher.d.ts.map +1 -1
  97. package/dist/domain/graph/watcher.js +28 -20
  98. package/dist/domain/graph/watcher.js.map +1 -1
  99. package/dist/domain/parser.d.ts +12 -23
  100. package/dist/domain/parser.d.ts.map +1 -1
  101. package/dist/domain/parser.js +153 -80
  102. package/dist/domain/parser.js.map +1 -1
  103. package/dist/domain/search/generator.d.ts +3 -1
  104. package/dist/domain/search/generator.d.ts.map +1 -1
  105. package/dist/domain/search/generator.js +68 -45
  106. package/dist/domain/search/generator.js.map +1 -1
  107. package/dist/domain/search/models.d.ts +18 -0
  108. package/dist/domain/search/models.d.ts.map +1 -1
  109. package/dist/domain/search/models.js +72 -4
  110. package/dist/domain/search/models.js.map +1 -1
  111. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  112. package/dist/domain/search/search/hybrid.js +49 -40
  113. package/dist/domain/search/search/hybrid.js.map +1 -1
  114. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  115. package/dist/domain/search/search/semantic.js +69 -49
  116. package/dist/domain/search/search/semantic.js.map +1 -1
  117. package/dist/domain/wasm-worker-entry.js +209 -137
  118. package/dist/domain/wasm-worker-entry.js.map +1 -1
  119. package/dist/extractors/c.js +25 -6
  120. package/dist/extractors/c.js.map +1 -1
  121. package/dist/extractors/cpp.js +47 -6
  122. package/dist/extractors/cpp.js.map +1 -1
  123. package/dist/extractors/cuda.js +90 -14
  124. package/dist/extractors/cuda.js.map +1 -1
  125. package/dist/extractors/elixir.js +108 -4
  126. package/dist/extractors/elixir.js.map +1 -1
  127. package/dist/extractors/erlang.js +56 -20
  128. package/dist/extractors/erlang.js.map +1 -1
  129. package/dist/extractors/fsharp.d.ts +7 -0
  130. package/dist/extractors/fsharp.d.ts.map +1 -1
  131. package/dist/extractors/fsharp.js +94 -0
  132. package/dist/extractors/fsharp.js.map +1 -1
  133. package/dist/extractors/gleam.d.ts.map +1 -1
  134. package/dist/extractors/gleam.js +29 -33
  135. package/dist/extractors/gleam.js.map +1 -1
  136. package/dist/extractors/groovy.js +41 -1
  137. package/dist/extractors/groovy.js.map +1 -1
  138. package/dist/extractors/haskell.js +48 -4
  139. package/dist/extractors/haskell.js.map +1 -1
  140. package/dist/extractors/helpers.d.ts +79 -1
  141. package/dist/extractors/helpers.d.ts.map +1 -1
  142. package/dist/extractors/helpers.js +137 -0
  143. package/dist/extractors/helpers.js.map +1 -1
  144. package/dist/extractors/java.d.ts.map +1 -1
  145. package/dist/extractors/java.js +37 -49
  146. package/dist/extractors/java.js.map +1 -1
  147. package/dist/extractors/javascript.d.ts.map +1 -1
  148. package/dist/extractors/javascript.js +44 -44
  149. package/dist/extractors/javascript.js.map +1 -1
  150. package/dist/extractors/julia.js +198 -74
  151. package/dist/extractors/julia.js.map +1 -1
  152. package/dist/extractors/kotlin.js +4 -0
  153. package/dist/extractors/kotlin.js.map +1 -1
  154. package/dist/extractors/objc.js +184 -47
  155. package/dist/extractors/objc.js.map +1 -1
  156. package/dist/extractors/python.js +7 -4
  157. package/dist/extractors/python.js.map +1 -1
  158. package/dist/extractors/r.d.ts.map +1 -1
  159. package/dist/extractors/r.js +103 -87
  160. package/dist/extractors/r.js.map +1 -1
  161. package/dist/extractors/scala.d.ts.map +1 -1
  162. package/dist/extractors/scala.js +18 -32
  163. package/dist/extractors/scala.js.map +1 -1
  164. package/dist/extractors/solidity.d.ts.map +1 -1
  165. package/dist/extractors/solidity.js +55 -69
  166. package/dist/extractors/solidity.js.map +1 -1
  167. package/dist/extractors/verilog.js +80 -15
  168. package/dist/extractors/verilog.js.map +1 -1
  169. package/dist/features/boundaries.d.ts.map +1 -1
  170. package/dist/features/boundaries.js +49 -39
  171. package/dist/features/boundaries.js.map +1 -1
  172. package/dist/features/cfg.d.ts.map +1 -1
  173. package/dist/features/cfg.js +90 -63
  174. package/dist/features/cfg.js.map +1 -1
  175. package/dist/features/check.d.ts.map +1 -1
  176. package/dist/features/check.js +43 -34
  177. package/dist/features/check.js.map +1 -1
  178. package/dist/features/cochange.d.ts.map +1 -1
  179. package/dist/features/cochange.js +68 -56
  180. package/dist/features/cochange.js.map +1 -1
  181. package/dist/features/complexity.d.ts.map +1 -1
  182. package/dist/features/complexity.js +105 -75
  183. package/dist/features/complexity.js.map +1 -1
  184. package/dist/features/dataflow.d.ts.map +1 -1
  185. package/dist/features/dataflow.js +37 -29
  186. package/dist/features/dataflow.js.map +1 -1
  187. package/dist/features/flow.d.ts.map +1 -1
  188. package/dist/features/flow.js +31 -22
  189. package/dist/features/flow.js.map +1 -1
  190. package/dist/features/graph-enrichment.d.ts.map +1 -1
  191. package/dist/features/graph-enrichment.js +77 -70
  192. package/dist/features/graph-enrichment.js.map +1 -1
  193. package/dist/features/owners.d.ts +17 -26
  194. package/dist/features/owners.d.ts.map +1 -1
  195. package/dist/features/owners.js +120 -109
  196. package/dist/features/owners.js.map +1 -1
  197. package/dist/features/sequence.d.ts.map +1 -1
  198. package/dist/features/sequence.js +59 -54
  199. package/dist/features/sequence.js.map +1 -1
  200. package/dist/features/structure-query.d.ts.map +1 -1
  201. package/dist/features/structure-query.js +60 -60
  202. package/dist/features/structure-query.js.map +1 -1
  203. package/dist/features/structure.js +28 -36
  204. package/dist/features/structure.js.map +1 -1
  205. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  206. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  207. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  208. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  209. package/dist/graph/classifiers/roles.js +63 -59
  210. package/dist/graph/classifiers/roles.js.map +1 -1
  211. package/dist/infrastructure/config.d.ts +1 -1
  212. package/dist/infrastructure/config.d.ts.map +1 -1
  213. package/dist/infrastructure/config.js +1 -1
  214. package/dist/infrastructure/config.js.map +1 -1
  215. package/dist/mcp/tool-registry.d.ts.map +1 -1
  216. package/dist/mcp/tool-registry.js +4 -0
  217. package/dist/mcp/tool-registry.js.map +1 -1
  218. package/dist/mcp/tools/semantic-search.d.ts +1 -0
  219. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  220. package/dist/mcp/tools/semantic-search.js +1 -0
  221. package/dist/mcp/tools/semantic-search.js.map +1 -1
  222. package/dist/presentation/cfg.d.ts.map +1 -1
  223. package/dist/presentation/cfg.js +44 -29
  224. package/dist/presentation/cfg.js.map +1 -1
  225. package/dist/presentation/flow.d.ts.map +1 -1
  226. package/dist/presentation/flow.js +58 -38
  227. package/dist/presentation/flow.js.map +1 -1
  228. package/dist/types.d.ts +16 -2
  229. package/dist/types.d.ts.map +1 -1
  230. package/grammars/tree-sitter-erlang.wasm +0 -0
  231. package/grammars/tree-sitter-fsharp.wasm +0 -0
  232. package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
  233. package/grammars/tree-sitter-gleam.wasm +0 -0
  234. package/package.json +10 -10
  235. package/src/ast-analysis/engine.ts +145 -61
  236. package/src/ast-analysis/rules/index.ts +87 -0
  237. package/src/ast-analysis/visitor-utils.ts +86 -46
  238. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  239. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  240. package/src/cli/commands/audit.ts +1 -1
  241. package/src/cli/commands/build.ts +2 -0
  242. package/src/cli/commands/check.ts +1 -1
  243. package/src/cli/commands/children.ts +1 -1
  244. package/src/cli/commands/diff-impact.ts +1 -1
  245. package/src/cli/commands/embed.ts +54 -4
  246. package/src/cli/commands/roles.ts +1 -1
  247. package/src/cli/commands/structure.ts +1 -1
  248. package/src/cli/shared/options.ts +1 -1
  249. package/src/db/connection.ts +8 -0
  250. package/src/domain/analysis/dependencies.ts +166 -85
  251. package/src/domain/analysis/fn-impact.ts +120 -50
  252. package/src/domain/analysis/module-map.ts +175 -140
  253. package/src/domain/graph/builder/helpers.ts +85 -76
  254. package/src/domain/graph/builder/incremental.ts +223 -131
  255. package/src/domain/graph/builder/pipeline.ts +32 -785
  256. package/src/domain/graph/builder/stages/build-edges.ts +207 -142
  257. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  258. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  259. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  260. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  261. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  262. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  263. package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
  264. package/src/domain/graph/cycles.ts +51 -49
  265. package/src/domain/graph/journal.ts +84 -69
  266. package/src/domain/graph/watcher.ts +29 -25
  267. package/src/domain/parser.ts +170 -67
  268. package/src/domain/search/generator.ts +132 -74
  269. package/src/domain/search/models.ts +75 -4
  270. package/src/domain/search/search/hybrid.ts +53 -42
  271. package/src/domain/search/search/semantic.ts +105 -65
  272. package/src/domain/wasm-worker-entry.ts +243 -153
  273. package/src/extractors/c.ts +27 -8
  274. package/src/extractors/cpp.ts +50 -8
  275. package/src/extractors/cuda.ts +90 -16
  276. package/src/extractors/elixir.ts +103 -4
  277. package/src/extractors/erlang.ts +63 -20
  278. package/src/extractors/fsharp.ts +104 -0
  279. package/src/extractors/gleam.ts +40 -39
  280. package/src/extractors/groovy.ts +45 -1
  281. package/src/extractors/haskell.ts +45 -4
  282. package/src/extractors/helpers.ts +205 -1
  283. package/src/extractors/java.ts +42 -45
  284. package/src/extractors/javascript.ts +44 -43
  285. package/src/extractors/julia.ts +191 -77
  286. package/src/extractors/kotlin.ts +4 -0
  287. package/src/extractors/objc.ts +171 -47
  288. package/src/extractors/python.ts +5 -3
  289. package/src/extractors/r.ts +104 -82
  290. package/src/extractors/scala.ts +24 -36
  291. package/src/extractors/solidity.ts +59 -78
  292. package/src/extractors/verilog.ts +83 -15
  293. package/src/features/boundaries.ts +64 -46
  294. package/src/features/cfg.ts +145 -74
  295. package/src/features/check.ts +60 -43
  296. package/src/features/cochange.ts +95 -72
  297. package/src/features/complexity.ts +134 -79
  298. package/src/features/dataflow.ts +57 -34
  299. package/src/features/flow.ts +48 -24
  300. package/src/features/graph-enrichment.ts +105 -70
  301. package/src/features/owners.ts +186 -146
  302. package/src/features/sequence.ts +99 -69
  303. package/src/features/structure-query.ts +94 -79
  304. package/src/features/structure.ts +56 -56
  305. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  306. package/src/graph/classifiers/roles.ts +64 -54
  307. package/src/infrastructure/config.ts +1 -1
  308. package/src/mcp/tool-registry.ts +5 -0
  309. package/src/mcp/tools/semantic-search.ts +2 -0
  310. package/src/presentation/cfg.ts +48 -32
  311. package/src/presentation/flow.ts +100 -52
  312. package/src/types.ts +16 -1
@@ -91,6 +91,40 @@ interface BfsResult {
91
91
  truncated: boolean;
92
92
  }
93
93
 
94
+ type CalleeNode = { id: number; name: string; file: string; kind: string; line: number };
95
+
96
+ interface BfsFrame {
97
+ visited: Set<number>;
98
+ messages: SequenceMessage[];
99
+ fileSet: Set<string>;
100
+ idToNode: Map<number, CalleeNode>;
101
+ nextFrontier: number[];
102
+ }
103
+
104
+ function processCallee(
105
+ c: CalleeNode,
106
+ caller: CalleeNode,
107
+ depth: number,
108
+ noTests: boolean,
109
+ frame: BfsFrame,
110
+ ): void {
111
+ if (noTests && isTestFile(c.file)) return;
112
+
113
+ frame.fileSet.add(c.file);
114
+ frame.messages.push({
115
+ from: caller.file,
116
+ to: c.file,
117
+ label: c.name,
118
+ type: 'call',
119
+ depth,
120
+ });
121
+
122
+ if (frame.visited.has(c.id)) return;
123
+ frame.visited.add(c.id);
124
+ frame.nextFrontier.push(c.id);
125
+ frame.idToNode.set(c.id, c);
126
+ }
127
+
94
128
  function bfsCallees(
95
129
  repo: Repository,
96
130
  matchNode: MatchNode,
@@ -101,46 +135,25 @@ function bfsCallees(
101
135
  let frontier = [matchNode.id];
102
136
  const messages: SequenceMessage[] = [];
103
137
  const fileSet = new Set<string>([matchNode.file]);
104
- const idToNode = new Map<
105
- number,
106
- { id: number; name: string; file: string; kind: string; line: number }
107
- >();
138
+ const idToNode = new Map<number, CalleeNode>();
108
139
  idToNode.set(matchNode.id, matchNode);
109
140
  let truncated = false;
110
141
 
111
142
  for (let d = 1; d <= maxDepth; d++) {
112
- const nextFrontier: number[] = [];
143
+ const frame: BfsFrame = { visited, messages, fileSet, idToNode, nextFrontier: [] };
113
144
 
114
145
  for (const fid of frontier) {
115
- const callees = repo.findCallees(fid);
116
146
  const caller = idToNode.get(fid)!;
117
-
118
- for (const c of callees) {
119
- if (noTests && isTestFile(c.file)) continue;
120
-
121
- fileSet.add(c.file);
122
- messages.push({
123
- from: caller.file,
124
- to: c.file,
125
- label: c.name,
126
- type: 'call',
127
- depth: d,
128
- });
129
-
130
- if (visited.has(c.id)) continue;
131
-
132
- visited.add(c.id);
133
- nextFrontier.push(c.id);
134
- idToNode.set(c.id, c);
147
+ for (const c of repo.findCallees(fid)) {
148
+ processCallee(c, caller, d, noTests, frame);
135
149
  }
136
150
  }
137
151
 
138
- frontier = nextFrontier;
152
+ frontier = frame.nextFrontier;
139
153
  if (frontier.length === 0) break;
140
154
 
141
- if (d === maxDepth && frontier.length > 0) {
142
- const hasMoreCalls = frontier.some((fid) => repo.findCallees(fid).length > 0);
143
- if (hasMoreCalls) truncated = true;
155
+ if (d === maxDepth && frontier.some((fid) => repo.findCallees(fid).length > 0)) {
156
+ truncated = true;
144
157
  }
145
158
  }
146
159
 
@@ -174,26 +187,16 @@ function annotateDataflow(
174
187
  }
175
188
  }
176
189
 
177
- function _annotateDataflowImpl(
178
- db: BetterSqlite3Database,
190
+ type DataflowStmts = {
191
+ getReturns: ReturnType<BetterSqlite3Database['prepare']>;
192
+ getFlowsTo: ReturnType<BetterSqlite3Database['prepare']>;
193
+ };
194
+
195
+ function appendReturnMessages(
179
196
  messages: SequenceMessage[],
180
- idToNode: Map<number, { id: number; name: string; file: string; kind: string; line: number }>,
197
+ nodeByNameFile: Map<string, { id: number; name: string; file: string }>,
198
+ stmts: DataflowStmts,
181
199
  ): void {
182
- const nodeByNameFile = new Map<string, { id: number; name: string; file: string }>();
183
- for (const n of idToNode.values()) {
184
- nodeByNameFile.set(`${n.name}|${n.file}`, n);
185
- }
186
-
187
- const getReturns = db.prepare(
188
- `SELECT d.expression FROM dataflow d
189
- WHERE d.source_id = ? AND d.kind = 'returns'`,
190
- );
191
- const getFlowsTo = db.prepare(
192
- `SELECT d.expression FROM dataflow d
193
- WHERE d.target_id = ? AND d.kind = 'flows_to'
194
- ORDER BY d.param_index`,
195
- );
196
-
197
200
  const seenReturns = new Set<string>();
198
201
  for (const msg of [...messages]) {
199
202
  if (msg.type !== 'call') continue;
@@ -203,40 +206,67 @@ function _annotateDataflowImpl(
203
206
  const returnKey = `${msg.to}->${msg.from}:${msg.label}`;
204
207
  if (seenReturns.has(returnKey)) continue;
205
208
 
206
- const returns = getReturns.all(targetNode.id) as { expression: string }[];
207
-
208
- if (returns.length > 0) {
209
- seenReturns.add(returnKey);
210
- const expr = returns[0]!.expression || 'result';
211
- messages.push({
212
- from: msg.to,
213
- to: msg.from,
214
- label: expr,
215
- type: 'return',
216
- depth: msg.depth,
217
- });
218
- }
209
+ const returns = stmts.getReturns.all(targetNode.id) as { expression: string }[];
210
+ if (returns.length === 0) continue;
211
+
212
+ seenReturns.add(returnKey);
213
+ messages.push({
214
+ from: msg.to,
215
+ to: msg.from,
216
+ label: returns[0]!.expression || 'result',
217
+ type: 'return',
218
+ depth: msg.depth,
219
+ });
219
220
  }
221
+ }
220
222
 
223
+ function annotateCallParams(
224
+ messages: SequenceMessage[],
225
+ nodeByNameFile: Map<string, { id: number; name: string; file: string }>,
226
+ stmts: DataflowStmts,
227
+ ): void {
221
228
  for (const msg of messages) {
222
229
  if (msg.type !== 'call') continue;
223
230
  const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
224
231
  if (!targetNode) continue;
225
232
 
226
- const params = getFlowsTo.all(targetNode.id) as { expression: string }[];
227
-
228
- if (params.length > 0) {
229
- const paramNames = params
230
- .map((p) => p.expression)
231
- .filter(Boolean)
232
- .slice(0, 3);
233
- if (paramNames.length > 0) {
234
- msg.label = `${msg.label}(${paramNames.join(', ')})`;
235
- }
233
+ const params = stmts.getFlowsTo.all(targetNode.id) as { expression: string }[];
234
+ const paramNames = params
235
+ .map((p) => p.expression)
236
+ .filter(Boolean)
237
+ .slice(0, 3);
238
+ if (paramNames.length > 0) {
239
+ msg.label = `${msg.label}(${paramNames.join(', ')})`;
236
240
  }
237
241
  }
238
242
  }
239
243
 
244
+ function _annotateDataflowImpl(
245
+ db: BetterSqlite3Database,
246
+ messages: SequenceMessage[],
247
+ idToNode: Map<number, { id: number; name: string; file: string; kind: string; line: number }>,
248
+ ): void {
249
+ const nodeByNameFile = new Map<string, { id: number; name: string; file: string }>();
250
+ for (const n of idToNode.values()) {
251
+ nodeByNameFile.set(`${n.name}|${n.file}`, n);
252
+ }
253
+
254
+ const stmts: DataflowStmts = {
255
+ getReturns: db.prepare(
256
+ `SELECT d.expression FROM dataflow d
257
+ WHERE d.source_id = ? AND d.kind = 'returns'`,
258
+ ),
259
+ getFlowsTo: db.prepare(
260
+ `SELECT d.expression FROM dataflow d
261
+ WHERE d.target_id = ? AND d.kind = 'flows_to'
262
+ ORDER BY d.param_index`,
263
+ ),
264
+ };
265
+
266
+ appendReturnMessages(messages, nodeByNameFile, stmts);
267
+ annotateCallParams(messages, nodeByNameFile, stmts);
268
+ }
269
+
240
270
  interface Participant {
241
271
  id: string;
242
272
  label: string;
@@ -227,6 +227,96 @@ interface HotspotsDataOpts {
227
227
  noTests?: boolean;
228
228
  }
229
229
 
230
+ type HotspotEntry = {
231
+ name: string;
232
+ kind: string;
233
+ lineCount: number | null;
234
+ symbolCount: number | null;
235
+ importCount: number | null;
236
+ exportCount: number | null;
237
+ fanIn: number | null;
238
+ fanOut: number | null;
239
+ cohesion: number | null;
240
+ fileCount: number | null;
241
+ density: number;
242
+ coupling: number;
243
+ };
244
+
245
+ /** Compute density from either fileCount/symbolCount or lineCount/symbolCount. */
246
+ function computeHotspotDensity(
247
+ symbolCount: number | null,
248
+ fileCount: number | null,
249
+ lineCount: number | null,
250
+ ): number {
251
+ if ((fileCount ?? 0) > 0) return (symbolCount || 0) / (fileCount ?? 1);
252
+ if ((lineCount ?? 0) > 0) return (symbolCount || 0) / (lineCount ?? 1);
253
+ return 0;
254
+ }
255
+
256
+ /** Map a native-engine hotspot row (camelCase keys) to the public HotspotEntry shape. */
257
+ function mapNativeHotspotRow(r: {
258
+ name: string;
259
+ kind: string;
260
+ lineCount: number | null;
261
+ symbolCount: number | null;
262
+ importCount: number | null;
263
+ exportCount: number | null;
264
+ fanIn: number | null;
265
+ fanOut: number | null;
266
+ cohesion: number | null;
267
+ fileCount: number | null;
268
+ }): HotspotEntry {
269
+ return {
270
+ name: r.name,
271
+ kind: r.kind,
272
+ lineCount: r.lineCount,
273
+ symbolCount: r.symbolCount,
274
+ importCount: r.importCount,
275
+ exportCount: r.exportCount,
276
+ fanIn: r.fanIn,
277
+ fanOut: r.fanOut,
278
+ cohesion: r.cohesion,
279
+ fileCount: r.fileCount,
280
+ density: computeHotspotDensity(r.symbolCount, r.fileCount, r.lineCount),
281
+ coupling: (r.fanIn || 0) + (r.fanOut || 0),
282
+ };
283
+ }
284
+
285
+ /** Map a JS-path hotspot row (snake_case keys from SQLite) to the public HotspotEntry shape. */
286
+ function mapJsHotspotRow(r: HotspotRow): HotspotEntry {
287
+ return {
288
+ name: r.name,
289
+ kind: r.kind,
290
+ lineCount: r.line_count,
291
+ symbolCount: r.symbol_count,
292
+ importCount: r.import_count,
293
+ exportCount: r.export_count,
294
+ fanIn: r.fan_in,
295
+ fanOut: r.fan_out,
296
+ cohesion: r.cohesion,
297
+ fileCount: r.file_count,
298
+ density: computeHotspotDensity(r.symbol_count, r.file_count, r.line_count),
299
+ coupling: (r.fan_in || 0) + (r.fan_out || 0),
300
+ };
301
+ }
302
+
303
+ /** ORDER BY clause for each ranking dimension (strategy pattern). */
304
+ const HOTSPOT_ORDER_BY: Record<string, string> = {
305
+ 'fan-in': 'nm.fan_in DESC NULLS LAST',
306
+ 'fan-out': 'nm.fan_out DESC NULLS LAST',
307
+ density: 'nm.symbol_count DESC NULLS LAST',
308
+ coupling: '(COALESCE(nm.fan_in, 0) + COALESCE(nm.fan_out, 0)) DESC NULLS LAST',
309
+ };
310
+
311
+ /** Build the JS-path SQL query for a given metric and test filter. */
312
+ function buildHotspotQuery(metric: string, testFilter: string): string {
313
+ const orderBy = HOTSPOT_ORDER_BY[metric] ?? HOTSPOT_ORDER_BY['fan-in'];
314
+ return `SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
315
+ nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
316
+ FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
317
+ WHERE n.kind = ? ${testFilter} ORDER BY ${orderBy} LIMIT ?`;
318
+ }
319
+
230
320
  export function hotspotsData(
231
321
  customDbPath?: string,
232
322
  opts: HotspotsDataOpts = {},
@@ -242,96 +332,21 @@ export function hotspotsData(
242
332
  const level = opts.level || 'file';
243
333
  const limit = opts.limit || 10;
244
334
  const noTests = opts.noTests || false;
245
-
246
335
  const kind = level === 'directory' ? 'directory' : 'file';
247
336
 
248
- const mapRow = (r: {
249
- name: string;
250
- kind: string;
251
- lineCount: number | null;
252
- symbolCount: number | null;
253
- importCount: number | null;
254
- exportCount: number | null;
255
- fanIn: number | null;
256
- fanOut: number | null;
257
- cohesion: number | null;
258
- fileCount: number | null;
259
- }) => ({
260
- name: r.name,
261
- kind: r.kind,
262
- lineCount: r.lineCount,
263
- symbolCount: r.symbolCount,
264
- importCount: r.importCount,
265
- exportCount: r.exportCount,
266
- fanIn: r.fanIn,
267
- fanOut: r.fanOut,
268
- cohesion: r.cohesion,
269
- fileCount: r.fileCount,
270
- density:
271
- (r.fileCount ?? 0) > 0
272
- ? (r.symbolCount || 0) / (r.fileCount ?? 1)
273
- : (r.lineCount ?? 0) > 0
274
- ? (r.symbolCount || 0) / (r.lineCount ?? 1)
275
- : 0,
276
- coupling: (r.fanIn || 0) + (r.fanOut || 0),
277
- });
278
-
279
337
  // ── Native fast path: single query instead of 4 eagerly prepared ──
280
338
  if (nativeDb?.getHotspots) {
281
339
  const rows = nativeDb.getHotspots(kind, metric, noTests, limit);
282
- const hotspots = rows.map(mapRow);
340
+ const hotspots = rows.map(mapNativeHotspotRow);
283
341
  const base = { metric, level, limit, hotspots };
284
342
  return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
285
343
  }
286
344
 
287
345
  // ── JS fallback ───────────────────────────────────────────────────
288
346
  const testFilter = testFilterSQL('n.name', noTests && kind === 'file');
289
-
290
- const HOTSPOT_QUERIES: Record<string, { all(...params: unknown[]): HotspotRow[] }> = {
291
- 'fan-in': db.prepare(`
292
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
293
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
294
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
295
- WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_in DESC NULLS LAST LIMIT ?`),
296
- 'fan-out': db.prepare(`
297
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
298
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
299
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
300
- WHERE n.kind = ? ${testFilter} ORDER BY nm.fan_out DESC NULLS LAST LIMIT ?`),
301
- density: db.prepare(`
302
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
303
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
304
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
305
- WHERE n.kind = ? ${testFilter} ORDER BY nm.symbol_count DESC NULLS LAST LIMIT ?`),
306
- coupling: db.prepare(`
307
- SELECT n.name, n.kind, nm.line_count, nm.symbol_count, nm.import_count, nm.export_count,
308
- nm.fan_in, nm.fan_out, nm.cohesion, nm.file_count
309
- FROM nodes n JOIN node_metrics nm ON n.id = nm.node_id
310
- WHERE n.kind = ? ${testFilter} ORDER BY (COALESCE(nm.fan_in, 0) + COALESCE(nm.fan_out, 0)) DESC NULLS LAST LIMIT ?`),
311
- };
312
-
313
- const stmt = HOTSPOT_QUERIES[metric] ?? HOTSPOT_QUERIES['fan-in'];
314
- const rows = stmt!.all(kind, limit);
315
-
316
- const hotspots = rows.map((r) => ({
317
- name: r.name,
318
- kind: r.kind,
319
- lineCount: r.line_count,
320
- symbolCount: r.symbol_count,
321
- importCount: r.import_count,
322
- exportCount: r.export_count,
323
- fanIn: r.fan_in,
324
- fanOut: r.fan_out,
325
- cohesion: r.cohesion,
326
- fileCount: r.file_count,
327
- density:
328
- (r.file_count ?? 0) > 0
329
- ? (r.symbol_count || 0) / (r.file_count ?? 1)
330
- : (r.line_count ?? 0) > 0
331
- ? (r.symbol_count || 0) / (r.line_count ?? 1)
332
- : 0,
333
- coupling: (r.fan_in || 0) + (r.fan_out || 0),
334
- }));
347
+ const stmt = db.prepare(buildHotspotQuery(metric, testFilter));
348
+ const rows = stmt.all(kind, limit) as HotspotRow[];
349
+ const hotspots = rows.map(mapJsHotspotRow);
335
350
 
336
351
  const base = { metric, level, limit, hotspots };
337
352
  return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
@@ -532,6 +532,56 @@ function batchUpdateRoles(
532
532
  })();
533
533
  }
534
534
 
535
+ interface CallableNodeRow {
536
+ id: number;
537
+ name: string;
538
+ kind: string;
539
+ file: string;
540
+ fan_in: number;
541
+ fan_out: number;
542
+ }
543
+
544
+ /** Build the activeFiles set: files with at least one callable connected to the graph. */
545
+ function buildActiveFilesSet(rows: CallableNodeRow[]): Set<string> {
546
+ const activeFiles = new Set<string>();
547
+ for (const r of rows) {
548
+ if ((r.fan_in > 0 || r.fan_out > 0) && r.kind !== 'constant') {
549
+ activeFiles.add(r.file);
550
+ }
551
+ }
552
+ return activeFiles;
553
+ }
554
+
555
+ /** Map callable rows to classifier input objects, attaching exported/prod-fan-in/active-file metadata. */
556
+ function buildClassifierInput(
557
+ rows: CallableNodeRow[],
558
+ exportedIds: Set<number>,
559
+ prodFanInMap: Map<number, number>,
560
+ activeFiles: Set<string>,
561
+ ): Array<{
562
+ id: string;
563
+ name: string;
564
+ kind: string;
565
+ file: string;
566
+ fanIn: number;
567
+ fanOut: number;
568
+ isExported: boolean;
569
+ productionFanIn: number;
570
+ hasActiveFileSiblings: boolean | undefined;
571
+ }> {
572
+ return rows.map((r) => ({
573
+ id: String(r.id),
574
+ name: r.name,
575
+ kind: r.kind,
576
+ file: r.file,
577
+ fanIn: r.fan_in,
578
+ fanOut: r.fan_out,
579
+ isExported: exportedIds.has(r.id),
580
+ productionFanIn: prodFanInMap.get(r.id) || 0,
581
+ hasActiveFileSiblings: r.kind === 'constant' ? activeFiles.has(r.file) : undefined,
582
+ }));
583
+ }
584
+
535
585
  function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSummary): RoleSummary {
536
586
  // Leaf kinds (parameter, property) can never have callers/callees.
537
587
  // Classify them directly as dead-leaf without the expensive fan-in/fan-out JOINs.
@@ -558,14 +608,7 @@ function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSumm
558
608
  ) fo ON n.id = fo.source_id
559
609
  WHERE n.kind NOT IN ('file', 'directory', 'parameter', 'property')`,
560
610
  )
561
- .all() as {
562
- id: number;
563
- name: string;
564
- kind: string;
565
- file: string;
566
- fan_in: number;
567
- fan_out: number;
568
- }[];
611
+ .all() as CallableNodeRow[];
569
612
 
570
613
  if (rows.length === 0 && leafRows.length === 0) return emptySummary;
571
614
 
@@ -629,28 +672,9 @@ function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSumm
629
672
  prodFanInMap.set(r.target_id, r.cnt);
630
673
  }
631
674
 
632
- // Files with at least one callable (non-constant) connected to the graph.
633
- // Constants in these files are likely consumed locally via identifier reference.
634
- const activeFiles = new Set<string>();
635
- for (const r of rows) {
636
- if ((r.fan_in > 0 || r.fan_out > 0) && r.kind !== 'constant') {
637
- activeFiles.add(r.file);
638
- }
639
- }
640
-
641
675
  // Delegate classification to the pure-logic classifier
642
- const classifierInput = rows.map((r) => ({
643
- id: String(r.id),
644
- name: r.name,
645
- kind: r.kind,
646
- file: r.file,
647
- fanIn: r.fan_in,
648
- fanOut: r.fan_out,
649
- isExported: exportedIds.has(r.id),
650
- productionFanIn: prodFanInMap.get(r.id) || 0,
651
- hasActiveFileSiblings: r.kind === 'constant' ? activeFiles.has(r.file) : undefined,
652
- }));
653
-
676
+ const activeFiles = buildActiveFilesSet(rows);
677
+ const classifierInput = buildClassifierInput(rows, exportedIds, prodFanInMap, activeFiles);
654
678
  const roleMap = classifyRoles(classifierInput);
655
679
 
656
680
  const { summary, idsByRole } = buildRoleSummary(rows, leafRows, roleMap, emptySummary);
@@ -733,14 +757,7 @@ function classifyNodeRolesIncremental(
733
757
  WHERE n.kind NOT IN ('file', 'directory', 'parameter', 'property')
734
758
  AND n.file IN (${placeholders})`,
735
759
  )
736
- .all(...allAffectedFiles) as {
737
- id: number;
738
- name: string;
739
- kind: string;
740
- file: string;
741
- fan_in: number;
742
- fan_out: number;
743
- }[];
760
+ .all(...allAffectedFiles) as CallableNodeRow[];
744
761
 
745
762
  if (rows.length === 0 && leafRows.length === 0) return emptySummary;
746
763
 
@@ -810,25 +827,8 @@ function classifyNodeRolesIncremental(
810
827
  }
811
828
 
812
829
  // 5. Classify affected nodes using global medians
813
- const activeFiles = new Set<string>();
814
- for (const r of rows) {
815
- if ((r.fan_in > 0 || r.fan_out > 0) && r.kind !== 'constant') {
816
- activeFiles.add(r.file);
817
- }
818
- }
819
-
820
- const classifierInput = rows.map((r) => ({
821
- id: String(r.id),
822
- name: r.name,
823
- kind: r.kind,
824
- file: r.file,
825
- fanIn: r.fan_in,
826
- fanOut: r.fan_out,
827
- isExported: exportedIds.has(r.id),
828
- productionFanIn: prodFanInMap.get(r.id) || 0,
829
- hasActiveFileSiblings: r.kind === 'constant' ? activeFiles.has(r.file) : undefined,
830
- }));
831
-
830
+ const activeFiles = buildActiveFilesSet(rows);
831
+ const classifierInput = buildClassifierInput(rows, exportedIds, prodFanInMap, activeFiles);
832
832
  const roleMap = classifyRoles(classifierInput, globalMedians);
833
833
 
834
834
  // 6. Build summary (only for affected nodes) and update only those nodes