@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.
- package/README.md +38 -31
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +91 -60
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/visitor-utils.d.ts +3 -0
- package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
- package/dist/ast-analysis/visitor-utils.js +83 -49
- package/dist/ast-analysis/visitor-utils.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
- package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
- package/dist/cli/commands/embed.d.ts.map +1 -1
- package/dist/cli/commands/embed.js +49 -4
- package/dist/cli/commands/embed.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +106 -80
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
- package/dist/domain/analysis/fn-impact.js +77 -52
- package/dist/domain/analysis/fn-impact.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +132 -121
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/call-resolver.d.ts +71 -0
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
- package/dist/domain/graph/builder/call-resolver.js +130 -0
- package/dist/domain/graph/builder/call-resolver.js.map +1 -0
- package/dist/domain/graph/builder/helpers.d.ts +4 -4
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +47 -33
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts +6 -0
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +214 -127
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts +1 -44
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +10 -766
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +151 -192
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.js +82 -65
- package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +60 -51
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
- package/dist/domain/graph/cycles.d.ts +6 -4
- package/dist/domain/graph/cycles.d.ts.map +1 -1
- package/dist/domain/graph/cycles.js +50 -55
- package/dist/domain/graph/cycles.js.map +1 -1
- package/dist/domain/graph/journal.d.ts.map +1 -1
- package/dist/domain/graph/journal.js +89 -70
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +10 -4
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +12 -23
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +126 -79
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/generator.d.ts +3 -1
- package/dist/domain/search/generator.d.ts.map +1 -1
- package/dist/domain/search/generator.js +68 -45
- package/dist/domain/search/generator.js.map +1 -1
- package/dist/domain/search/models.d.ts +2 -0
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +37 -3
- package/dist/domain/search/models.js.map +1 -1
- package/dist/domain/search/search/hybrid.d.ts.map +1 -1
- package/dist/domain/search/search/hybrid.js +49 -40
- package/dist/domain/search/search/hybrid.js.map +1 -1
- package/dist/domain/search/search/semantic.d.ts.map +1 -1
- package/dist/domain/search/search/semantic.js +69 -49
- package/dist/domain/search/search/semantic.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +201 -136
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/extractors/elixir.js +95 -71
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/gleam.d.ts.map +1 -1
- package/dist/extractors/gleam.js +23 -31
- package/dist/extractors/gleam.js.map +1 -1
- package/dist/extractors/helpers.d.ts +79 -1
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +137 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +37 -49
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +44 -44
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/julia.js +27 -34
- package/dist/extractors/julia.js.map +1 -1
- package/dist/extractors/r.d.ts.map +1 -1
- package/dist/extractors/r.js +33 -58
- package/dist/extractors/r.js.map +1 -1
- package/dist/extractors/solidity.d.ts.map +1 -1
- package/dist/extractors/solidity.js +38 -61
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/features/boundaries.d.ts.map +1 -1
- package/dist/features/boundaries.js +49 -39
- package/dist/features/boundaries.js.map +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +90 -63
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/check.d.ts.map +1 -1
- package/dist/features/check.js +43 -34
- package/dist/features/check.js.map +1 -1
- package/dist/features/cochange.d.ts.map +1 -1
- package/dist/features/cochange.js +68 -56
- package/dist/features/cochange.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +105 -75
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +37 -29
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/flow.d.ts.map +1 -1
- package/dist/features/flow.js +31 -22
- package/dist/features/flow.js.map +1 -1
- package/dist/features/graph-enrichment.d.ts.map +1 -1
- package/dist/features/graph-enrichment.js +77 -70
- package/dist/features/graph-enrichment.js.map +1 -1
- package/dist/features/owners.d.ts +17 -26
- package/dist/features/owners.d.ts.map +1 -1
- package/dist/features/owners.js +120 -109
- package/dist/features/owners.js.map +1 -1
- package/dist/features/sequence.d.ts.map +1 -1
- package/dist/features/sequence.js +59 -54
- package/dist/features/sequence.js.map +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +60 -60
- package/dist/features/structure-query.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +149 -52
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.js +100 -69
- package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +63 -59
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/infrastructure/config.d.ts +1 -1
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +1 -1
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/presentation/cfg.d.ts.map +1 -1
- package/dist/presentation/cfg.js +44 -29
- package/dist/presentation/cfg.js.map +1 -1
- package/dist/presentation/flow.d.ts.map +1 -1
- package/dist/presentation/flow.js +58 -38
- package/dist/presentation/flow.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/package.json +9 -9
- package/src/ast-analysis/engine.ts +145 -61
- package/src/ast-analysis/visitor-utils.ts +86 -46
- package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
- package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
- package/src/cli/commands/embed.ts +54 -4
- package/src/domain/analysis/dependencies.ts +166 -85
- package/src/domain/analysis/fn-impact.ts +120 -50
- package/src/domain/analysis/module-map.ts +175 -140
- package/src/domain/graph/builder/call-resolver.ts +181 -0
- package/src/domain/graph/builder/helpers.ts +85 -76
- package/src/domain/graph/builder/incremental.ts +321 -152
- package/src/domain/graph/builder/pipeline.ts +19 -957
- package/src/domain/graph/builder/stages/build-edges.ts +229 -275
- package/src/domain/graph/builder/stages/build-structure.ts +115 -82
- package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
- package/src/domain/graph/builder/stages/finalize.ts +72 -70
- package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
- package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
- package/src/domain/graph/cycles.ts +51 -49
- package/src/domain/graph/journal.ts +84 -69
- package/src/domain/graph/watcher.ts +12 -4
- package/src/domain/parser.ts +143 -66
- package/src/domain/search/generator.ts +132 -74
- package/src/domain/search/models.ts +39 -3
- package/src/domain/search/search/hybrid.ts +53 -42
- package/src/domain/search/search/semantic.ts +105 -65
- package/src/domain/wasm-worker-entry.ts +235 -152
- package/src/extractors/elixir.ts +91 -64
- package/src/extractors/gleam.ts +33 -37
- package/src/extractors/helpers.ts +205 -1
- package/src/extractors/java.ts +42 -45
- package/src/extractors/javascript.ts +44 -43
- package/src/extractors/julia.ts +28 -35
- package/src/extractors/r.ts +38 -56
- package/src/extractors/solidity.ts +43 -71
- package/src/features/boundaries.ts +64 -46
- package/src/features/cfg.ts +145 -74
- package/src/features/check.ts +60 -43
- package/src/features/cochange.ts +95 -72
- package/src/features/complexity.ts +134 -79
- package/src/features/dataflow.ts +57 -34
- package/src/features/flow.ts +48 -24
- package/src/features/graph-enrichment.ts +105 -70
- package/src/features/owners.ts +186 -146
- package/src/features/sequence.ts +99 -69
- package/src/features/structure-query.ts +94 -79
- package/src/features/structure.ts +199 -79
- package/src/graph/algorithms/leiden/optimiser.ts +142 -87
- package/src/graph/classifiers/roles.ts +64 -54
- package/src/infrastructure/config.ts +1 -1
- package/src/presentation/cfg.ts +48 -32
- package/src/presentation/flow.ts +100 -52
- 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
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 =
|
|
156
|
-
const
|
|
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
|
|
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 =
|
|
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
|
|
358
|
-
|
|
359
|
-
|
|
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
|
}
|