@optave/codegraph 3.11.0 → 3.11.2

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 (230) hide show
  1. package/README.md +38 -31
  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/visitor-utils.d.ts +3 -0
  6. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  7. package/dist/ast-analysis/visitor-utils.js +83 -49
  8. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  11. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  12. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  14. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  15. package/dist/cli/commands/embed.d.ts.map +1 -1
  16. package/dist/cli/commands/embed.js +49 -4
  17. package/dist/cli/commands/embed.js.map +1 -1
  18. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  19. package/dist/domain/analysis/dependencies.js +106 -80
  20. package/dist/domain/analysis/dependencies.js.map +1 -1
  21. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  22. package/dist/domain/analysis/fn-impact.js +77 -52
  23. package/dist/domain/analysis/fn-impact.js.map +1 -1
  24. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  25. package/dist/domain/analysis/module-map.js +132 -121
  26. package/dist/domain/analysis/module-map.js.map +1 -1
  27. package/dist/domain/graph/builder/call-resolver.d.ts +71 -0
  28. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
  29. package/dist/domain/graph/builder/call-resolver.js +130 -0
  30. package/dist/domain/graph/builder/call-resolver.js.map +1 -0
  31. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  32. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/helpers.js +47 -33
  34. package/dist/domain/graph/builder/helpers.js.map +1 -1
  35. package/dist/domain/graph/builder/incremental.d.ts +6 -0
  36. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/incremental.js +214 -127
  38. package/dist/domain/graph/builder/incremental.js.map +1 -1
  39. package/dist/domain/graph/builder/pipeline.d.ts +1 -44
  40. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/pipeline.js +10 -766
  42. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  43. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/stages/build-edges.js +151 -192
  45. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  46. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  48. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  49. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  51. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  54. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  56. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  58. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  60. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  61. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  62. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  63. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  64. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  65. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  66. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  67. package/dist/domain/graph/cycles.d.ts +6 -4
  68. package/dist/domain/graph/cycles.d.ts.map +1 -1
  69. package/dist/domain/graph/cycles.js +50 -55
  70. package/dist/domain/graph/cycles.js.map +1 -1
  71. package/dist/domain/graph/journal.d.ts.map +1 -1
  72. package/dist/domain/graph/journal.js +89 -70
  73. package/dist/domain/graph/journal.js.map +1 -1
  74. package/dist/domain/graph/watcher.d.ts.map +1 -1
  75. package/dist/domain/graph/watcher.js +10 -4
  76. package/dist/domain/graph/watcher.js.map +1 -1
  77. package/dist/domain/parser.d.ts +12 -23
  78. package/dist/domain/parser.d.ts.map +1 -1
  79. package/dist/domain/parser.js +126 -79
  80. package/dist/domain/parser.js.map +1 -1
  81. package/dist/domain/search/generator.d.ts +3 -1
  82. package/dist/domain/search/generator.d.ts.map +1 -1
  83. package/dist/domain/search/generator.js +68 -45
  84. package/dist/domain/search/generator.js.map +1 -1
  85. package/dist/domain/search/models.d.ts +2 -0
  86. package/dist/domain/search/models.d.ts.map +1 -1
  87. package/dist/domain/search/models.js +37 -3
  88. package/dist/domain/search/models.js.map +1 -1
  89. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  90. package/dist/domain/search/search/hybrid.js +49 -40
  91. package/dist/domain/search/search/hybrid.js.map +1 -1
  92. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  93. package/dist/domain/search/search/semantic.js +69 -49
  94. package/dist/domain/search/search/semantic.js.map +1 -1
  95. package/dist/domain/wasm-worker-entry.js +201 -136
  96. package/dist/domain/wasm-worker-entry.js.map +1 -1
  97. package/dist/extractors/elixir.js +95 -71
  98. package/dist/extractors/elixir.js.map +1 -1
  99. package/dist/extractors/gleam.d.ts.map +1 -1
  100. package/dist/extractors/gleam.js +23 -31
  101. package/dist/extractors/gleam.js.map +1 -1
  102. package/dist/extractors/helpers.d.ts +79 -1
  103. package/dist/extractors/helpers.d.ts.map +1 -1
  104. package/dist/extractors/helpers.js +137 -0
  105. package/dist/extractors/helpers.js.map +1 -1
  106. package/dist/extractors/java.d.ts.map +1 -1
  107. package/dist/extractors/java.js +37 -49
  108. package/dist/extractors/java.js.map +1 -1
  109. package/dist/extractors/javascript.d.ts.map +1 -1
  110. package/dist/extractors/javascript.js +44 -44
  111. package/dist/extractors/javascript.js.map +1 -1
  112. package/dist/extractors/julia.js +27 -34
  113. package/dist/extractors/julia.js.map +1 -1
  114. package/dist/extractors/r.d.ts.map +1 -1
  115. package/dist/extractors/r.js +33 -58
  116. package/dist/extractors/r.js.map +1 -1
  117. package/dist/extractors/solidity.d.ts.map +1 -1
  118. package/dist/extractors/solidity.js +38 -61
  119. package/dist/extractors/solidity.js.map +1 -1
  120. package/dist/features/boundaries.d.ts.map +1 -1
  121. package/dist/features/boundaries.js +49 -39
  122. package/dist/features/boundaries.js.map +1 -1
  123. package/dist/features/cfg.d.ts.map +1 -1
  124. package/dist/features/cfg.js +90 -63
  125. package/dist/features/cfg.js.map +1 -1
  126. package/dist/features/check.d.ts.map +1 -1
  127. package/dist/features/check.js +43 -34
  128. package/dist/features/check.js.map +1 -1
  129. package/dist/features/cochange.d.ts.map +1 -1
  130. package/dist/features/cochange.js +68 -56
  131. package/dist/features/cochange.js.map +1 -1
  132. package/dist/features/complexity.d.ts.map +1 -1
  133. package/dist/features/complexity.js +105 -75
  134. package/dist/features/complexity.js.map +1 -1
  135. package/dist/features/dataflow.d.ts.map +1 -1
  136. package/dist/features/dataflow.js +37 -29
  137. package/dist/features/dataflow.js.map +1 -1
  138. package/dist/features/flow.d.ts.map +1 -1
  139. package/dist/features/flow.js +31 -22
  140. package/dist/features/flow.js.map +1 -1
  141. package/dist/features/graph-enrichment.d.ts.map +1 -1
  142. package/dist/features/graph-enrichment.js +77 -70
  143. package/dist/features/graph-enrichment.js.map +1 -1
  144. package/dist/features/owners.d.ts +17 -26
  145. package/dist/features/owners.d.ts.map +1 -1
  146. package/dist/features/owners.js +120 -109
  147. package/dist/features/owners.js.map +1 -1
  148. package/dist/features/sequence.d.ts.map +1 -1
  149. package/dist/features/sequence.js +59 -54
  150. package/dist/features/sequence.js.map +1 -1
  151. package/dist/features/structure-query.d.ts.map +1 -1
  152. package/dist/features/structure-query.js +60 -60
  153. package/dist/features/structure-query.js.map +1 -1
  154. package/dist/features/structure.d.ts.map +1 -1
  155. package/dist/features/structure.js +149 -52
  156. package/dist/features/structure.js.map +1 -1
  157. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  158. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  159. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  160. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  161. package/dist/graph/classifiers/roles.js +63 -59
  162. package/dist/graph/classifiers/roles.js.map +1 -1
  163. package/dist/infrastructure/config.d.ts +1 -1
  164. package/dist/infrastructure/config.d.ts.map +1 -1
  165. package/dist/infrastructure/config.js +1 -1
  166. package/dist/infrastructure/config.js.map +1 -1
  167. package/dist/presentation/cfg.d.ts.map +1 -1
  168. package/dist/presentation/cfg.js +44 -29
  169. package/dist/presentation/cfg.js.map +1 -1
  170. package/dist/presentation/flow.d.ts.map +1 -1
  171. package/dist/presentation/flow.js +58 -38
  172. package/dist/presentation/flow.js.map +1 -1
  173. package/dist/types.d.ts +1 -1
  174. package/dist/types.d.ts.map +1 -1
  175. package/grammars/tree-sitter-erlang.wasm +0 -0
  176. package/package.json +9 -9
  177. package/src/ast-analysis/engine.ts +145 -61
  178. package/src/ast-analysis/visitor-utils.ts +86 -46
  179. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  180. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  181. package/src/cli/commands/embed.ts +54 -4
  182. package/src/domain/analysis/dependencies.ts +166 -85
  183. package/src/domain/analysis/fn-impact.ts +120 -50
  184. package/src/domain/analysis/module-map.ts +175 -140
  185. package/src/domain/graph/builder/call-resolver.ts +181 -0
  186. package/src/domain/graph/builder/helpers.ts +85 -76
  187. package/src/domain/graph/builder/incremental.ts +321 -152
  188. package/src/domain/graph/builder/pipeline.ts +19 -957
  189. package/src/domain/graph/builder/stages/build-edges.ts +229 -275
  190. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  191. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  192. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  193. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  194. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  195. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  196. package/src/domain/graph/cycles.ts +51 -49
  197. package/src/domain/graph/journal.ts +84 -69
  198. package/src/domain/graph/watcher.ts +12 -4
  199. package/src/domain/parser.ts +143 -66
  200. package/src/domain/search/generator.ts +132 -74
  201. package/src/domain/search/models.ts +39 -3
  202. package/src/domain/search/search/hybrid.ts +53 -42
  203. package/src/domain/search/search/semantic.ts +105 -65
  204. package/src/domain/wasm-worker-entry.ts +235 -152
  205. package/src/extractors/elixir.ts +91 -64
  206. package/src/extractors/gleam.ts +33 -37
  207. package/src/extractors/helpers.ts +205 -1
  208. package/src/extractors/java.ts +42 -45
  209. package/src/extractors/javascript.ts +44 -43
  210. package/src/extractors/julia.ts +28 -35
  211. package/src/extractors/r.ts +38 -56
  212. package/src/extractors/solidity.ts +43 -71
  213. package/src/features/boundaries.ts +64 -46
  214. package/src/features/cfg.ts +145 -74
  215. package/src/features/check.ts +60 -43
  216. package/src/features/cochange.ts +95 -72
  217. package/src/features/complexity.ts +134 -79
  218. package/src/features/dataflow.ts +57 -34
  219. package/src/features/flow.ts +48 -24
  220. package/src/features/graph-enrichment.ts +105 -70
  221. package/src/features/owners.ts +186 -146
  222. package/src/features/sequence.ts +99 -69
  223. package/src/features/structure-query.ts +94 -79
  224. package/src/features/structure.ts +199 -79
  225. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  226. package/src/graph/classifiers/roles.ts +64 -54
  227. package/src/infrastructure/config.ts +1 -1
  228. package/src/presentation/cfg.ts +48 -32
  229. package/src/presentation/flow.ts +100 -52
  230. package/src/types.ts +1 -1
