@optave/codegraph 3.5.0 → 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 (310) hide show
  1. package/README.md +35 -14
  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 +164 -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/go.d.ts.map +1 -1
  103. package/dist/extractors/go.js +126 -130
  104. package/dist/extractors/go.js.map +1 -1
  105. package/dist/extractors/hcl.js +6 -6
  106. package/dist/extractors/hcl.js.map +1 -1
  107. package/dist/extractors/helpers.d.ts +32 -1
  108. package/dist/extractors/helpers.d.ts.map +1 -1
  109. package/dist/extractors/helpers.js +74 -0
  110. package/dist/extractors/helpers.js.map +1 -1
  111. package/dist/extractors/index.d.ts +6 -0
  112. package/dist/extractors/index.d.ts.map +1 -1
  113. package/dist/extractors/index.js +6 -0
  114. package/dist/extractors/index.js.map +1 -1
  115. package/dist/extractors/java.d.ts.map +1 -1
  116. package/dist/extractors/java.js +32 -47
  117. package/dist/extractors/java.js.map +1 -1
  118. package/dist/extractors/javascript.d.ts.map +1 -1
  119. package/dist/extractors/javascript.js +306 -292
  120. package/dist/extractors/javascript.js.map +1 -1
  121. package/dist/extractors/kotlin.d.ts +6 -0
  122. package/dist/extractors/kotlin.d.ts.map +1 -0
  123. package/dist/extractors/kotlin.js +275 -0
  124. package/dist/extractors/kotlin.js.map +1 -0
  125. package/dist/extractors/php.d.ts.map +1 -1
  126. package/dist/extractors/php.js +39 -44
  127. package/dist/extractors/php.js.map +1 -1
  128. package/dist/extractors/python.d.ts.map +1 -1
  129. package/dist/extractors/python.js +75 -93
  130. package/dist/extractors/python.js.map +1 -1
  131. package/dist/extractors/ruby.js +6 -13
  132. package/dist/extractors/ruby.js.map +1 -1
  133. package/dist/extractors/rust.d.ts.map +1 -1
  134. package/dist/extractors/rust.js +58 -83
  135. package/dist/extractors/rust.js.map +1 -1
  136. package/dist/extractors/scala.d.ts +6 -0
  137. package/dist/extractors/scala.d.ts.map +1 -0
  138. package/dist/extractors/scala.js +269 -0
  139. package/dist/extractors/scala.js.map +1 -0
  140. package/dist/extractors/swift.d.ts +6 -0
  141. package/dist/extractors/swift.d.ts.map +1 -0
  142. package/dist/extractors/swift.js +275 -0
  143. package/dist/extractors/swift.js.map +1 -0
  144. package/dist/features/ast.d.ts +2 -0
  145. package/dist/features/ast.d.ts.map +1 -1
  146. package/dist/features/ast.js +9 -24
  147. package/dist/features/ast.js.map +1 -1
  148. package/dist/features/audit.d.ts.map +1 -1
  149. package/dist/features/audit.js +17 -21
  150. package/dist/features/audit.js.map +1 -1
  151. package/dist/features/branch-compare.d.ts.map +1 -1
  152. package/dist/features/branch-compare.js +47 -3
  153. package/dist/features/branch-compare.js.map +1 -1
  154. package/dist/features/cfg.d.ts +7 -1
  155. package/dist/features/cfg.d.ts.map +1 -1
  156. package/dist/features/cfg.js +118 -62
  157. package/dist/features/cfg.js.map +1 -1
  158. package/dist/features/check.d.ts.map +1 -1
  159. package/dist/features/check.js +79 -62
  160. package/dist/features/check.js.map +1 -1
  161. package/dist/features/complexity-query.d.ts.map +1 -1
  162. package/dist/features/complexity-query.js +142 -137
  163. package/dist/features/complexity-query.js.map +1 -1
  164. package/dist/features/complexity.d.ts +7 -1
  165. package/dist/features/complexity.d.ts.map +1 -1
  166. package/dist/features/complexity.js +62 -1
  167. package/dist/features/complexity.js.map +1 -1
  168. package/dist/features/dataflow.d.ts +7 -1
  169. package/dist/features/dataflow.d.ts.map +1 -1
  170. package/dist/features/dataflow.js +356 -188
  171. package/dist/features/dataflow.js.map +1 -1
  172. package/dist/features/graph-enrichment.d.ts.map +1 -1
  173. package/dist/features/graph-enrichment.js +117 -104
  174. package/dist/features/graph-enrichment.js.map +1 -1
  175. package/dist/features/sequence.d.ts.map +1 -1
  176. package/dist/features/sequence.js +25 -4
  177. package/dist/features/sequence.js.map +1 -1
  178. package/dist/features/structure-query.d.ts.map +1 -1
  179. package/dist/features/structure-query.js +29 -4
  180. package/dist/features/structure-query.js.map +1 -1
  181. package/dist/features/structure.d.ts.map +1 -1
  182. package/dist/features/structure.js +35 -15
  183. package/dist/features/structure.js.map +1 -1
  184. package/dist/graph/algorithms/leiden/adapter.d.ts.map +1 -1
  185. package/dist/graph/algorithms/leiden/adapter.js +88 -73
  186. package/dist/graph/algorithms/leiden/adapter.js.map +1 -1
  187. package/dist/graph/algorithms/leiden/index.js +43 -28
  188. package/dist/graph/algorithms/leiden/index.js.map +1 -1
  189. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  190. package/dist/graph/algorithms/leiden/optimiser.js +90 -104
  191. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  192. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  193. package/dist/graph/algorithms/leiden/partition.js +89 -106
  194. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  195. package/dist/graph/model.d.ts +2 -0
  196. package/dist/graph/model.d.ts.map +1 -1
  197. package/dist/graph/model.js +20 -8
  198. package/dist/graph/model.js.map +1 -1
  199. package/dist/infrastructure/config.d.ts +0 -8
  200. package/dist/infrastructure/config.d.ts.map +1 -1
  201. package/dist/infrastructure/config.js +73 -62
  202. package/dist/infrastructure/config.js.map +1 -1
  203. package/dist/infrastructure/registry.d.ts +0 -8
  204. package/dist/infrastructure/registry.d.ts.map +1 -1
  205. package/dist/infrastructure/registry.js +12 -14
  206. package/dist/infrastructure/registry.js.map +1 -1
  207. package/dist/mcp/server.d.ts.map +1 -1
  208. package/dist/mcp/server.js +45 -36
  209. package/dist/mcp/server.js.map +1 -1
  210. package/dist/presentation/audit.d.ts.map +1 -1
  211. package/dist/presentation/audit.js +61 -57
  212. package/dist/presentation/audit.js.map +1 -1
  213. package/dist/presentation/branch-compare.d.ts.map +1 -1
  214. package/dist/presentation/branch-compare.js +56 -38
  215. package/dist/presentation/branch-compare.js.map +1 -1
  216. package/dist/presentation/check.d.ts.map +1 -1
  217. package/dist/presentation/check.js +30 -32
  218. package/dist/presentation/check.js.map +1 -1
  219. package/dist/presentation/colors.d.ts.map +1 -1
  220. package/dist/presentation/colors.js +2 -0
  221. package/dist/presentation/colors.js.map +1 -1
  222. package/dist/presentation/complexity.d.ts.map +1 -1
  223. package/dist/presentation/complexity.js +25 -19
  224. package/dist/presentation/complexity.js.map +1 -1
  225. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  226. package/dist/presentation/queries-cli/exports.js +15 -15
  227. package/dist/presentation/queries-cli/exports.js.map +1 -1
  228. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  229. package/dist/presentation/queries-cli/impact.js +29 -19
  230. package/dist/presentation/queries-cli/impact.js.map +1 -1
  231. package/dist/types.d.ts +182 -7
  232. package/dist/types.d.ts.map +1 -1
  233. package/grammars/tree-sitter-bash.wasm +0 -0
  234. package/grammars/tree-sitter-c.wasm +0 -0
  235. package/grammars/tree-sitter-cpp.wasm +0 -0
  236. package/grammars/tree-sitter-kotlin.wasm +0 -0
  237. package/grammars/tree-sitter-scala.wasm +0 -0
  238. package/grammars/tree-sitter-swift.wasm +0 -0
  239. package/package.json +13 -7
  240. package/src/ast-analysis/engine.ts +147 -138
  241. package/src/ast-analysis/visitors/ast-store-visitor.ts +15 -2
  242. package/src/ast-analysis/visitors/complexity-visitor.ts +11 -11
  243. package/src/db/connection.ts +90 -59
  244. package/src/db/index.ts +1 -0
  245. package/src/db/migrations.ts +36 -32
  246. package/src/domain/analysis/context.ts +73 -75
  247. package/src/domain/analysis/dependencies.ts +78 -68
  248. package/src/domain/analysis/exports.ts +45 -34
  249. package/src/domain/analysis/fn-impact.ts +67 -64
  250. package/src/domain/analysis/module-map.ts +103 -8
  251. package/src/domain/analysis/query-helpers.ts +35 -0
  252. package/src/domain/graph/builder/helpers.ts +12 -6
  253. package/src/domain/graph/builder/incremental.ts +3 -2
  254. package/src/domain/graph/builder/pipeline.ts +71 -3
  255. package/src/domain/graph/builder/stages/build-edges.ts +10 -75
  256. package/src/domain/graph/builder/stages/build-structure.ts +9 -7
  257. package/src/domain/graph/builder/stages/collect-files.ts +2 -2
  258. package/src/domain/graph/builder/stages/detect-changes.ts +7 -2
  259. package/src/domain/graph/builder/stages/finalize.ts +159 -125
  260. package/src/domain/graph/builder/stages/insert-nodes.ts +32 -21
  261. package/src/domain/graph/builder/stages/resolve-imports.ts +3 -2
  262. package/src/domain/graph/resolve.ts +34 -46
  263. package/src/domain/graph/watcher.ts +12 -14
  264. package/src/domain/parser.ts +168 -97
  265. package/src/domain/search/search/cli-formatter.ts +121 -94
  266. package/src/extractors/bash.ts +97 -0
  267. package/src/extractors/c.ts +212 -0
  268. package/src/extractors/cpp.ts +298 -0
  269. package/src/extractors/csharp.ts +53 -56
  270. package/src/extractors/go.ts +152 -134
  271. package/src/extractors/hcl.ts +6 -6
  272. package/src/extractors/helpers.ts +93 -1
  273. package/src/extractors/index.ts +6 -0
  274. package/src/extractors/java.ts +43 -48
  275. package/src/extractors/javascript.ts +328 -281
  276. package/src/extractors/kotlin.ts +293 -0
  277. package/src/extractors/php.ts +46 -40
  278. package/src/extractors/python.ts +81 -104
  279. package/src/extractors/ruby.ts +6 -13
  280. package/src/extractors/rust.ts +65 -85
  281. package/src/extractors/scala.ts +285 -0
  282. package/src/extractors/swift.ts +293 -0
  283. package/src/features/ast.ts +10 -25
  284. package/src/features/audit.ts +24 -20
  285. package/src/features/branch-compare.ts +51 -4
  286. package/src/features/cfg.ts +158 -65
  287. package/src/features/check.ts +90 -74
  288. package/src/features/complexity-query.ts +181 -163
  289. package/src/features/complexity.ts +64 -1
  290. package/src/features/dataflow.ts +462 -217
  291. package/src/features/graph-enrichment.ts +161 -117
  292. package/src/features/sequence.ts +27 -4
  293. package/src/features/structure-query.ts +43 -4
  294. package/src/features/structure.ts +50 -22
  295. package/src/graph/algorithms/leiden/adapter.ts +126 -71
  296. package/src/graph/algorithms/leiden/index.ts +67 -28
  297. package/src/graph/algorithms/leiden/optimiser.ts +114 -105
  298. package/src/graph/algorithms/leiden/partition.ts +131 -98
  299. package/src/graph/model.ts +19 -7
  300. package/src/infrastructure/config.ts +60 -58
  301. package/src/infrastructure/registry.ts +17 -14
  302. package/src/mcp/server.ts +46 -37
  303. package/src/presentation/audit.ts +72 -67
  304. package/src/presentation/branch-compare.ts +54 -50
  305. package/src/presentation/check.ts +34 -34
  306. package/src/presentation/colors.ts +2 -0
  307. package/src/presentation/complexity.ts +39 -33
  308. package/src/presentation/queries-cli/exports.ts +17 -17
  309. package/src/presentation/queries-cli/impact.ts +30 -22
  310. package/src/types.ts +189 -7
