@optave/codegraph 3.11.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.
- 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/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 +142 -76
- 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 +133 -96
- 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 +5 -2
- 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.js +28 -36
- 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/package.json +7 -7
- 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/helpers.ts +85 -76
- package/src/domain/graph/builder/incremental.ts +217 -90
- package/src/domain/graph/builder/pipeline.ts +19 -957
- package/src/domain/graph/builder/stages/build-edges.ts +198 -140
- 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 +8 -2
- 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 +56 -56
- 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
|
@@ -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
|
}
|
|
@@ -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
|
-
*
|
|
84
|
-
* `config.
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
|