@@ -83,6 +83,63 @@ function expandImplementors(
83
83
  }
84
84
  }
85
85
 
86
+ /** Record a caller node at depth `d`, adding to frontier and levels. */
87
+ function recordCaller(
88
+ caller: RelatedNodeRow,
89
+ parentId: number,
90
+ depth: number,
91
+ visited: Set<number>,
92
+ nextFrontier: number[],
93
+ levels: BfsLevels,
94
+ noTests: boolean,
95
+ onVisit?: BfsOnVisit,
96
+ ): void {
97
+ if (visited.has(caller.id) || (noTests && isTestFile(caller.file))) return;
98
+ visited.add(caller.id);
99
+ nextFrontier.push(caller.id);
100
+ if (!levels[depth]) levels[depth] = [];
101
+ levels[depth]!.push(toSymbolRef(caller));
102
+ if (onVisit) onVisit(caller, parentId, depth);
103
+ }
104
+
105
+ /** Process all callers of one frontier node, recording new nodes and expanding implementors. */
106
+ function processFrontierNode(
107
+ repo: InstanceType<typeof Repository>,
108
+ fid: number,
109
+ depth: number,
110
+ visited: Set<number>,
111
+ nextFrontier: number[],
112
+ levels: BfsLevels,
113
+ noTests: boolean,
114
+ resolveImplementors: boolean,
115
+ onVisit?: BfsOnVisit,
116
+ ): void {
117
+ const callers = repo.findDistinctCallers(fid) as RelatedNodeRow[];
118
+ for (const c of callers) {
119
+ recordCaller(c, fid, depth, visited, nextFrontier, levels, noTests, onVisit);
120
+ if (resolveImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
121
+ expandImplementors(repo, c.id, depth + 1, visited, nextFrontier, levels, noTests, onVisit);
122
+ }
123
+ }
124
+ }
125
+
126
+ /** Seed BFS with implementors of the start node when it is an interface/trait. */
127
+ function seedInterfaceImplementors(
128
+ repo: InstanceType<typeof Repository>,
129
+ startId: number,
130
+ visited: Set<number>,
131
+ levels: BfsLevels,
132
+ noTests: boolean,
133
+ onVisit?: BfsOnVisit,
134
+ ): number[] {
135
+ const implNextFrontier: number[] = [];
136
+ const startNode = repo.findNodeById(startId) as NodeRow | undefined;
137
+ if (startNode && INTERFACE_LIKE_KINDS.has(startNode.kind)) {
138
+ expandImplementors(repo, startId, 1, visited, implNextFrontier, levels, noTests, onVisit);
139
+ }
140
+ return implNextFrontier;
141
+ }
142
+
86
143
  export function bfsTransitiveCallers(
87
144
  dbOrRepo: BetterSqlite3Database | InstanceType<typeof Repository>,
88
145
  startId: number,
@@ -105,13 +162,9 @@ export function bfsTransitiveCallers(
105
162
  let frontier = [startId];
106
163
 
107
164
  // Seed: if start node is an interface/trait, include its implementors at depth 1
108
- const implNextFrontier: number[] = [];
109
- if (resolveImplementors) {
110
- const startNode = repo.findNodeById(startId) as NodeRow | undefined;
111
- if (startNode && INTERFACE_LIKE_KINDS.has(startNode.kind)) {
112
- expandImplementors(repo, startId, 1, visited, implNextFrontier, levels, noTests, onVisit);
113
- }
114
- }
165
+ const implNextFrontier = resolveImplementors
166
+ ? seedInterfaceImplementors(repo, startId, visited, levels, noTests, onVisit)
167
+ : [];
115
168
 
116
169
  for (let d = 1; d <= maxDepth; d++) {
117
170
  if (d === 1 && implNextFrontier.length > 0) {
@@ -119,19 +172,17 @@ export function bfsTransitiveCallers(
119
172
  }
120
173
  const nextFrontier: number[] = [];
121
174
  for (const fid of frontier) {
122
- const callers = repo.findDistinctCallers(fid) as RelatedNodeRow[];
123
- for (const c of callers) {
124
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
125
- visited.add(c.id);
126
- nextFrontier.push(c.id);
127
- if (!levels[d]) levels[d] = [];
128
- levels[d]!.push(toSymbolRef(c));
129
- if (onVisit) onVisit(c, fid, d);
130
- }
131
- if (resolveImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
132
- expandImplementors(repo, c.id, d + 1, visited, nextFrontier, levels, noTests, onVisit);
133
- }
134
- }
175
+ processFrontierNode(
176
+ repo,
177
+ fid,
178
+ d,
179
+ visited,
180
+ nextFrontier,
181
+ levels,
182
+ noTests,
183
+ resolveImplementors,
184
+ onVisit,
185
+ );
135
186
  }
136
187
  frontier = nextFrontier;
137
188
  if (frontier.length === 0) break;
@@ -140,6 +191,53 @@ export function bfsTransitiveCallers(
140
191
  return { totalDependents: visited.size - 1, levels };
141
192
  }
142
193
 
194
+ /** BFS over import dependents, returning visited node IDs and depth-per-id map. */
195
+ function bfsImportDependents(
196
+ repo: InstanceType<typeof Repository>,
197
+ seedNodes: NodeRow[],
198
+ noTests: boolean,
199
+ ): { visited: Set<number>; levels: Map<number, number> } {
200
+ const visited = new Set<number>();
201
+ const queue: number[] = [];
202
+ const levels = new Map<number, number>();
203
+
204
+ for (const fn of seedNodes) {
205
+ visited.add(fn.id);
206
+ queue.push(fn.id);
207
+ levels.set(fn.id, 0);
208
+ }
209
+
210
+ while (queue.length > 0) {
211
+ const current = queue.shift()!;
212
+ const level = levels.get(current)!;
213
+ const dependents = repo.findImportDependents(current) as RelatedNodeRow[];
214
+ for (const dep of dependents) {
215
+ if (visited.has(dep.id)) continue;
216
+ if (noTests && isTestFile(dep.file)) continue;
217
+ visited.add(dep.id);
218
+ queue.push(dep.id);
219
+ levels.set(dep.id, level + 1);
220
+ }
221
+ }
222
+
223
+ return { visited, levels };
224
+ }
225
+
226
+ /** Group visited dependents by depth (excluding seed depth 0). */
227
+ function groupDependentsByLevel(
228
+ repo: InstanceType<typeof Repository>,
229
+ levels: Map<number, number>,
230
+ ): Record<number, Array<{ file: string }>> {
231
+ const byLevel: Record<number, Array<{ file: string }>> = {};
232
+ for (const [id, level] of levels) {
233
+ if (level === 0) continue;
234
+ if (!byLevel[level]) byLevel[level] = [];
235
+ const node = repo.findNodeById(id) as NodeRow | undefined;
236
+ if (node) byLevel[level].push({ file: node.file });
237
+ }
238
+ return byLevel;
239
+ }
240
+
143
241
  export function impactAnalysisData(
144
242
  file: string,
145
243
  customDbPath: string,
@@ -152,36 +250,8 @@ export function impactAnalysisData(
152
250
  return { file, sources: [], levels: {}, totalDependents: 0 };
153
251
  }
154
252
 
155
- const visited = new Set<number>();
156
- const queue: number[] = [];
157
- const levels = new Map<number, number>();
158
-
159
- for (const fn of fileNodes) {
160
- visited.add(fn.id);
161
- queue.push(fn.id);
162
- levels.set(fn.id, 0);
163
- }
164
-
165
- while (queue.length > 0) {
166
- const current = queue.shift()!;
167
- const level = levels.get(current)!;
168
- const dependents = repo.findImportDependents(current) as RelatedNodeRow[];
169
- for (const dep of dependents) {
170
- if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
171
- visited.add(dep.id);
172
- queue.push(dep.id);
173
- levels.set(dep.id, level + 1);
174
- }
175
- }
176
- }
177
-
178
- const byLevel: Record<number, Array<{ file: string }>> = {};
179
- for (const [id, level] of levels) {
180
- if (level === 0) continue;
181
- if (!byLevel[level]) byLevel[level] = [];
182
- const node = repo.findNodeById(id) as NodeRow | undefined;
183
- if (node) byLevel[level].push({ file: node.file });
184
- }
253
+ const { visited, levels } = bfsImportDependents(repo, fileNodes, noTests);
254
+ const byLevel = groupDependentsByLevel(repo, levels);
185
255
 
186
256
  return {
187
257
  file,
@@ -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
  }