@optave/codegraph 3.10.0 → 3.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -33
- 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/rules/index.d.ts.map +1 -1
- package/dist/ast-analysis/rules/index.js +77 -0
- package/dist/ast-analysis/rules/index.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/audit.js +1 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +2 -0
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/check.js +1 -1
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/children.js +1 -1
- package/dist/cli/commands/children.js.map +1 -1
- package/dist/cli/commands/diff-impact.js +1 -1
- package/dist/cli/commands/diff-impact.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/cli/commands/roles.js +1 -1
- package/dist/cli/commands/roles.js.map +1 -1
- package/dist/cli/commands/structure.js +1 -1
- package/dist/cli/commands/structure.js.map +1 -1
- package/dist/cli/shared/options.js +1 -1
- package/dist/cli/shared/options.js.map +1 -1
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +8 -0
- package/dist/db/connection.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 -6
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +148 -99
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts +1 -0
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +23 -637
- 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 +141 -98
- 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/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- 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 +28 -20
- 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 +153 -80
- 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 +18 -0
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +72 -4
- 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 +209 -137
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/extractors/c.js +25 -6
- package/dist/extractors/c.js.map +1 -1
- package/dist/extractors/cpp.js +47 -6
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/cuda.js +90 -14
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/elixir.js +108 -4
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/erlang.js +56 -20
- package/dist/extractors/erlang.js.map +1 -1
- package/dist/extractors/fsharp.d.ts +7 -0
- package/dist/extractors/fsharp.d.ts.map +1 -1
- package/dist/extractors/fsharp.js +94 -0
- package/dist/extractors/fsharp.js.map +1 -1
- package/dist/extractors/gleam.d.ts.map +1 -1
- package/dist/extractors/gleam.js +29 -33
- package/dist/extractors/gleam.js.map +1 -1
- package/dist/extractors/groovy.js +41 -1
- package/dist/extractors/groovy.js.map +1 -1
- package/dist/extractors/haskell.js +48 -4
- package/dist/extractors/haskell.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 +198 -74
- package/dist/extractors/julia.js.map +1 -1
- package/dist/extractors/kotlin.js +4 -0
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/objc.js +184 -47
- package/dist/extractors/objc.js.map +1 -1
- package/dist/extractors/python.js +7 -4
- package/dist/extractors/python.js.map +1 -1
- package/dist/extractors/r.d.ts.map +1 -1
- package/dist/extractors/r.js +103 -87
- package/dist/extractors/r.js.map +1 -1
- package/dist/extractors/scala.d.ts.map +1 -1
- package/dist/extractors/scala.js +18 -32
- package/dist/extractors/scala.js.map +1 -1
- package/dist/extractors/solidity.d.ts.map +1 -1
- package/dist/extractors/solidity.js +55 -69
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/extractors/verilog.js +80 -15
- package/dist/extractors/verilog.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/mcp/tool-registry.d.ts.map +1 -1
- package/dist/mcp/tool-registry.js +4 -0
- package/dist/mcp/tool-registry.js.map +1 -1
- package/dist/mcp/tools/semantic-search.d.ts +1 -0
- package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
- package/dist/mcp/tools/semantic-search.js +1 -0
- package/dist/mcp/tools/semantic-search.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 +16 -2
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/grammars/tree-sitter-fsharp.wasm +0 -0
- package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +10 -10
- package/src/ast-analysis/engine.ts +145 -61
- package/src/ast-analysis/rules/index.ts +87 -0
- 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/audit.ts +1 -1
- package/src/cli/commands/build.ts +2 -0
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/children.ts +1 -1
- package/src/cli/commands/diff-impact.ts +1 -1
- package/src/cli/commands/embed.ts +54 -4
- package/src/cli/commands/roles.ts +1 -1
- package/src/cli/commands/structure.ts +1 -1
- package/src/cli/shared/options.ts +1 -1
- package/src/db/connection.ts +8 -0
- 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 +223 -131
- package/src/domain/graph/builder/pipeline.ts +32 -785
- package/src/domain/graph/builder/stages/build-edges.ts +207 -142
- 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/builder/stages/resolve-imports.ts +79 -25
- package/src/domain/graph/cycles.ts +51 -49
- package/src/domain/graph/journal.ts +84 -69
- package/src/domain/graph/watcher.ts +29 -25
- package/src/domain/parser.ts +170 -67
- package/src/domain/search/generator.ts +132 -74
- package/src/domain/search/models.ts +75 -4
- 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 +243 -153
- package/src/extractors/c.ts +27 -8
- package/src/extractors/cpp.ts +50 -8
- package/src/extractors/cuda.ts +90 -16
- package/src/extractors/elixir.ts +103 -4
- package/src/extractors/erlang.ts +63 -20
- package/src/extractors/fsharp.ts +104 -0
- package/src/extractors/gleam.ts +40 -39
- package/src/extractors/groovy.ts +45 -1
- package/src/extractors/haskell.ts +45 -4
- 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 +191 -77
- package/src/extractors/kotlin.ts +4 -0
- package/src/extractors/objc.ts +171 -47
- package/src/extractors/python.ts +5 -3
- package/src/extractors/r.ts +104 -82
- package/src/extractors/scala.ts +24 -36
- package/src/extractors/solidity.ts +59 -78
- package/src/extractors/verilog.ts +83 -15
- 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/mcp/tool-registry.ts +5 -0
- package/src/mcp/tools/semantic-search.ts +2 -0
- package/src/presentation/cfg.ts +48 -32
- package/src/presentation/flow.ts +100 -52
- package/src/types.ts +16 -1
|
@@ -336,13 +336,13 @@ interface FileLevelEdge {
|
|
|
336
336
|
target: string;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
|
|
339
|
+
/** Load file-level import/call edges from the DB and optionally exclude test files. */
|
|
340
|
+
function loadFileLevelEdges(
|
|
340
341
|
db: BetterSqlite3Database,
|
|
341
342
|
noTests: boolean,
|
|
342
343
|
minConf: number,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
let edges = db
|
|
344
|
+
): FileLevelEdge[] {
|
|
345
|
+
const edges = db
|
|
346
346
|
.prepare<FileLevelEdge>(
|
|
347
347
|
`
|
|
348
348
|
SELECT DISTINCT n1.file AS source, n2.file AS target
|
|
@@ -354,73 +354,118 @@ function prepareFileLevelData(
|
|
|
354
354
|
`,
|
|
355
355
|
)
|
|
356
356
|
.all(minConf);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const files = new Set<string>();
|
|
360
|
-
for (const { source, target } of edges) {
|
|
361
|
-
files.add(source);
|
|
362
|
-
files.add(target);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const fileIds = new Map<string, number>();
|
|
366
|
-
let idx = 0;
|
|
367
|
-
for (const f of files) fileIds.set(f, idx++);
|
|
357
|
+
return noTests ? edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target)) : edges;
|
|
358
|
+
}
|
|
368
359
|
|
|
369
|
-
|
|
360
|
+
/** Compute fan-in and fan-out for each file from a list of edges. */
|
|
361
|
+
function computeFileFanCounts(edges: FileLevelEdge[]): {
|
|
362
|
+
fanInCount: Map<string, number>;
|
|
363
|
+
fanOutCount: Map<string, number>;
|
|
364
|
+
} {
|
|
370
365
|
const fanInCount = new Map<string, number>();
|
|
371
366
|
const fanOutCount = new Map<string, number>();
|
|
372
367
|
for (const { source, target } of edges) {
|
|
373
368
|
fanOutCount.set(source, (fanOutCount.get(source) || 0) + 1);
|
|
374
369
|
fanInCount.set(target, (fanInCount.get(target) || 0) + 1);
|
|
375
370
|
}
|
|
371
|
+
return { fanInCount, fanOutCount };
|
|
372
|
+
}
|
|
376
373
|
|
|
377
|
-
|
|
374
|
+
/** Run Louvain community detection on the file-level graph. Returns empty map on failure. */
|
|
375
|
+
function detectFileCommunities(files: Set<string>, edges: FileLevelEdge[]): Map<string, number> {
|
|
378
376
|
const communityMap = new Map<string, number>();
|
|
379
|
-
if (files.size
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
const { assignments } = louvainCommunities(fileGraph);
|
|
388
|
-
for (const [file, cid] of assignments) communityMap.set(file, cid);
|
|
389
|
-
} catch {
|
|
390
|
-
// ignore
|
|
377
|
+
if (files.size === 0) return communityMap;
|
|
378
|
+
try {
|
|
379
|
+
const fileGraph = new CodeGraph();
|
|
380
|
+
for (const f of files) fileGraph.addNode(f);
|
|
381
|
+
for (const { source, target } of edges) {
|
|
382
|
+
if (source !== target && !fileGraph.hasEdge(source, target))
|
|
383
|
+
fileGraph.addEdge(source, target);
|
|
391
384
|
}
|
|
385
|
+
const { assignments } = louvainCommunities(fileGraph);
|
|
386
|
+
for (const [file, cid] of assignments) communityMap.set(file, cid);
|
|
387
|
+
} catch {
|
|
388
|
+
// louvain can fail on disconnected graphs
|
|
392
389
|
}
|
|
390
|
+
return communityMap;
|
|
391
|
+
}
|
|
393
392
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
393
|
+
/** Build a VisNode for a single file, applying color based on cfg.colorBy. */
|
|
394
|
+
function buildFileVisNode(
|
|
395
|
+
file: string,
|
|
396
|
+
id: number,
|
|
397
|
+
community: number | null,
|
|
398
|
+
fanIn: number,
|
|
399
|
+
fanOut: number,
|
|
400
|
+
cfg: PlotConfig,
|
|
401
|
+
): VisNode {
|
|
402
|
+
const color: string =
|
|
403
|
+
cfg.colorBy === 'community' && community !== null
|
|
404
|
+
? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length] || '#ccc'
|
|
405
|
+
: cfg.nodeColors?.file || (DEFAULT_NODE_COLORS as Record<string, string>).file || '#ccc';
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
id,
|
|
409
|
+
label: path.basename(file),
|
|
410
|
+
title: file,
|
|
411
|
+
color,
|
|
412
|
+
kind: 'file',
|
|
413
|
+
role: '',
|
|
414
|
+
file,
|
|
415
|
+
line: 0,
|
|
416
|
+
community,
|
|
417
|
+
cognitive: null,
|
|
418
|
+
cyclomatic: null,
|
|
419
|
+
maintainabilityIndex: null,
|
|
420
|
+
fanIn,
|
|
421
|
+
fanOut,
|
|
422
|
+
directory: path.dirname(file),
|
|
423
|
+
risk: [],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Select seed node IDs for the file-level graph based on configured strategy. */
|
|
428
|
+
function selectFileSeedNodes(visNodes: VisNode[], cfg: PlotConfig): (number | string)[] {
|
|
429
|
+
if (cfg.seedStrategy === 'top-fanin') {
|
|
430
|
+
const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
|
|
431
|
+
return sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
|
|
432
|
+
}
|
|
433
|
+
// Both 'entry' and the default fallback include every node — file-level graphs
|
|
434
|
+
// don't track per-file roles, so 'entry' has no meaningful filter.
|
|
435
|
+
return visNodes.map((n) => n.id);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function prepareFileLevelData(
|
|
439
|
+
db: BetterSqlite3Database,
|
|
440
|
+
noTests: boolean,
|
|
441
|
+
minConf: number,
|
|
442
|
+
cfg: PlotConfig,
|
|
443
|
+
): GraphData {
|
|
444
|
+
const edges = loadFileLevelEdges(db, noTests, minConf);
|
|
445
|
+
|
|
446
|
+
const files = new Set<string>();
|
|
447
|
+
for (const { source, target } of edges) {
|
|
448
|
+
files.add(source);
|
|
449
|
+
files.add(target);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const fileIds = new Map<string, number>();
|
|
453
|
+
let idx = 0;
|
|
454
|
+
for (const f of files) fileIds.set(f, idx++);
|
|
455
|
+
|
|
456
|
+
const { fanInCount, fanOutCount } = computeFileFanCounts(edges);
|
|
457
|
+
const communityMap = detectFileCommunities(files, edges);
|
|
458
|
+
|
|
459
|
+
const visNodes: VisNode[] = [...files].map((f) =>
|
|
460
|
+
buildFileVisNode(
|
|
461
|
+
f,
|
|
462
|
+
fileIds.get(f)!,
|
|
463
|
+
communityMap.get(f) ?? null,
|
|
464
|
+
fanInCount.get(f) || 0,
|
|
465
|
+
fanOutCount.get(f) || 0,
|
|
466
|
+
cfg,
|
|
467
|
+
),
|
|
468
|
+
);
|
|
424
469
|
|
|
425
470
|
const visEdges: VisEdge[] = edges.map(({ source, target }, i) => ({
|
|
426
471
|
id: `e${i}`,
|
|
@@ -428,17 +473,7 @@ function prepareFileLevelData(
|
|
|
428
473
|
to: fileIds.get(target)!,
|
|
429
474
|
}));
|
|
430
475
|
|
|
431
|
-
|
|
432
|
-
if (cfg.seedStrategy === 'top-fanin') {
|
|
433
|
-
const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
|
|
434
|
-
seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
|
|
435
|
-
} else if (cfg.seedStrategy === 'entry') {
|
|
436
|
-
seedNodeIds = visNodes.map((n) => n.id);
|
|
437
|
-
} else {
|
|
438
|
-
seedNodeIds = visNodes.map((n) => n.id);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return { nodes: visNodes, edges: visEdges, seedNodeIds };
|
|
476
|
+
return { nodes: visNodes, edges: visEdges, seedNodeIds: selectFileSeedNodes(visNodes, cfg) };
|
|
442
477
|
}
|
|
443
478
|
|
|
444
479
|
// ─── HTML Generation (thin wrapper) ──────────────────────────────────
|
package/src/features/owners.ts
CHANGED
|
@@ -139,18 +139,25 @@ interface OwnersDataOpts {
|
|
|
139
139
|
boundary?: boolean;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
interface OwnedSymbol {
|
|
143
|
+
name: string;
|
|
144
|
+
kind: string;
|
|
145
|
+
file: string;
|
|
146
|
+
line: number;
|
|
147
|
+
owners: string[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface OwnerBoundary {
|
|
151
|
+
from: OwnedSymbol;
|
|
152
|
+
to: OwnedSymbol;
|
|
153
|
+
edgeKind: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface OwnersDataResult {
|
|
146
157
|
codeownersFile: string | null;
|
|
147
158
|
files: { file: string; owners: string[] }[];
|
|
148
|
-
symbols:
|
|
149
|
-
boundaries:
|
|
150
|
-
from: { name: string; kind: string; file: string; line: number; owners: string[] };
|
|
151
|
-
to: { name: string; kind: string; file: string; line: number; owners: string[] };
|
|
152
|
-
edgeKind: string;
|
|
153
|
-
}[];
|
|
159
|
+
symbols: OwnedSymbol[];
|
|
160
|
+
boundaries: OwnerBoundary[];
|
|
154
161
|
summary: {
|
|
155
162
|
totalFiles: number;
|
|
156
163
|
ownedFiles: number;
|
|
@@ -159,160 +166,193 @@ export function ownersData(
|
|
|
159
166
|
ownerCount: number;
|
|
160
167
|
byOwner: { owner: string; fileCount: number }[];
|
|
161
168
|
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface BetterSqlite3DatabaseLike {
|
|
172
|
+
prepare(sql: string): { all(...params: unknown[]): unknown[] };
|
|
173
|
+
close(): void;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function emptyOwnersResult(codeownersFile: string | null): OwnersDataResult {
|
|
177
|
+
return {
|
|
178
|
+
codeownersFile,
|
|
179
|
+
files: [],
|
|
180
|
+
symbols: [],
|
|
181
|
+
boundaries: [],
|
|
182
|
+
summary: {
|
|
183
|
+
totalFiles: 0,
|
|
184
|
+
ownedFiles: 0,
|
|
185
|
+
unownedFiles: 0,
|
|
186
|
+
coveragePercent: 0,
|
|
187
|
+
ownerCount: 0,
|
|
188
|
+
byOwner: [],
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Load all distinct files from the DB and apply test/file filters. */
|
|
194
|
+
function loadFilteredFiles(db: BetterSqlite3DatabaseLike, opts: OwnersDataOpts): string[] {
|
|
195
|
+
let allFiles = (db.prepare('SELECT DISTINCT file FROM nodes').all() as { file: string }[]).map(
|
|
196
|
+
(r) => r.file,
|
|
197
|
+
);
|
|
198
|
+
if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
|
|
199
|
+
const fileFilters = normalizeFileFilter(opts.file);
|
|
200
|
+
if (fileFilters.length > 0) {
|
|
201
|
+
allFiles = allFiles.filter((f) => fileFilters.some((filter) => f.includes(filter)));
|
|
202
|
+
}
|
|
203
|
+
return allFiles;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Build owner index (owner -> list of files) and count owned files. */
|
|
207
|
+
function buildOwnerIndex(fileOwners: { file: string; owners: string[] }[]): {
|
|
208
|
+
ownerIndex: Map<string, string[]>;
|
|
209
|
+
ownedCount: number;
|
|
162
210
|
} {
|
|
211
|
+
const ownerIndex = new Map<string, string[]>();
|
|
212
|
+
let ownedCount = 0;
|
|
213
|
+
for (const fo of fileOwners) {
|
|
214
|
+
if (fo.owners.length > 0) ownedCount++;
|
|
215
|
+
for (const o of fo.owners) {
|
|
216
|
+
if (!ownerIndex.has(o)) ownerIndex.set(o, []);
|
|
217
|
+
ownerIndex.get(o)!.push(fo.file);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { ownerIndex, ownedCount };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Load symbols restricted to the given file set, applying noTests and kind filters. */
|
|
224
|
+
function loadSymbolsForFiles(
|
|
225
|
+
db: BetterSqlite3DatabaseLike,
|
|
226
|
+
fileSet: Set<string>,
|
|
227
|
+
opts: OwnersDataOpts,
|
|
228
|
+
rules: CodeownersRule[],
|
|
229
|
+
): OwnedSymbol[] {
|
|
230
|
+
let symbols = (
|
|
231
|
+
db.prepare('SELECT name, kind, file, line FROM nodes').all() as {
|
|
232
|
+
name: string;
|
|
233
|
+
kind: string;
|
|
234
|
+
file: string;
|
|
235
|
+
line: number;
|
|
236
|
+
}[]
|
|
237
|
+
).filter((n) => fileSet.has(n.file));
|
|
238
|
+
|
|
239
|
+
if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
|
|
240
|
+
if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
|
|
241
|
+
|
|
242
|
+
return symbols.map((s) => ({ ...s, owners: matchOwners(s.file, rules) }));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
interface CallEdgeRow {
|
|
246
|
+
id: number;
|
|
247
|
+
edgeKind: string;
|
|
248
|
+
srcName: string;
|
|
249
|
+
srcKind: string;
|
|
250
|
+
srcFile: string;
|
|
251
|
+
srcLine: number;
|
|
252
|
+
tgtName: string;
|
|
253
|
+
tgtKind: string;
|
|
254
|
+
tgtFile: string;
|
|
255
|
+
tgtLine: number;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Compute cross-owner call boundaries. Returns empty array when boundary mode is off. */
|
|
259
|
+
function computeOwnerBoundaries(
|
|
260
|
+
db: BetterSqlite3DatabaseLike,
|
|
261
|
+
rules: CodeownersRule[],
|
|
262
|
+
noTests: boolean,
|
|
263
|
+
): OwnerBoundary[] {
|
|
264
|
+
const edges = db
|
|
265
|
+
.prepare(
|
|
266
|
+
`SELECT e.id, e.kind AS edgeKind,
|
|
267
|
+
s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
|
|
268
|
+
t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
|
|
269
|
+
FROM edges e
|
|
270
|
+
JOIN nodes s ON e.source_id = s.id
|
|
271
|
+
JOIN nodes t ON e.target_id = t.id
|
|
272
|
+
WHERE e.kind = 'calls'`,
|
|
273
|
+
)
|
|
274
|
+
.all() as CallEdgeRow[];
|
|
275
|
+
|
|
276
|
+
const boundaries: OwnerBoundary[] = [];
|
|
277
|
+
for (const e of edges) {
|
|
278
|
+
if (noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
|
|
279
|
+
const srcOwners = matchOwners(e.srcFile, rules);
|
|
280
|
+
const tgtOwners = matchOwners(e.tgtFile, rules);
|
|
281
|
+
// Cross-boundary: different owner sets (sort for deterministic comparison + output)
|
|
282
|
+
const sortedSrc = [...srcOwners].sort();
|
|
283
|
+
const sortedTgt = [...tgtOwners].sort();
|
|
284
|
+
const srcKey = sortedSrc.join(',');
|
|
285
|
+
const tgtKey = sortedTgt.join(',');
|
|
286
|
+
if (srcKey === tgtKey) continue;
|
|
287
|
+
boundaries.push({
|
|
288
|
+
from: {
|
|
289
|
+
name: e.srcName,
|
|
290
|
+
kind: e.srcKind,
|
|
291
|
+
file: e.srcFile,
|
|
292
|
+
line: e.srcLine,
|
|
293
|
+
owners: sortedSrc,
|
|
294
|
+
},
|
|
295
|
+
to: { name: e.tgtName, kind: e.tgtKind, file: e.tgtFile, line: e.tgtLine, owners: sortedTgt },
|
|
296
|
+
edgeKind: e.edgeKind,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return boundaries;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Build summary stats (totals, coverage, by-owner counts). */
|
|
303
|
+
function buildOwnersSummary(
|
|
304
|
+
totalFiles: number,
|
|
305
|
+
ownedCount: number,
|
|
306
|
+
ownerIndex: Map<string, string[]>,
|
|
307
|
+
): OwnersDataResult['summary'] {
|
|
308
|
+
const byOwner = [...ownerIndex.entries()]
|
|
309
|
+
.map(([owner, files]) => ({ owner, fileCount: files.length }))
|
|
310
|
+
.sort((a, b) => b.fileCount - a.fileCount);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
totalFiles,
|
|
314
|
+
ownedFiles: ownedCount,
|
|
315
|
+
unownedFiles: totalFiles - ownedCount,
|
|
316
|
+
coveragePercent: totalFiles > 0 ? Math.round((ownedCount / totalFiles) * 100) : 0,
|
|
317
|
+
ownerCount: ownerIndex.size,
|
|
318
|
+
byOwner,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function ownersData(customDbPath?: string, opts: OwnersDataOpts = {}): OwnersDataResult {
|
|
163
323
|
const db = openReadonlyOrFail(customDbPath);
|
|
164
324
|
try {
|
|
165
325
|
const dbPath = findDbPath(customDbPath);
|
|
166
326
|
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
167
327
|
|
|
168
328
|
const parsed = parseCodeowners(repoRoot);
|
|
169
|
-
if (!parsed)
|
|
170
|
-
return {
|
|
171
|
-
codeownersFile: null,
|
|
172
|
-
files: [],
|
|
173
|
-
symbols: [],
|
|
174
|
-
boundaries: [],
|
|
175
|
-
summary: {
|
|
176
|
-
totalFiles: 0,
|
|
177
|
-
ownedFiles: 0,
|
|
178
|
-
unownedFiles: 0,
|
|
179
|
-
coveragePercent: 0,
|
|
180
|
-
ownerCount: 0,
|
|
181
|
-
byOwner: [],
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Get all distinct files from nodes
|
|
187
|
-
let allFiles = (db.prepare('SELECT DISTINCT file FROM nodes').all() as { file: string }[]).map(
|
|
188
|
-
(r) => r.file,
|
|
189
|
-
);
|
|
329
|
+
if (!parsed) return emptyOwnersResult(null);
|
|
190
330
|
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Map files to owners
|
|
198
|
-
const fileOwners = allFiles.map((file) => ({
|
|
199
|
-
file,
|
|
200
|
-
owners: matchOwners(file, parsed.rules),
|
|
201
|
-
}));
|
|
202
|
-
|
|
203
|
-
// Build owner-to-files index
|
|
204
|
-
const ownerIndex = new Map<string, string[]>();
|
|
205
|
-
let ownedCount = 0;
|
|
206
|
-
for (const fo of fileOwners) {
|
|
207
|
-
if (fo.owners.length > 0) ownedCount++;
|
|
208
|
-
for (const o of fo.owners) {
|
|
209
|
-
if (!ownerIndex.has(o)) ownerIndex.set(o, []);
|
|
210
|
-
ownerIndex.get(o)!.push(fo.file);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
331
|
+
// Stage 1: load files and bucket them by owner
|
|
332
|
+
const allFiles = loadFilteredFiles(db, opts);
|
|
333
|
+
const fileOwners = allFiles.map((file) => ({ file, owners: matchOwners(file, parsed.rules) }));
|
|
334
|
+
const { ownerIndex, ownedCount } = buildOwnerIndex(fileOwners);
|
|
213
335
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
336
|
+
// Stage 2: apply optional --owner filter
|
|
337
|
+
const filteredFiles = opts.owner
|
|
338
|
+
? fileOwners.filter((fo) => fo.owners.includes(opts.owner!))
|
|
339
|
+
: fileOwners;
|
|
219
340
|
|
|
220
|
-
//
|
|
341
|
+
// Stage 3: load symbols for filtered files
|
|
221
342
|
const fileSet = new Set(filteredFiles.map((fo) => fo.file));
|
|
222
|
-
|
|
223
|
-
db.prepare('SELECT name, kind, file, line FROM nodes').all() as {
|
|
224
|
-
name: string;
|
|
225
|
-
kind: string;
|
|
226
|
-
file: string;
|
|
227
|
-
line: number;
|
|
228
|
-
}[]
|
|
229
|
-
).filter((n) => fileSet.has(n.file));
|
|
230
|
-
|
|
231
|
-
if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
|
|
232
|
-
if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
|
|
233
|
-
|
|
234
|
-
const symbolsWithOwners = symbols.map((s) => ({
|
|
235
|
-
...s,
|
|
236
|
-
owners: matchOwners(s.file, parsed.rules),
|
|
237
|
-
}));
|
|
238
|
-
|
|
239
|
-
// Boundary analysis — cross-owner call edges
|
|
240
|
-
const boundaries: {
|
|
241
|
-
from: { name: string; kind: string; file: string; line: number; owners: string[] };
|
|
242
|
-
to: { name: string; kind: string; file: string; line: number; owners: string[] };
|
|
243
|
-
edgeKind: string;
|
|
244
|
-
}[] = [];
|
|
245
|
-
if (opts.boundary) {
|
|
246
|
-
const edges = db
|
|
247
|
-
.prepare(
|
|
248
|
-
`SELECT e.id, e.kind AS edgeKind,
|
|
249
|
-
s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
|
|
250
|
-
t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
|
|
251
|
-
FROM edges e
|
|
252
|
-
JOIN nodes s ON e.source_id = s.id
|
|
253
|
-
JOIN nodes t ON e.target_id = t.id
|
|
254
|
-
WHERE e.kind = 'calls'`,
|
|
255
|
-
)
|
|
256
|
-
.all() as {
|
|
257
|
-
id: number;
|
|
258
|
-
edgeKind: string;
|
|
259
|
-
srcName: string;
|
|
260
|
-
srcKind: string;
|
|
261
|
-
srcFile: string;
|
|
262
|
-
srcLine: number;
|
|
263
|
-
tgtName: string;
|
|
264
|
-
tgtKind: string;
|
|
265
|
-
tgtFile: string;
|
|
266
|
-
tgtLine: number;
|
|
267
|
-
}[];
|
|
268
|
-
|
|
269
|
-
for (const e of edges) {
|
|
270
|
-
if (opts.noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
|
|
271
|
-
const srcOwners = matchOwners(e.srcFile, parsed.rules);
|
|
272
|
-
const tgtOwners = matchOwners(e.tgtFile, parsed.rules);
|
|
273
|
-
// Cross-boundary: different owner sets
|
|
274
|
-
const srcKey = srcOwners.sort().join(',');
|
|
275
|
-
const tgtKey = tgtOwners.sort().join(',');
|
|
276
|
-
if (srcKey !== tgtKey) {
|
|
277
|
-
boundaries.push({
|
|
278
|
-
from: {
|
|
279
|
-
name: e.srcName,
|
|
280
|
-
kind: e.srcKind,
|
|
281
|
-
file: e.srcFile,
|
|
282
|
-
line: e.srcLine,
|
|
283
|
-
owners: srcOwners,
|
|
284
|
-
},
|
|
285
|
-
to: {
|
|
286
|
-
name: e.tgtName,
|
|
287
|
-
kind: e.tgtKind,
|
|
288
|
-
file: e.tgtFile,
|
|
289
|
-
line: e.tgtLine,
|
|
290
|
-
owners: tgtOwners,
|
|
291
|
-
},
|
|
292
|
-
edgeKind: e.edgeKind,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
343
|
+
const symbolsWithOwners = loadSymbolsForFiles(db, fileSet, opts, parsed.rules);
|
|
297
344
|
|
|
298
|
-
//
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
345
|
+
// Stage 4: optional boundary analysis (cross-owner call edges)
|
|
346
|
+
const boundaries = opts.boundary
|
|
347
|
+
? computeOwnerBoundaries(db, parsed.rules, opts.noTests ?? false)
|
|
348
|
+
: [];
|
|
302
349
|
|
|
303
350
|
return {
|
|
304
351
|
codeownersFile: parsed.path,
|
|
305
352
|
files: filteredFiles,
|
|
306
353
|
symbols: symbolsWithOwners,
|
|
307
354
|
boundaries,
|
|
308
|
-
summary:
|
|
309
|
-
totalFiles: allFiles.length,
|
|
310
|
-
ownedFiles: ownedCount,
|
|
311
|
-
unownedFiles: allFiles.length - ownedCount,
|
|
312
|
-
coveragePercent: allFiles.length > 0 ? Math.round((ownedCount / allFiles.length) * 100) : 0,
|
|
313
|
-
ownerCount: ownerIndex.size,
|
|
314
|
-
byOwner,
|
|
315
|
-
},
|
|
355
|
+
summary: buildOwnersSummary(allFiles.length, ownedCount, ownerIndex),
|
|
316
356
|
};
|
|
317
357
|
} finally {
|
|
318
358
|
db.close();
|