@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
@@ -4,7 +4,7 @@ import { loadConfig } from '../../infrastructure/config.js';
4
4
  import { debug } from '../../infrastructure/logger.js';
5
5
  import { isTestFile } from '../../infrastructure/test-filter.js';
6
6
  import { DEAD_ROLE_PREFIX } from '../../shared/kinds.js';
7
- import type { BetterSqlite3Database } from '../../types.js';
7
+ import type { BetterSqlite3Database, NativeDatabase } from '../../types.js';
8
8
  import { findCycles } from '../graph/cycles.js';
9
9
  import { LANGUAGE_REGISTRY } from '../parser.js';
10
10
 
@@ -198,30 +198,13 @@ function computeQualityMetrics(
198
198
  ).c;
199
199
  const callConfidence = totalCallEdges > 0 ? highConfCallEdges / totalCallEdges : 0;
200
200
 
201
- const fpRows = db
202
- .prepare(`
203
- SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
204
- FROM nodes n
205
- LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
206
- WHERE n.kind IN ('function', 'method')
207
- GROUP BY n.id
208
- HAVING caller_count > ?
209
- ORDER BY caller_count DESC
210
- `)
211
- .all(fpThreshold) as Array<{ name: string; file: string; line: number; caller_count: number }>;
212
- const falsePositiveWarnings = fpRows
213
- .filter((r) =>
214
- FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop()! : r.name),
215
- )
216
- .map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
201
+ const falsePositiveWarnings = buildFalsePositiveWarnings(queryFalsePositiveRows(db, fpThreshold));
217
202
 
218
203
  let fpEdgeCount = 0;
219
204
  for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
220
205
  const falsePositiveRatio = totalCallEdges > 0 ? fpEdgeCount / totalCallEdges : 0;
221
206
 
222
- const score = Math.round(
223
- callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20,
224
- );
207
+ const score = computeQualityScore(callerCoverage, callConfidence, falsePositiveRatio);
225
208
 