@@ -83,50 +83,18 @@ interface FunctionEdgeRow {
83
83
  edge_kind: string;
84
84
  }
85
85
 
86
- function prepareFunctionLevelData(
87
- db: BetterSqlite3Database,
88
- noTests: boolean,
89
- minConf: number,
90
- cfg: PlotConfig,
91
- ): GraphData {
92
- let edges = db
93
- .prepare<FunctionEdgeRow>(
94
- `
95
- SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
96
- n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
97
- n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
98
- n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
99
- e.kind AS edge_kind
100
- FROM edges e
101
- JOIN nodes n1 ON e.source_id = n1.id
102
- JOIN nodes n2 ON e.target_id = n2.id
103
- WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')
104
- AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')
105
- AND e.kind = 'calls'
106
- AND e.confidence >= ?
107
- `,
108
- )
109
- .all(minConf);
110
- if (noTests)
111
- edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
112
-
113
- if (cfg.filter?.kinds) {
114
- const kinds = new Set(cfg.filter.kinds);
115
- edges = edges.filter((e) => kinds.has(e.source_kind) && kinds.has(e.target_kind));
116
- }
117
- if (cfg.filter?.files) {
118
- const patterns = cfg.filter.files;
119
- edges = edges.filter(
120
- (e) =>
121
- patterns.some((p) => e.source_file.includes(p)) &&
122
- patterns.some((p) => e.target_file.includes(p)),
123
- );
124
- }
86
+ type NodeInfo = {
87
+ id: number;
88
+ name: string;
89
+ kind: string;
90
+ file: string;
91
+ line: number;
92
+ role: string | null;
93
+ };
125
94
 
126
- const nodeMap = new Map<
127
- number,
128
- { id: number; name: string; kind: string; file: string; line: number; role: string | null }
129
- >();
95
+ /** Build node map from edge rows, collecting unique source/target nodes. */
96
+ function buildNodeMapFromEdges(edges: FunctionEdgeRow[]): Map<number, NodeInfo> {
97
+ const nodeMap = new Map<number, NodeInfo>();
130
98
  for (const e of edges) {
131
99
  if (!nodeMap.has(e.source_id)) {
132
100
  nodeMap.set(e.source_id, {
@@ -149,17 +117,13 @@ function prepareFunctionLevelData(
149
117
  });
150
118
  }
151
119
  }
120
+ return nodeMap;
121
+ }
152
122
 
153
- if (cfg.filter?.roles) {
154
- const roles = new Set(cfg.filter.roles);
155
- for (const [id, n] of nodeMap) {
156
- if (n.role === null || !roles.has(n.role)) nodeMap.delete(id);
157
- }
158
- const nodeIds = new Set(nodeMap.keys());
159
- edges = edges.filter((e) => nodeIds.has(e.source_id) && nodeIds.has(e.target_id));
160
- }
161
-
162
- // Complexity data
123
+ /** Load complexity data from function_complexity table. */
124
+ function loadComplexityMap(
125
+ db: BetterSqlite3Database,
126
+ ): Map<number, { cognitive: number; cyclomatic: number; maintainabilityIndex: number }> {
163
127
  const complexityMap = new Map<
164
128
  number,
165
129
  { cognitive: number; cyclomatic: number; maintainabilityIndex: number }
@@ -186,19 +150,17 @@ function prepareFunctionLevelData(
186
150
  } catch {
187
151
  // table may not exist in old DBs
188
152
  }
153
+ return complexityMap;
154
+ }
189
155
 
190
- // Fan-in / fan-out via graph subsystem
191
- const fnGraph = new CodeGraph();
192
- for (const [id] of nodeMap) fnGraph.addNode(String(id));
193
- for (const e of edges) {
194
- const src = String(e.source_id);
195
- const tgt = String(e.target_id);
196
- if (src !== tgt && !fnGraph.hasEdge(src, tgt)) fnGraph.addEdge(src, tgt);
197
- }
198
-
199
- // Use DB-level fan-in/fan-out (counts ALL call edges, not just visible)
156
+ /** Load fan-in and fan-out maps from edges table. */
157
+ function loadFanMaps(db: BetterSqlite3Database): {
158
+ fanInMap: Map<number, number>;
159
+ fanOutMap: Map<number, number>;
160
+ } {
200
161
  const fanInMap = new Map<number, number>();
201
162
  const fanOutMap = new Map<number, number>();
163
+
202
164
  const fanInRows = db
203
165
  .prepare<{ node_id: number; fan_in: number }>(
204
166
  "SELECT target_id AS node_id, COUNT(*) AS fan_in FROM edges WHERE kind = 'calls' GROUP BY target_id",
@@ -213,6 +175,138 @@ function prepareFunctionLevelData(
213
175
  .all();
214
176
  for (const r of fanOutRows) fanOutMap.set(r.node_id, r.fan_out);
215
177
 
178
+ return { fanInMap, fanOutMap };
179
+ }
180
+
181
+ /** Build an enriched VisNode from raw node info and computed maps. */
182
+ function buildEnrichedVisNode(
183
+ n: NodeInfo,
184
+ complexityMap: Map<
185
+ number,
186
+ { cognitive: number; cyclomatic: number; maintainabilityIndex: number }
187
+ >,
188
+ fanInMap: Map<number, number>,
189
+ fanOutMap: Map<number, number>,
190
+ communityMap: Map<number, number>,
191
+ cfg: PlotConfig,
192
+ ): VisNode {
193
+ const cx = complexityMap.get(n.id) || null;
194
+ const fanIn = fanInMap.get(n.id) || 0;
195
+ const fanOut = fanOutMap.get(n.id) || 0;
196
+ const community = communityMap.get(n.id) ?? null;
197
+ const directory = path.dirname(n.file);
198
+ const risk: string[] = [];
199
+ if (n.role?.startsWith('dead')) risk.push('dead-code');
200
+ if (fanIn >= (cfg.riskThresholds?.highBlastRadius ?? 10)) risk.push('high-blast-radius');
201
+ if (cx && cx.maintainabilityIndex < (cfg.riskThresholds?.lowMI ?? 40)) risk.push('low-mi');
202
+
203
+ const color: string =
204
+ cfg.colorBy === 'role' && n.role
205
+ ? cfg.roleColors?.[n.role] ||
206
+ (DEFAULT_ROLE_COLORS as Record<string, string>)[n.role] ||
207
+ '#ccc'
208
+ : cfg.colorBy === 'community' && community !== null
209
+ ? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length] || '#ccc'
210
+ : cfg.nodeColors?.[n.kind] ||
211
+ (DEFAULT_NODE_COLORS as Record<string, string>)[n.kind] ||
212
+ '#ccc';
213
+
214
+ return {
215
+ id: n.id,
216
+ label: n.name,
217
+ title: `${n.file}:${n.line} (${n.kind}${n.role ? `, ${n.role}` : ''})`,
218
+ color,
219
+ kind: n.kind,
220
+ role: n.role || '',
221
+ file: n.file,
222
+ line: n.line,
223
+ community,
224
+ cognitive: cx?.cognitive ?? null,
225
+ cyclomatic: cx?.cyclomatic ?? null,
226
+ maintainabilityIndex: cx?.maintainabilityIndex ?? null,
227
+ fanIn,
228
+ fanOut,
229
+ directory,
230
+ risk,
231
+ };
232
+ }
233
+
234
+ /** Select seed node IDs based on configured strategy. */
235
+ function selectSeedNodes(visNodes: VisNode[], cfg: PlotConfig): (number | string)[] {
236
+ if (cfg.seedStrategy === 'top-fanin') {
237
+ const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
238
+ return sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
239
+ }
240
+ if (cfg.seedStrategy === 'entry') {
241
+ return visNodes.filter((n) => n.role === 'entry').map((n) => n.id);
242
+ }
243
+ return visNodes.map((n) => n.id);
244
+ }
245
+
246
+ function prepareFunctionLevelData(
247
+ db: BetterSqlite3Database,
248
+ noTests: boolean,
249
+ minConf: number,
250
+ cfg: PlotConfig,
251
+ ): GraphData {
252
+ let edges = db
253
+ .prepare<FunctionEdgeRow>(
254
+ `
255
+ SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
256
+ n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
257
+ n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
258
+ n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
259
+ e.kind AS edge_kind
260
+ FROM edges e
261
+ JOIN nodes n1 ON e.source_id = n1.id
262
+ JOIN nodes n2 ON e.target_id = n2.id
263
+ WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')
264
+ AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')
265
+ AND e.kind = 'calls'
266
+ AND e.confidence >= ?
267
+ `,
268
+ )
269
+ .all(minConf);
270
+ if (noTests)
271
+ edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
272
+
273
+ if (cfg.filter?.kinds) {
274
+ const kinds = new Set(cfg.filter.kinds);
275
+ edges = edges.filter((e) => kinds.has(e.source_kind) && kinds.has(e.target_kind));
276
+ }
277
+ if (cfg.filter?.files) {
278
+ const patterns = cfg.filter.files;
279
+ edges = edges.filter(
280
+ (e) =>
281
+ patterns.some((p) => e.source_file.includes(p)) &&
282
+ patterns.some((p) => e.target_file.includes(p)),
283
+ );
284
+ }
285
+
286
+ const nodeMap = buildNodeMapFromEdges(edges);
287
+
288
+ if (cfg.filter?.roles) {
289
+ const roles = new Set(cfg.filter.roles);
290
+ for (const [id, n] of nodeMap) {
291
+ if (n.role === null || !roles.has(n.role)) nodeMap.delete(id);
292
+ }
293
+ const nodeIds = new Set(nodeMap.keys());
294
+ edges = edges.filter((e) => nodeIds.has(e.source_id) && nodeIds.has(e.target_id));
295
+ }
296
+
297
+ const complexityMap = loadComplexityMap(db);
298
+
299
+ // Build CodeGraph for Louvain community detection
300
+ const fnGraph = new CodeGraph();
301
+ for (const [id] of nodeMap) fnGraph.addNode(String(id));
302
+ for (const e of edges) {
303
+ const src = String(e.source_id);
304
+ const tgt = String(e.target_id);
305
+ if (src !== tgt && !fnGraph.hasEdge(src, tgt)) fnGraph.addEdge(src, tgt);
306
+ }
307
+
308
+ const { fanInMap, fanOutMap } = loadFanMaps(db);
309
+
216
310
  // Communities (Louvain) via graph subsystem
217
311
  const communityMap = new Map<number, number>();
218
312
  if (nodeMap.size > 0) {
@@ -224,48 +318,9 @@ function prepareFunctionLevelData(
224
318
  }
225
319
  }
226
320
 
227
- // Build enriched nodes
228
- const visNodes: VisNode[] = [...nodeMap.values()].map((n) => {
229
- const cx = complexityMap.get(n.id) || null;
230
- const fanIn = fanInMap.get(n.id) || 0;
231
- const fanOut = fanOutMap.get(n.id) || 0;
232
- const community = communityMap.get(n.id) ?? null;
233
- const directory = path.dirname(n.file);
234
- const risk: string[] = [];
235
- if (n.role?.startsWith('dead')) risk.push('dead-code');
236
- if (fanIn >= (cfg.riskThresholds?.highBlastRadius ?? 10)) risk.push('high-blast-radius');
237
- if (cx && cx.maintainabilityIndex < (cfg.riskThresholds?.lowMI ?? 40)) risk.push('low-mi');
238
-
239
- const color: string =
240
- cfg.colorBy === 'role' && n.role
241
- ? cfg.roleColors?.[n.role] ||
242
- (DEFAULT_ROLE_COLORS as Record<string, string>)[n.role] ||
243
- '#ccc'
244
- : cfg.colorBy === 'community' && community !== null
245
- ? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length] || '#ccc'
246
- : cfg.nodeColors?.[n.kind] ||
247
- (DEFAULT_NODE_COLORS as Record<string, string>)[n.kind] ||
248
- '#ccc';
249
-
250
- return {
251
- id: n.id,
252
- label: n.name,
253
- title: `${n.file}:${n.line} (${n.kind}${n.role ? `, ${n.role}` : ''})`,
254
- color,
255
- kind: n.kind,
256
- role: n.role || '',
257
- file: n.file,
258
- line: n.line,
259
- community,
260
- cognitive: cx?.cognitive ?? null,
261
- cyclomatic: cx?.cyclomatic ?? null,
262
- maintainabilityIndex: cx?.maintainabilityIndex ?? null,
263
- fanIn,
264
- fanOut,
265
- directory,
266
- risk,
267
- };
268
- });
321
+ const visNodes: VisNode[] = [...nodeMap.values()].map((n) =>
322
+ buildEnrichedVisNode(n, complexityMap, fanInMap, fanOutMap, communityMap, cfg),
323
+ );
269
324
 
270
325
  const visEdges: VisEdge[] = edges.map((e, i) => ({
271
326
  id: `e${i}`,
@@ -273,18 +328,7 @@ function prepareFunctionLevelData(
273
328
  to: e.target_id,
274
329
  }));
275
330
 
276
- // Seed strategy
277
- let seedNodeIds: (number | string)[];
278
- if (cfg.seedStrategy === 'top-fanin') {
279
- const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
280
- seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
281
- } else if (cfg.seedStrategy === 'entry') {
282
- seedNodeIds = visNodes.filter((n) => n.role === 'entry').map((n) => n.id);
283
- } else {
284
- seedNodeIds = visNodes.map((n) => n.id);
285
- }
286
-
287
- return { nodes: visNodes, edges: visEdges, seedNodeIds };
331
+ return { nodes: visNodes, edges: visEdges, seedNodeIds: selectSeedNodes(visNodes, cfg) };
288
332
  }
289
333
 
290
334
  interface FileLevelEdge {
@@ -1,10 +1,11 @@
1
+ import Database from 'better-sqlite3';
1
2
  import { openRepo, type Repository } from '../db/index.js';
2
3
  import { SqliteRepository } from '../db/repository/sqlite-repository.js';
3
4
  import { findMatchingNodes } from '../domain/queries.js';
4
5
  import { loadConfig } from '../infrastructure/config.js';
5
6
  import { isTestFile } from '../infrastructure/test-filter.js';
6
7
  import { paginateResult } from '../shared/paginate.js';
7
- import type { CodegraphConfig, NodeRowWithFanIn } from '../types.js';
8
+ import type { BetterSqlite3Database, CodegraphConfig, NodeRowWithFanIn } from '../types.js';
8
9
  import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
9
10
 
10
11
  // ─── Alias generation ────────────────────────────────────────────────
@@ -150,12 +151,34 @@ function annotateDataflow(
150
151
  repo: Repository,
151
152
  messages: SequenceMessage[],
152
153
  idToNode: Map<number, { id: number; name: string; file: string; kind: string; line: number }>,
154
+ dbPath?: string,
153
155
  ): void {
154
156
  const hasTable = repo.hasDataflowTable();
157
+ if (!hasTable) return;
158
+
159
+ let db: BetterSqlite3Database;
160
+ let ownDb = false;
161
+ if (repo instanceof SqliteRepository) {
162
+ db = repo.db;
163
+ } else if (dbPath) {
164
+ db = new Database(dbPath, { readonly: true }) as unknown as BetterSqlite3Database;
165
+ ownDb = true;
166
+ } else {
167
+ return;
168
+ }
155
169
 
156
- if (!hasTable || !(repo instanceof SqliteRepository)) return;
170
+ try {
171
+ _annotateDataflowImpl(db, messages, idToNode);
172
+ } finally {
173
+ if (ownDb) db.close();
174
+ }
175
+ }
157
176
 
158
- const db = repo.db;
177
+ function _annotateDataflowImpl(
178
+ db: BetterSqlite3Database,
179
+ messages: SequenceMessage[],
180
+ idToNode: Map<number, { id: number; name: string; file: string; kind: string; line: number }>,
181
+ ): void {
159
182
  const nodeByNameFile = new Map<string, { id: number; name: string; file: string }>();
160
183
  for (const n of idToNode.values()) {
161
184
  nodeByNameFile.set(`${n.name}|${n.file}`, n);
@@ -308,7 +331,7 @@ export function sequenceData(
308
331
  );
309
332
 
310
333
  if (opts.dataflow && messages.length > 0) {
311
- annotateDataflow(repo, messages, idToNode);
334
+ annotateDataflow(repo, messages, idToNode, dbPath);
312
335
  }
313
336
 
314
337
  messages.sort((a, b) => {
@@ -7,7 +7,7 @@
7
7
  * role classification).
8
8
  */
9
9
 
10
- import { openReadonlyOrFail, testFilterSQL } from '../db/index.js';
10
+ import { openReadonlyOrFail, openReadonlyWithNative, testFilterSQL } from '../db/index.js';
11
11
  import { loadConfig } from '../infrastructure/config.js';
12
12
  import { isTestFile } from '../infrastructure/test-filter.js';
13
13
  import { normalizePath } from '../shared/constants.js';
@@ -221,7 +221,7 @@ export function hotspotsData(
221
221
  limit: number;
222
222
  hotspots: unknown[];
223
223
  } {
224
- const db = openReadonlyOrFail(customDbPath);
224
+ const { db, nativeDb, close } = openReadonlyWithNative(customDbPath);
225
225
  try {
226
226
  const metric = opts.metric || 'fan-in';
227
227
  const level = opts.level || 'file';
@@ -230,6 +230,46 @@ export function hotspotsData(
230
230
 
231
231
  const kind = level === 'directory' ? 'directory' : 'file';
232
232
 
233
+ const mapRow = (r: {
234
+ name: string;
235
+ kind: string;
236
+ lineCount: number | null;
237
+ symbolCount: number | null;
238
+ importCount: number | null;
239
+ exportCount: number | null;
240
+ fanIn: number | null;
241
+ fanOut: number | null;
242
+ cohesion: number | null;
243
+ fileCount: number | null;
244
+ }) => ({
245
+ name: r.name,
246
+ kind: r.kind,
247
+ lineCount: r.lineCount,
248
+ symbolCount: r.symbolCount,
249
+ importCount: r.importCount,
250
+ exportCount: r.exportCount,
251
+ fanIn: r.fanIn,
252
+ fanOut: r.fanOut,
253
+ cohesion: r.cohesion,
254
+ fileCount: r.fileCount,
255
+ density:
256
+ (r.fileCount ?? 0) > 0
257
+ ? (r.symbolCount || 0) / (r.fileCount ?? 1)
258
+ : (r.lineCount ?? 0) > 0
259
+ ? (r.symbolCount || 0) / (r.lineCount ?? 1)
260
+ : 0,
261
+ coupling: (r.fanIn || 0) + (r.fanOut || 0),
262
+ });
263
+
264
+ // ── Native fast path: single query instead of 4 eagerly prepared ──
265
+ if (nativeDb?.getHotspots) {
266
+ const rows = nativeDb.getHotspots(kind, metric, noTests, limit);
267
+ const hotspots = rows.map(mapRow);
268
+ const base = { metric, level, limit, hotspots };
269
+ return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
270
+ }
271
+
272
+ // ── JS fallback ───────────────────────────────────────────────────
233
273
  const testFilter = testFilterSQL('n.name', noTests && kind === 'file');
234
274
 
235
275
  const HOTSPOT_QUERIES: Record<string, { all(...params: unknown[]): HotspotRow[] }> = {
@@ -256,7 +296,6 @@ export function hotspotsData(
256
296
  };
257
297
 
258
298
  const stmt = HOTSPOT_QUERIES[metric] ?? HOTSPOT_QUERIES['fan-in'];
259
- // stmt is always defined: metric is a valid key or the fallback is a concrete property
260
299
  const rows = stmt!.all(kind, limit);
261
300
 
262
301
  const hotspots = rows.map((r) => ({
@@ -282,7 +321,7 @@ export function hotspotsData(
282
321
  const base = { metric, level, limit, hotspots };
283
322
  return paginateResult(base, 'hotspots', { limit: opts.limit, offset: opts.offset });
284
323
  } finally {
285
- db.close();
324
+ close();
286
325
  }
287
326
  }
288
327
 
@@ -199,14 +199,11 @@ function computeFileMetrics(
199
199
  })();
200
200
  }
201
201
 
202
- function computeDirectoryMetrics(
203
- db: BetterSqlite3Database,
204
- upsertMetric: SqliteStatement,
205
- getNodeIdStmt: NodeIdStmt,
206
- fileSymbols: Map<string, FileSymbolData>,
202
+ /** Map each directory to the files it transitively contains. */
203
+ function buildDirFilesMap(
207
204
  allDirs: Set<string>,
208
- importEdges: ImportEdge[],
209
- ): void {
205
+ fileSymbols: Map<string, FileSymbolData>,
206
+ ): Map<string, string[]> {
210
207
  const dirFiles = new Map<string, string[]>();
211
208
  for (const dir of allDirs) {
212
209
  dirFiles.set(dir, []);
@@ -220,7 +217,11 @@ function computeDirectoryMetrics(
220
217
  d = normalizePath(path.dirname(d));
221
218
  }
222
219
  }
220
+ return dirFiles;
221
+ }
223
222
 
223
+ /** Build reverse map: file -> set of ancestor directories. */
224
+ function buildFileToAncestorDirs(dirFiles: Map<string, string[]>): Map<string, Set<string>> {
224
225
  const fileToAncestorDirs = new Map<string, Set<string>>();
225
226
  for (const [dir, files] of dirFiles) {
226
227
  for (const f of files) {
@@ -228,7 +229,15 @@ function computeDirectoryMetrics(
228
229
  fileToAncestorDirs.get(f)?.add(dir);
229
230
  }
230
231
  }
232
+ return fileToAncestorDirs;
233
+ }
231
234
 
235
+ /** Count intra-directory, fan-in, and fan-out edges per directory. */
236
+ function countDirectoryEdges(
237
+ allDirs: Set<string>,
238
+ importEdges: ImportEdge[],
239
+ fileToAncestorDirs: Map<string, Set<string>>,
240
+ ): Map<string, { intra: number; fanIn: number; fanOut: number }> {
232
241
  const dirEdgeCounts = new Map<string, { intra: number; fanIn: number; fanOut: number }>();
233
242
  for (const dir of allDirs) {
234
243
  dirEdgeCounts.set(dir, { intra: 0, fanIn: 0, fanOut: 0 });
@@ -258,6 +267,39 @@ function computeDirectoryMetrics(
258
267
  }
259
268
  }
260
269
  }
270
+ return dirEdgeCounts;
271
+ }
272
+
273
+ /** Count unique symbols in a list of files. */
274
+ function countSymbolsInFiles(files: string[], fileSymbols: Map<string, FileSymbolData>): number {
275
+ let symbolCount = 0;
276
+ for (const f of files) {
277
+ const sym = fileSymbols.get(f);
278
+ if (sym) {
279
+ const seen = new Set<string>();
280
+ for (const d of sym.definitions) {
281
+ const key = `${d.name}|${d.kind}|${d.line}`;
282
+ if (!seen.has(key)) {
283
+ seen.add(key);
284
+ symbolCount++;
285
+ }
286
+ }
287
+ }
288
+ }
289
+ return symbolCount;
290
+ }
291
+
292
+ function computeDirectoryMetrics(
293
+ db: BetterSqlite3Database,
294
+ upsertMetric: SqliteStatement,
295
+ getNodeIdStmt: NodeIdStmt,
296
+ fileSymbols: Map<string, FileSymbolData>,
297
+ allDirs: Set<string>,
298
+ importEdges: ImportEdge[],
299
+ ): void {
300
+ const dirFiles = buildDirFilesMap(allDirs, fileSymbols);
301
+ const fileToAncestorDirs = buildFileToAncestorDirs(dirFiles);
302
+ const dirEdgeCounts = countDirectoryEdges(allDirs, importEdges, fileToAncestorDirs);
261
303
 
262
304
  db.transaction(() => {
263
305
  for (const [dir, files] of dirFiles) {
@@ -265,21 +307,7 @@ function computeDirectoryMetrics(
265
307
  if (!dirRow) continue;
266
308
 
267
309
  const fileCount = files.length;
268
- let symbolCount = 0;
269
-
270
- for (const f of files) {
271
- const sym = fileSymbols.get(f);
272
- if (sym) {
273
- const seen = new Set<string>();
274
- for (const d of sym.definitions) {
275
- const key = `${d.name}|${d.kind}|${d.line}`;
276
- if (!seen.has(key)) {
277
- seen.add(key);
278
- symbolCount++;
279
- }
280
- }
281
- }
282
- }
310
+ const symbolCount = countSymbolsInFiles(files, fileSymbols);
283
311
 
284
312
  const counts = dirEdgeCounts.get(dir) || { intra: 0, fanIn: 0, fanOut: 0 };
285
313
  const totalEdges = counts.intra + counts.fanIn + counts.fanOut;