226
209
  return {
227
210
  score,
@@ -347,6 +330,169 @@ export function moduleMapData(customDbPath: string, limit = 20, opts: { noTests?
347
330
  }
348
331
  }
349
332
 
333
+ type FalsePositiveRow = { name: string; file: string; line: number; caller_count: number };
334
+
335
+ /** SQL query for false-positive caller counts above a threshold (shared by native and JS paths). */
336
+ function queryFalsePositiveRows(
337
+ db: BetterSqlite3Database,
338
+ fpThreshold: number,
339
+ ): FalsePositiveRow[] {
340
+ return db
341
+ .prepare(`
342
+ SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
343
+ FROM nodes n
344
+ LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
345
+ WHERE n.kind IN ('function', 'method')
346
+ GROUP BY n.id
347
+ HAVING caller_count > ?
348
+ ORDER BY caller_count DESC
349
+ `)
350
+ .all(fpThreshold) as FalsePositiveRow[];
351
+ }
352
+
353
+ /** Filter false-positive rows by the configured name set and shape them for the report. */
354
+ function buildFalsePositiveWarnings(rows: FalsePositiveRow[]) {
355
+ return rows
356
+ .filter((r) =>
357
+ FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop()! : r.name),
358
+ )
359
+ .map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
360
+ }
361
+
362
+ /** Compute the composite quality score (0-100) from coverage, confidence, and FP ratio. */
363
+ function computeQualityScore(
364
+ callerCoverage: number,
365
+ callConfidence: number,
366
+ falsePositiveRatio: number,
367
+ ): number {
368
+ return Math.round(callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20);
369
+ }
370
+
371
+ /** Aggregate role counts and derive the `dead` total. */
372
+ function aggregateRolesFromNative(roleCounts: Array<{ role: string; count: number }>) {
373
+ const roles: Record<string, number> & { dead?: number } = {};
374
+ let deadTotal = 0;
375
+ for (const r of roleCounts) {
376
+ roles[r.role] = r.count;
377
+ if (r.role.startsWith(DEAD_ROLE_PREFIX)) deadTotal += r.count;
378
+ }
379
+ if (deadTotal > 0) roles.dead = deadTotal;
380
+ return roles;
381
+ }
382
+
383
+ type NativeGraphStatsFn = NonNullable<NativeDatabase['getGraphStats']>;
384
+ type NativeGraphStats = ReturnType<NativeGraphStatsFn>;
385
+
386
+ /** Build the native fast-path stats result by combining native aggregations with JS-only sections. */
387
+ function buildStatsFromNative(
388
+ db: BetterSqlite3Database,
389
+ nativeStats: NativeGraphStats,
390
+ config: any,
391
+ jsSections: {
392
+ files: ReturnType<typeof countFilesByLanguage>;
393
+ fileCycles: unknown[];
394
+ fnCycles: unknown[];
395
+ },
396
+ ) {
397
+ const s = nativeStats;
398
+ const nodesByKind: Record<string, number> = {};
399
+ for (const k of s.nodesByKind) nodesByKind[k.kind] = k.count;
400
+ const edgesByKind: Record<string, number> = {};
401
+ for (const k of s.edgesByKind) edgesByKind[k.kind] = k.count;
402
+ const roles = aggregateRolesFromNative(s.roleCounts);
403
+
404
+ const callerCoverage =
405
+ s.quality.callableTotal > 0 ? s.quality.callableWithCallers / s.quality.callableTotal : 0;
406
+ const callConfidence =
407
+ s.quality.callEdges > 0 ? s.quality.highConfCallEdges / s.quality.callEdges : 0;
408
+
409
+ // False-positive analysis still uses JS (needs FALSE_POSITIVE_NAMES set)
410
+ const fpThreshold = config.analysis?.falsePositiveCallers ?? FALSE_POSITIVE_CALLER_THRESHOLD;
411
+ const falsePositiveWarnings = buildFalsePositiveWarnings(queryFalsePositiveRows(db, fpThreshold));
412
+ let fpEdgeCount = 0;
413
+ for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
414
+ const falsePositiveRatio = s.quality.callEdges > 0 ? fpEdgeCount / s.quality.callEdges : 0;
415
+ const score = computeQualityScore(callerCoverage, callConfidence, falsePositiveRatio);
416
+
417
+ return {
418
+ nodes: { total: s.totalNodes, byKind: nodesByKind },
419
+ edges: { total: s.totalEdges, byKind: edgesByKind },
420
+ files: jsSections.files,
421
+ cycles: { fileLevel: jsSections.fileCycles.length, functionLevel: jsSections.fnCycles.length },
422
+ hotspots: s.hotspots.map((h) => ({ file: h.file, fanIn: h.fanIn, fanOut: h.fanOut })),
423
+ embeddings: s.embeddings
424
+ ? {
425
+ count: s.embeddings.count,
426
+ model: s.embeddings.model,
427
+ dim: s.embeddings.dim,
428
+ builtAt: s.embeddings.builtAt,
429
+ }
430
+ : null,
431
+ quality: {
432
+ score,
433
+ callerCoverage: {
434
+ ratio: callerCoverage,
435
+ covered: s.quality.callableWithCallers,
436
+ total: s.quality.callableTotal,
437
+ },
438
+ callConfidence: {
439
+ ratio: callConfidence,
440
+ highConf: s.quality.highConfCallEdges,
441
+ total: s.quality.callEdges,
442
+ },
443
+ falsePositiveWarnings,
444
+ },
445
+ roles,
446
+ complexity: s.complexity
447
+ ? {
448
+ analyzed: s.complexity.analyzed,
449
+ avgCognitive: s.complexity.avgCognitive,
450
+ avgCyclomatic: s.complexity.avgCyclomatic,
451
+ maxCognitive: s.complexity.maxCognitive,
452
+ maxCyclomatic: s.complexity.maxCyclomatic,
453
+ avgMI: s.complexity.avgMi,
454
+ minMI: s.complexity.minMi,
455
+ }
456
+ : null,
457
+ };
458
+ }
459
+
460
+ /** Build the JS-fallback stats result using SQL aggregations from the helpers above. */
461
+ function buildStatsFromJs(
462
+ db: BetterSqlite3Database,
463
+ noTests: boolean,
464
+ config: any,
465
+ jsSections: {
466
+ files: ReturnType<typeof countFilesByLanguage>;
467
+ fileCycles: unknown[];
468
+ fnCycles: unknown[];
469
+ },
470
+ ) {
471
+ const testFilter = testFilterSQL('n.file', noTests);
472
+
473
+ const { total: totalNodes, byKind: nodesByKind } = countNodesByKind(db, noTests);
474
+ const { total: totalEdges, byKind: edgesByKind } = countEdgesByKind(db, noTests);
475
+
476
+ const hotspots = findHotspots(db, noTests, 5);
477
+ const embeddings = getEmbeddingsInfo(db);
478
+ const fpThreshold = config.analysis?.falsePositiveCallers ?? FALSE_POSITIVE_CALLER_THRESHOLD;
479
+ const quality = computeQualityMetrics(db, testFilter, fpThreshold);
480
+ const roles = countRoles(db, noTests);
481
+ const complexity = getComplexitySummary(db, testFilter);
482
+
483
+ return {
484
+ nodes: { total: totalNodes, byKind: nodesByKind },
485
+ edges: { total: totalEdges, byKind: edgesByKind },
486
+ files: jsSections.files,
487
+ cycles: { fileLevel: jsSections.fileCycles.length, functionLevel: jsSections.fnCycles.length },
488
+ hotspots,
489
+ embeddings,
490
+ quality,
491
+ roles,
492
+ complexity,
493
+ };
494
+ }
495
+
350
496
  export function statsData(customDbPath: string, opts: { noTests?: boolean; config?: any } = {}) {
351
497
  const { db, nativeDb, close } = openReadonlyWithNative(customDbPath);
352
498
  try {
@@ -354,127 +500,16 @@ export function statsData(customDbPath: string, opts: { noTests?: boolean; confi
354
500
  const config = opts.config || loadConfig();
355
501
 
356
502
  // These always need JS (non-SQL logic)
357
- const files = countFilesByLanguage(db, noTests);
358
- const fileCycles = findCycles(db, { fileLevel: true, noTests });
359
- const fnCycles = findCycles(db, { fileLevel: false, noTests });
360
-
361
- // ── Native fast path: batch all SQL aggregations in one napi call ──
362
- if (nativeDb?.getGraphStats) {
363
- const s = nativeDb.getGraphStats(noTests);
364
- const nodesByKind: Record<string, number> = {};
365
- for (const k of s.nodesByKind) nodesByKind[k.kind] = k.count;
366
- const edgesByKind: Record<string, number> = {};
367
- for (const k of s.edgesByKind) edgesByKind[k.kind] = k.count;
368
- const roles: Record<string, number> & { dead?: number } = {};
369
- let deadTotal = 0;
370
- for (const r of s.roleCounts) {
371
- roles[r.role] = r.count;
372
- if (r.role.startsWith(DEAD_ROLE_PREFIX)) deadTotal += r.count;
373
- }
374
- if (deadTotal > 0) roles.dead = deadTotal;
375
-
376
- const callerCoverage =
377
- s.quality.callableTotal > 0 ? s.quality.callableWithCallers / s.quality.callableTotal : 0;
378
- const callConfidence =
379
- s.quality.callEdges > 0 ? s.quality.highConfCallEdges / s.quality.callEdges : 0;
380
-
381
- // False-positive analysis still uses JS (needs FALSE_POSITIVE_NAMES set)
382
- const fpThreshold = config.analysis?.falsePositiveCallers ?? FALSE_POSITIVE_CALLER_THRESHOLD;
383
- const fpRows = db
384
- .prepare(`
385
- SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
386
- FROM nodes n
387
- LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
388
- WHERE n.kind IN ('function', 'method')
389
- GROUP BY n.id
390
- HAVING caller_count > ?
391
- ORDER BY caller_count DESC
392
- `)
393
- .all(fpThreshold) as Array<{
394
- name: string;
395
- file: string;
396
- line: number;
397
- caller_count: number;
398
- }>;
399
- const falsePositiveWarnings = fpRows
400
- .filter((r) =>
401
- FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop()! : r.name),
402
- )
403
- .map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
404
- let fpEdgeCount = 0;
405
- for (const fp of falsePositiveWarnings) fpEdgeCount += fp.callerCount;
406
- const falsePositiveRatio = s.quality.callEdges > 0 ? fpEdgeCount / s.quality.callEdges : 0;
407
- const score = Math.round(
408
- callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20,
409
- );
410
-
411
- return {
412
- nodes: { total: s.totalNodes, byKind: nodesByKind },
413
- edges: { total: s.totalEdges, byKind: edgesByKind },
414
- files,
415
- cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
416
- hotspots: s.hotspots.map((h) => ({ file: h.file, fanIn: h.fanIn, fanOut: h.fanOut })),
417
- embeddings: s.embeddings
418
- ? {
419
- count: s.embeddings.count,
420
- model: s.embeddings.model,
421
- dim: s.embeddings.dim,
422
- builtAt: s.embeddings.builtAt,
423
- }
424
- : null,
425
- quality: {
426
- score,
427
- callerCoverage: {
428
- ratio: callerCoverage,
429
- covered: s.quality.callableWithCallers,
430
- total: s.quality.callableTotal,
431
- },
432
- callConfidence: {
433
- ratio: callConfidence,
434
- highConf: s.quality.highConfCallEdges,
435
- total: s.quality.callEdges,
436
- },
437
- falsePositiveWarnings,
438
- },
439
- roles,
440
- complexity: s.complexity
441
- ? {
442
- analyzed: s.complexity.analyzed,
443
- avgCognitive: s.complexity.avgCognitive,
444
- avgCyclomatic: s.complexity.avgCyclomatic,
445
- maxCognitive: s.complexity.maxCognitive,
446
- maxCyclomatic: s.complexity.maxCyclomatic,
447
- avgMI: s.complexity.avgMi,
448
- minMI: s.complexity.minMi,
449
- }
450
- : null,
451
- };
452
- }
453
-
454
- // ── JS fallback ───────────────────────────────────────────────────
455
- const testFilter = testFilterSQL('n.file', noTests);
456
-
457
- const { total: totalNodes, byKind: nodesByKind } = countNodesByKind(db, noTests);
458
- const { total: totalEdges, byKind: edgesByKind } = countEdgesByKind(db, noTests);
459
-
460
- const hotspots = findHotspots(db, noTests, 5);
461
- const embeddings = getEmbeddingsInfo(db);
462
- const fpThreshold = config.analysis?.falsePositiveCallers ?? FALSE_POSITIVE_CALLER_THRESHOLD;
463
- const quality = computeQualityMetrics(db, testFilter, fpThreshold);
464
- const roles = countRoles(db, noTests);
465
- const complexity = getComplexitySummary(db, testFilter);
466
-
467
- return {
468
- nodes: { total: totalNodes, byKind: nodesByKind },
469
- edges: { total: totalEdges, byKind: edgesByKind },
470
- files,
471
- cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
472
- hotspots,
473
- embeddings,
474
- quality,
475
- roles,
476
- complexity,
503
+ const jsSections = {
504
+ files: countFilesByLanguage(db, noTests),
505
+ fileCycles: findCycles(db, { fileLevel: true, noTests }),
506
+ fnCycles: findCycles(db, { fileLevel: false, noTests }),
477
507
  };
508
+
509
+ const nativeStats = nativeDb?.getGraphStats?.(noTests);
510
+ return nativeStats
511
+ ? buildStatsFromNative(db, nativeStats, config, jsSections)
512
+ : buildStatsFromJs(db, noTests, config, jsSections);
478
513
  } finally {
479
514
  close();
480
515
  }
@@ -76,108 +76,117 @@ export function passesIncludeExclude(
76
76
  return true;
77
77
  }
78
78
 
79
+ /** Per-walk state computed once at the top-level invocation. */
80
+ interface CollectContext {
81
+ readonly rootDir: string;
82
+ readonly includeRegexes: readonly RegExp[];
83
+ readonly excludeRegexes: readonly RegExp[];
84
+ readonly hasGlobFilters: boolean;
85
+ readonly extraIgnore: Set<string> | null;
86
+ readonly visited: Set<string>;
87
+ }
88
+
89
+ /** Detect a symlink loop for `dir`. Returns true if `dir` was already visited. */
90
+ function isSymlinkLoop(dir: string, visited: Set<string>): boolean {
91
+ let realDir: string;
92
+ try {
93
+ realDir = fs.realpathSync(dir);
94
+ } catch {
95
+ return true;
96
+ }
97
+ if (visited.has(realDir)) {
98
+ warn(`Symlink loop detected, skipping: ${dir}`);
99
+ return true;
100
+ }
101
+ visited.add(realDir);
102
+ return false;
103
+ }
104
+
105
+ /** Read directory entries, returning null on error (already logged). */
106
+ function readDirSafe(dir: string): fs.Dirent[] | null {
107
+ try {
108
+ return fs.readdirSync(dir, { withFileTypes: true });
109
+ } catch (err: unknown) {
110
+ warn(`Cannot read directory ${dir}: ${(err as Error).message}`);
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /** True if `entry` is a source file we should collect under `ctx`. */
116
+ function isCollectableSourceFile(full: string, entry: fs.Dirent, ctx: CollectContext): boolean {
117
+ if (!EXTENSIONS.has(path.extname(entry.name))) return false;
118
+ if (!ctx.hasGlobFilters) return true;
119
+ const rel = normalizePath(path.relative(ctx.rootDir, full));
120
+ return passesIncludeExclude(rel, ctx.includeRegexes, ctx.excludeRegexes);
121
+ }
122
+
123
+ function walkCollect(
124
+ dir: string,
125
+ files: string[],
126
+ directories: Set<string> | null,
127
+ ctx: CollectContext,
128
+ ): void {
129
+ if (isSymlinkLoop(dir, ctx.visited)) return;
130
+
131
+ const entries = readDirSafe(dir);
132
+ if (!entries) return;
133
+
134
+ let hasFiles = false;
135
+ for (const entry of entries) {
136
+ if (shouldSkipEntry(entry, ctx.extraIgnore)) continue;
137
+
138
+ const full = path.join(dir, entry.name);
139
+ if (entry.isDirectory()) {
140
+ walkCollect(full, files, directories, ctx);
141
+ } else if (isCollectableSourceFile(full, entry, ctx)) {
142
+ files.push(full);
143
+ hasFiles = true;
144
+ }
145
+ }
146
+ if (directories && hasFiles) {
147
+ directories.add(dir);
148
+ }
149
+ }
150
+
79
151
  /**
80
152
  * Recursively collect all source files under `dir`.
81
153
  * When `directories` is a Set, also tracks which directories contain files.
82
154
  *
83
- * The first invocation establishes `dir` as the project root against which
84
- * `config.include` / `config.exclude` globs are matched.
155
+ * `dir` establishes the project root against which `config.include` /
156
+ * `config.exclude` globs are matched.
85
157
  */
86
158
  export function collectFiles(
87
159
  dir: string,
88
160
  files: string[],
89
161
  config: Partial<CodegraphConfig>,
90
162
  directories: Set<string>,
91
- _visited?: Set<string>,
92
- _rootDir?: string,
93
- _includeRegexes?: readonly RegExp[],
94
- _excludeRegexes?: readonly RegExp[],
95
163
  ): { files: string[]; directories: Set<string> };
96
164
  export function collectFiles(
97
165
  dir: string,
98
166
  files?: string[],
99
167
  config?: Partial<CodegraphConfig>,
100
168
  directories?: null,
101
- _visited?: Set<string>,
102
- _rootDir?: string,
103
- _includeRegexes?: readonly RegExp[],
104
- _excludeRegexes?: readonly RegExp[],
105
169
  ): string[];
106
170
  export function collectFiles(
107
171
  dir: string,
108
172
  files: string[] = [],
109
173
  config: Partial<CodegraphConfig> = {},
110
174
  directories: Set<string> | null = null,
111
- _visited: Set<string> = new Set(),
112
- _rootDir?: string,
113
- _includeRegexes?: readonly RegExp[],
114
- _excludeRegexes?: readonly RegExp[],
115
175
  ): string[] | { files: string[]; directories: Set<string> } {
116
176
  const trackDirs = directories instanceof Set;
117
- let hasFiles = false;
118
-
119
- // First call: compute root and compile include/exclude patterns once,
120
- // then pass them down recursive calls so we don't recompile per directory.
121
- const rootDir = _rootDir ?? dir;
122
- const includeRegexes = _includeRegexes ?? compileGlobs(config.include);
123
- const excludeRegexes = _excludeRegexes ?? compileGlobs(config.exclude);
124
- const hasGlobFilters = includeRegexes.length > 0 || excludeRegexes.length > 0;
125
-
126
- // Merge config ignoreDirs with defaults
127
- const extraIgnore = config.ignoreDirs ? new Set(config.ignoreDirs) : null;
128
-
129
- // Detect symlink loops (before I/O to avoid wasted readdirSync)
130
- let realDir: string;
131
- try {
132
- realDir = fs.realpathSync(dir);
133
- } catch {
134
- return trackDirs ? { files, directories: directories as Set<string> } : files;
135
- }
136
- if (_visited.has(realDir)) {
137
- warn(`Symlink loop detected, skipping: ${dir}`);
138
- return trackDirs ? { files, directories: directories as Set<string> } : files;
139
- }
140
- _visited.add(realDir);
141
-
142
- let entries: fs.Dirent[];
143
- try {
144
- entries = fs.readdirSync(dir, { withFileTypes: true });
145
- } catch (err: unknown) {
146
- warn(`Cannot read directory ${dir}: ${(err as Error).message}`);
147
- return trackDirs ? { files, directories: directories as Set<string> } : files;
148
- }
177
+ const includeRegexes = compileGlobs(config.include);
178
+ const excludeRegexes = compileGlobs(config.exclude);
179
+ const ctx: CollectContext = {
180
+ rootDir: dir,
181
+ includeRegexes,
182
+ excludeRegexes,
183
+ hasGlobFilters: includeRegexes.length > 0 || excludeRegexes.length > 0,
184
+ extraIgnore: config.ignoreDirs ? new Set(config.ignoreDirs) : null,
185
+ visited: new Set(),
186
+ };
149
187
 
150
- for (const entry of entries) {
151
- if (shouldSkipEntry(entry, extraIgnore)) continue;
188
+ walkCollect(dir, files, trackDirs ? (directories as Set<string>) : null, ctx);
152
189
 
153
- const full = path.join(dir, entry.name);
154
- if (entry.isDirectory()) {
155
- if (trackDirs) {
156
- collectFiles(
157
- full,
158
- files,
159
- config,
160
- directories as Set<string>,
161
- _visited,
162
- rootDir,
163
- includeRegexes,
164
- excludeRegexes,
165
- );
166
- } else {
167
- collectFiles(full, files, config, null, _visited, rootDir, includeRegexes, excludeRegexes);
168
- }
169
- } else if (EXTENSIONS.has(path.extname(entry.name))) {
170
- if (hasGlobFilters) {
171
- const rel = normalizePath(path.relative(rootDir, full));
172
- if (!passesIncludeExclude(rel, includeRegexes, excludeRegexes)) continue;
173
- }
174
- files.push(full);
175
- hasFiles = true;
176
- }
177
- }
178
- if (trackDirs && hasFiles) {
179
- (directories as Set<string>).add(dir);
180
- }
181
190
  return trackDirs ? { files, directories: directories as Set<string> } : files;
182
191
  }
183
192