@optave/codegraph 3.1.3 → 3.1.5
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 -84
- package/package.json +13 -8
- package/src/ast-analysis/engine.js +32 -12
- package/src/ast-analysis/shared.js +6 -5
- package/src/cli/commands/ast.js +22 -0
- package/src/cli/commands/audit.js +45 -0
- package/src/cli/commands/batch.js +68 -0
- package/src/cli/commands/branch-compare.js +21 -0
- package/src/cli/commands/build.js +26 -0
- package/src/cli/commands/cfg.js +26 -0
- package/src/cli/commands/check.js +74 -0
- package/src/cli/commands/children.js +28 -0
- package/src/cli/commands/co-change.js +67 -0
- package/src/cli/commands/communities.js +19 -0
- package/src/cli/commands/complexity.js +46 -0
- package/src/cli/commands/context.js +30 -0
- package/src/cli/commands/cycles.js +32 -0
- package/src/cli/commands/dataflow.js +28 -0
- package/src/cli/commands/deps.js +12 -0
- package/src/cli/commands/diff-impact.js +26 -0
- package/src/cli/commands/embed.js +30 -0
- package/src/cli/commands/export.js +78 -0
- package/src/cli/commands/exports.js +14 -0
- package/src/cli/commands/flow.js +32 -0
- package/src/cli/commands/fn-impact.js +26 -0
- package/src/cli/commands/impact.js +12 -0
- package/src/cli/commands/info.js +76 -0
- package/src/cli/commands/map.js +19 -0
- package/src/cli/commands/mcp.js +18 -0
- package/src/cli/commands/models.js +19 -0
- package/src/cli/commands/owners.js +25 -0
- package/src/cli/commands/path.js +36 -0
- package/src/cli/commands/plot.js +89 -0
- package/src/cli/commands/query.js +45 -0
- package/src/cli/commands/registry.js +100 -0
- package/src/cli/commands/roles.js +30 -0
- package/src/cli/commands/search.js +42 -0
- package/src/cli/commands/sequence.js +28 -0
- package/src/cli/commands/snapshot.js +66 -0
- package/src/cli/commands/stats.js +15 -0
- package/src/cli/commands/structure.js +33 -0
- package/src/cli/commands/triage.js +78 -0
- package/src/cli/commands/watch.js +12 -0
- package/src/cli/commands/where.js +20 -0
- package/src/cli/index.js +124 -0
- package/src/cli/shared/open-graph.js +13 -0
- package/src/cli/shared/options.js +59 -0
- package/src/cli/shared/output.js +1 -0
- package/src/cli.js +11 -1522
- package/src/db/connection.js +130 -7
- package/src/{db.js → db/index.js} +17 -5
- package/src/db/migrations.js +42 -1
- package/src/db/query-builder.js +20 -12
- package/src/db/repository/base.js +201 -0
- package/src/db/repository/graph-read.js +7 -4
- package/src/db/repository/in-memory-repository.js +575 -0
- package/src/db/repository/index.js +5 -1
- package/src/db/repository/nodes.js +60 -6
- package/src/db/repository/sqlite-repository.js +219 -0
- package/src/domain/analysis/context.js +408 -0
- package/src/domain/analysis/dependencies.js +341 -0
- package/src/domain/analysis/exports.js +134 -0
- package/src/domain/analysis/impact.js +466 -0
- package/src/domain/analysis/module-map.js +322 -0
- package/src/domain/analysis/roles.js +45 -0
- package/src/domain/analysis/symbol-lookup.js +238 -0
- package/src/domain/graph/builder/context.js +85 -0
- package/src/domain/graph/builder/helpers.js +218 -0
- package/src/domain/graph/builder/incremental.js +178 -0
- package/src/domain/graph/builder/pipeline.js +130 -0
- package/src/domain/graph/builder/stages/build-edges.js +297 -0
- package/src/domain/graph/builder/stages/build-structure.js +113 -0
- package/src/domain/graph/builder/stages/collect-files.js +44 -0
- package/src/domain/graph/builder/stages/detect-changes.js +413 -0
- package/src/domain/graph/builder/stages/finalize.js +139 -0
- package/src/domain/graph/builder/stages/insert-nodes.js +195 -0
- package/src/domain/graph/builder/stages/parse-files.js +28 -0
- package/src/domain/graph/builder/stages/resolve-imports.js +143 -0
- package/src/domain/graph/builder/stages/run-analyses.js +44 -0
- package/src/domain/graph/builder.js +11 -0
- package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
- package/src/domain/graph/cycles.js +82 -0
- package/src/{journal.js → domain/graph/journal.js} +1 -1
- package/src/{resolve.js → domain/graph/resolve.js} +3 -3
- package/src/{watcher.js → domain/graph/watcher.js} +10 -150
- package/src/{parser.js → domain/parser.js} +5 -5
- package/src/domain/queries.js +48 -0
- package/src/domain/search/generator.js +163 -0
- package/src/domain/search/index.js +13 -0
- package/src/domain/search/models.js +218 -0
- package/src/domain/search/search/cli-formatter.js +151 -0
- package/src/domain/search/search/filters.js +46 -0
- package/src/domain/search/search/hybrid.js +121 -0
- package/src/domain/search/search/keyword.js +68 -0
- package/src/domain/search/search/prepare.js +66 -0
- package/src/domain/search/search/semantic.js +145 -0
- package/src/domain/search/stores/fts5.js +27 -0
- package/src/domain/search/stores/sqlite-blob.js +24 -0
- package/src/domain/search/strategies/source.js +14 -0
- package/src/domain/search/strategies/structured.js +43 -0
- package/src/domain/search/strategies/text-utils.js +43 -0
- package/src/extractors/csharp.js +10 -2
- package/src/extractors/go.js +3 -1
- package/src/extractors/helpers.js +71 -0
- package/src/extractors/java.js +9 -2
- package/src/extractors/javascript.js +39 -2
- package/src/extractors/php.js +3 -1
- package/src/extractors/python.js +14 -3
- package/src/extractors/rust.js +3 -1
- package/src/{ast.js → features/ast.js} +8 -8
- package/src/{audit.js → features/audit.js} +16 -44
- package/src/{batch.js → features/batch.js} +6 -5
- package/src/{boundaries.js → features/boundaries.js} +2 -2
- package/src/{branch-compare.js → features/branch-compare.js} +3 -3
- package/src/{cfg.js → features/cfg.js} +11 -12
- package/src/{check.js → features/check.js} +13 -30
- package/src/{cochange.js → features/cochange.js} +5 -5
- package/src/{communities.js → features/communities.js} +18 -90
- package/src/{complexity.js → features/complexity.js} +13 -13
- package/src/{dataflow.js → features/dataflow.js} +12 -13
- package/src/features/export.js +378 -0
- package/src/{flow.js → features/flow.js} +4 -4
- package/src/features/graph-enrichment.js +327 -0
- package/src/{manifesto.js → features/manifesto.js} +6 -6
- package/src/{owners.js → features/owners.js} +2 -2
- package/src/{sequence.js → features/sequence.js} +16 -52
- package/src/{snapshot.js → features/snapshot.js} +8 -7
- package/src/{structure.js → features/structure.js} +20 -45
- package/src/{triage.js → features/triage.js} +27 -79
- package/src/graph/algorithms/bfs.js +49 -0
- package/src/graph/algorithms/centrality.js +16 -0
- package/src/graph/algorithms/index.js +5 -0
- package/src/graph/algorithms/louvain.js +26 -0
- package/src/graph/algorithms/shortest-path.js +41 -0
- package/src/graph/algorithms/tarjan.js +49 -0
- package/src/graph/builders/dependency.js +110 -0
- package/src/graph/builders/index.js +3 -0
- package/src/graph/builders/structure.js +40 -0
- package/src/graph/builders/temporal.js +33 -0
- package/src/graph/classifiers/index.js +2 -0
- package/src/graph/classifiers/risk.js +85 -0
- package/src/graph/classifiers/roles.js +64 -0
- package/src/graph/index.js +13 -0
- package/src/graph/model.js +230 -0
- package/src/index.cjs +16 -0
- package/src/index.js +42 -219
- package/src/{native.js → infrastructure/native.js} +3 -1
- package/src/infrastructure/result-formatter.js +2 -21
- package/src/mcp/index.js +2 -0
- package/src/mcp/middleware.js +26 -0
- package/src/mcp/server.js +128 -0
- package/src/{mcp.js → mcp/tool-registry.js} +6 -675
- package/src/mcp/tools/ast-query.js +14 -0
- package/src/mcp/tools/audit.js +21 -0
- package/src/mcp/tools/batch-query.js +11 -0
- package/src/mcp/tools/branch-compare.js +12 -0
- package/src/mcp/tools/cfg.js +21 -0
- package/src/mcp/tools/check.js +43 -0
- package/src/mcp/tools/co-changes.js +20 -0
- package/src/mcp/tools/code-owners.js +12 -0
- package/src/mcp/tools/communities.js +15 -0
- package/src/mcp/tools/complexity.js +18 -0
- package/src/mcp/tools/context.js +17 -0
- package/src/mcp/tools/dataflow.js +26 -0
- package/src/mcp/tools/diff-impact.js +24 -0
- package/src/mcp/tools/execution-flow.js +26 -0
- package/src/mcp/tools/export-graph.js +57 -0
- package/src/mcp/tools/file-deps.js +12 -0
- package/src/mcp/tools/file-exports.js +13 -0
- package/src/mcp/tools/find-cycles.js +15 -0
- package/src/mcp/tools/fn-impact.js +15 -0
- package/src/mcp/tools/impact-analysis.js +12 -0
- package/src/mcp/tools/index.js +71 -0
- package/src/mcp/tools/list-functions.js +14 -0
- package/src/mcp/tools/list-repos.js +11 -0
- package/src/mcp/tools/module-map.js +6 -0
- package/src/mcp/tools/node-roles.js +14 -0
- package/src/mcp/tools/path.js +12 -0
- package/src/mcp/tools/query.js +30 -0
- package/src/mcp/tools/semantic-search.js +65 -0
- package/src/mcp/tools/sequence.js +17 -0
- package/src/mcp/tools/structure.js +15 -0
- package/src/mcp/tools/symbol-children.js +14 -0
- package/src/mcp/tools/triage.js +35 -0
- package/src/mcp/tools/where.js +13 -0
- package/src/{commands → presentation}/audit.js +2 -2
- package/src/{commands → presentation}/batch.js +1 -1
- package/src/{commands → presentation}/branch-compare.js +2 -2
- package/src/{commands → presentation}/cfg.js +1 -1
- package/src/{commands → presentation}/check.js +6 -6
- package/src/presentation/colors.js +44 -0
- package/src/{commands → presentation}/communities.js +1 -1
- package/src/{commands → presentation}/complexity.js +1 -1
- package/src/{commands → presentation}/dataflow.js +1 -1
- package/src/presentation/export.js +444 -0
- package/src/{commands → presentation}/flow.js +2 -2
- package/src/{commands → presentation}/manifesto.js +4 -4
- package/src/{commands → presentation}/owners.js +1 -1
- package/src/presentation/queries-cli/exports.js +46 -0
- package/src/presentation/queries-cli/impact.js +198 -0
- package/src/presentation/queries-cli/index.js +5 -0
- package/src/presentation/queries-cli/inspect.js +334 -0
- package/src/presentation/queries-cli/overview.js +197 -0
- package/src/presentation/queries-cli/path.js +58 -0
- package/src/presentation/queries-cli.js +27 -0
- package/src/{commands → presentation}/query.js +1 -1
- package/src/presentation/result-formatter.js +144 -0
- package/src/presentation/sequence-renderer.js +43 -0
- package/src/{commands → presentation}/sequence.js +2 -2
- package/src/{commands → presentation}/structure.js +2 -2
- package/src/presentation/table.js +47 -0
- package/src/{commands → presentation}/triage.js +1 -1
- package/src/{viewer.js → presentation/viewer.js} +68 -382
- package/src/{constants.js → shared/constants.js} +1 -1
- package/src/shared/errors.js +78 -0
- package/src/shared/file-utils.js +153 -0
- package/src/shared/generators.js +125 -0
- package/src/shared/hierarchy.js +27 -0
- package/src/shared/normalize.js +59 -0
- package/src/builder.js +0 -1486
- package/src/cycles.js +0 -137
- package/src/embedder.js +0 -1097
- package/src/export.js +0 -681
- package/src/queries-cli.js +0 -866
- package/src/queries.js +0 -2289
- /package/src/{config.js → infrastructure/config.js} +0 -0
- /package/src/{logger.js → infrastructure/logger.js} +0 -0
- /package/src/{registry.js → infrastructure/registry.js} +0 -0
- /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
- /package/src/{commands → presentation}/cochange.js +0 -0
- /package/src/{kinds.js → shared/kinds.js} +0 -0
- /package/src/{paginate.js → shared/paginate.js} +0 -0
|
@@ -1,39 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { warn } from '
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
// ─── Constants ────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
const DEFAULT_WEIGHTS = {
|
|
9
|
-
fanIn: 0.25,
|
|
10
|
-
complexity: 0.3,
|
|
11
|
-
churn: 0.2,
|
|
12
|
-
role: 0.15,
|
|
13
|
-
mi: 0.1,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const ROLE_WEIGHTS = {
|
|
17
|
-
core: 1.0,
|
|
18
|
-
utility: 0.9,
|
|
19
|
-
entry: 0.8,
|
|
20
|
-
adapter: 0.5,
|
|
21
|
-
leaf: 0.2,
|
|
22
|
-
dead: 0.1,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const DEFAULT_ROLE_WEIGHT = 0.5;
|
|
26
|
-
|
|
27
|
-
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
/** Min-max normalize an array of numbers. All-equal → all zeros. */
|
|
30
|
-
function minMaxNormalize(values) {
|
|
31
|
-
const min = Math.min(...values);
|
|
32
|
-
const max = Math.max(...values);
|
|
33
|
-
if (max === min) return values.map(() => 0);
|
|
34
|
-
const range = max - min;
|
|
35
|
-
return values.map((v) => (v - min) / range);
|
|
36
|
-
}
|
|
1
|
+
import { openRepo } from '../db/index.js';
|
|
2
|
+
import { DEFAULT_WEIGHTS, scoreRisk } from '../graph/classifiers/risk.js';
|
|
3
|
+
import { warn } from '../infrastructure/logger.js';
|
|
4
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
5
|
+
import { paginateResult } from '../shared/paginate.js';
|
|
37
6
|
|
|
38
7
|
// ─── Data Function ────────────────────────────────────────────────────
|
|
39
8
|
|
|
@@ -45,7 +14,7 @@ function minMaxNormalize(values) {
|
|
|
45
14
|
* @returns {{ items: object[], summary: object, _pagination?: object }}
|
|
46
15
|
*/
|
|
47
16
|
export function triageData(customDbPath, opts = {}) {
|
|
48
|
-
const
|
|
17
|
+
const { repo, close } = openRepo(customDbPath, opts);
|
|
49
18
|
try {
|
|
50
19
|
const noTests = opts.noTests || false;
|
|
51
20
|
const fileFilter = opts.file || null;
|
|
@@ -57,7 +26,7 @@ export function triageData(customDbPath, opts = {}) {
|
|
|
57
26
|
|
|
58
27
|
let rows;
|
|
59
28
|
try {
|
|
60
|
-
rows = findNodesForTriage(
|
|
29
|
+
rows = repo.findNodesForTriage({
|
|
61
30
|
noTests,
|
|
62
31
|
file: fileFilter,
|
|
63
32
|
kind: kindFilter,
|
|
@@ -81,48 +50,27 @@ export function triageData(customDbPath, opts = {}) {
|
|
|
81
50
|
};
|
|
82
51
|
}
|
|
83
52
|
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
const cognitives = filtered.map((r) => r.cognitive);
|
|
87
|
-
const churns = filtered.map((r) => r.churn);
|
|
88
|
-
const mis = filtered.map((r) => r.mi);
|
|
89
|
-
|
|
90
|
-
// Min-max normalize
|
|
91
|
-
const normFanIns = minMaxNormalize(fanIns);
|
|
92
|
-
const normCognitives = minMaxNormalize(cognitives);
|
|
93
|
-
const normChurns = minMaxNormalize(churns);
|
|
94
|
-
// MI: higher is better, so invert: 1 - norm(mi)
|
|
95
|
-
const normMIsRaw = minMaxNormalize(mis);
|
|
96
|
-
const normMIs = normMIsRaw.map((v) => round4(1 - v));
|
|
53
|
+
// Delegate scoring to classifier
|
|
54
|
+
const riskMetrics = scoreRisk(filtered, weights);
|
|
97
55
|
|
|
98
56
|
// Compute risk scores
|
|
99
|
-
const items = filtered.map((r, i) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
churn: r.churn,
|
|
117
|
-
maintainabilityIndex: r.mi,
|
|
118
|
-
normFanIn: round4(normFanIns[i]),
|
|
119
|
-
normComplexity: round4(normCognitives[i]),
|
|
120
|
-
normChurn: round4(normChurns[i]),
|
|
121
|
-
normMI: round4(normMIs[i]),
|
|
122
|
-
roleWeight,
|
|
123
|
-
riskScore: round4(riskScore),
|
|
124
|
-
};
|
|
125
|
-
});
|
|
57
|
+
const items = filtered.map((r, i) => ({
|
|
58
|
+
name: r.name,
|
|
59
|
+
kind: r.kind,
|
|
60
|
+
file: r.file,
|
|
61
|
+
line: r.line,
|
|
62
|
+
role: r.role || null,
|
|
63
|
+
fanIn: r.fan_in,
|
|
64
|
+
cognitive: r.cognitive,
|
|
65
|
+
churn: r.churn,
|
|
66
|
+
maintainabilityIndex: r.mi,
|
|
67
|
+
normFanIn: riskMetrics[i].normFanIn,
|
|
68
|
+
normComplexity: riskMetrics[i].normComplexity,
|
|
69
|
+
normChurn: riskMetrics[i].normChurn,
|
|
70
|
+
normMI: riskMetrics[i].normMI,
|
|
71
|
+
roleWeight: riskMetrics[i].roleWeight,
|
|
72
|
+
riskScore: riskMetrics[i].riskScore,
|
|
73
|
+
}));
|
|
126
74
|
|
|
127
75
|
// Apply minScore filter
|
|
128
76
|
const scored = minScore != null ? items.filter((it) => it.riskScore >= minScore) : items;
|
|
@@ -167,7 +115,7 @@ export function triageData(customDbPath, opts = {}) {
|
|
|
167
115
|
offset: opts.offset,
|
|
168
116
|
});
|
|
169
117
|
} finally {
|
|
170
|
-
|
|
118
|
+
close();
|
|
171
119
|
}
|
|
172
120
|
}
|
|
173
121
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breadth-first traversal on a CodeGraph.
|
|
3
|
+
*
|
|
4
|
+
* @param {import('../model.js').CodeGraph} graph
|
|
5
|
+
* @param {string|string[]} startIds - One or more starting node IDs
|
|
6
|
+
* @param {{ maxDepth?: number, direction?: 'forward'|'backward'|'both' }} [opts]
|
|
7
|
+
* @returns {Map<string, number>} nodeId → depth from nearest start node
|
|
8
|
+
*/
|
|
9
|
+
export function bfs(graph, startIds, opts = {}) {
|
|
10
|
+
const maxDepth = opts.maxDepth ?? Infinity;
|
|
11
|
+
const direction = opts.direction ?? 'forward';
|
|
12
|
+
const starts = Array.isArray(startIds) ? startIds : [startIds];
|
|
13
|
+
|
|
14
|
+
const depths = new Map();
|
|
15
|
+
const queue = [];
|
|
16
|
+
|
|
17
|
+
for (const id of starts) {
|
|
18
|
+
const key = String(id);
|
|
19
|
+
if (graph.hasNode(key)) {
|
|
20
|
+
depths.set(key, 0);
|
|
21
|
+
queue.push(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let head = 0;
|
|
26
|
+
while (head < queue.length) {
|
|
27
|
+
const current = queue[head++];
|
|
28
|
+
const depth = depths.get(current);
|
|
29
|
+
if (depth >= maxDepth) continue;
|
|
30
|
+
|
|
31
|
+
let neighbors;
|
|
32
|
+
if (direction === 'forward') {
|
|
33
|
+
neighbors = graph.successors(current);
|
|
34
|
+
} else if (direction === 'backward') {
|
|
35
|
+
neighbors = graph.predecessors(current);
|
|
36
|
+
} else {
|
|
37
|
+
neighbors = graph.neighbors(current);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const n of neighbors) {
|
|
41
|
+
if (!depths.has(n)) {
|
|
42
|
+
depths.set(n, depth + 1);
|
|
43
|
+
queue.push(n);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return depths;
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fan-in / fan-out centrality for all nodes in a CodeGraph.
|
|
3
|
+
*
|
|
4
|
+
* @param {import('../model.js').CodeGraph} graph
|
|
5
|
+
* @returns {Map<string, { fanIn: number, fanOut: number }>}
|
|
6
|
+
*/
|
|
7
|
+
export function fanInOut(graph) {
|
|
8
|
+
const result = new Map();
|
|
9
|
+
for (const id of graph.nodeIds()) {
|
|
10
|
+
result.set(id, {
|
|
11
|
+
fanIn: graph.inDegree(id),
|
|
12
|
+
fanOut: graph.outDegree(id),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Louvain community detection via graphology.
|
|
3
|
+
*
|
|
4
|
+
* @param {import('../model.js').CodeGraph} graph
|
|
5
|
+
* @param {{ resolution?: number }} [opts]
|
|
6
|
+
* @returns {{ assignments: Map<string, number>, modularity: number }}
|
|
7
|
+
*/
|
|
8
|
+
import graphologyLouvain from 'graphology-communities-louvain';
|
|
9
|
+
|
|
10
|
+
export function louvainCommunities(graph, opts = {}) {
|
|
11
|
+
const gy = graph.toGraphology({ type: 'undirected' });
|
|
12
|
+
|
|
13
|
+
if (gy.order === 0 || gy.size === 0) {
|
|
14
|
+
return { assignments: new Map(), modularity: 0 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const resolution = opts.resolution ?? 1.0;
|
|
18
|
+
const details = graphologyLouvain.detailed(gy, { resolution });
|
|
19
|
+
|
|
20
|
+
const assignments = new Map();
|
|
21
|
+
for (const [nodeId, communityId] of Object.entries(details.communities)) {
|
|
22
|
+
assignments.set(nodeId, communityId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { assignments, modularity: details.modularity };
|
|
26
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BFS-based shortest path on a CodeGraph.
|
|
3
|
+
*
|
|
4
|
+
* @param {import('../model.js').CodeGraph} graph
|
|
5
|
+
* @param {string} fromId
|
|
6
|
+
* @param {string} toId
|
|
7
|
+
* @returns {string[]|null} Path from fromId to toId (inclusive), or null if unreachable
|
|
8
|
+
*/
|
|
9
|
+
export function shortestPath(graph, fromId, toId) {
|
|
10
|
+
const from = String(fromId);
|
|
11
|
+
const to = String(toId);
|
|
12
|
+
|
|
13
|
+
if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
|
|
14
|
+
if (from === to) return [from];
|
|
15
|
+
|
|
16
|
+
const parent = new Map();
|
|
17
|
+
parent.set(from, null);
|
|
18
|
+
const queue = [from];
|
|
19
|
+
let head = 0;
|
|
20
|
+
|
|
21
|
+
while (head < queue.length) {
|
|
22
|
+
const current = queue[head++];
|
|
23
|
+
for (const neighbor of graph.successors(current)) {
|
|
24
|
+
if (parent.has(neighbor)) continue;
|
|
25
|
+
parent.set(neighbor, current);
|
|
26
|
+
if (neighbor === to) {
|
|
27
|
+
// Reconstruct path
|
|
28
|
+
const path = [];
|
|
29
|
+
let node = to;
|
|
30
|
+
while (node !== null) {
|
|
31
|
+
path.push(node);
|
|
32
|
+
node = parent.get(node);
|
|
33
|
+
}
|
|
34
|
+
return path.reverse();
|
|
35
|
+
}
|
|
36
|
+
queue.push(neighbor);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tarjan's strongly connected components algorithm.
|
|
3
|
+
* Operates on a CodeGraph instance.
|
|
4
|
+
*
|
|
5
|
+
* @param {import('../model.js').CodeGraph} graph
|
|
6
|
+
* @returns {string[][]} SCCs with length > 1 (cycles)
|
|
7
|
+
*/
|
|
8
|
+
export function tarjan(graph) {
|
|
9
|
+
let index = 0;
|
|
10
|
+
const stack = [];
|
|
11
|
+
const onStack = new Set();
|
|
12
|
+
const indices = new Map();
|
|
13
|
+
const lowlinks = new Map();
|
|
14
|
+
const sccs = [];
|
|
15
|
+
|
|
16
|
+
function strongconnect(v) {
|
|
17
|
+
indices.set(v, index);
|
|
18
|
+
lowlinks.set(v, index);
|
|
19
|
+
index++;
|
|
20
|
+
stack.push(v);
|
|
21
|
+
onStack.add(v);
|
|
22
|
+
|
|
23
|
+
for (const w of graph.successors(v)) {
|
|
24
|
+
if (!indices.has(w)) {
|
|
25
|
+
strongconnect(w);
|
|
26
|
+
lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
|
|
27
|
+
} else if (onStack.has(w)) {
|
|
28
|
+
lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (lowlinks.get(v) === indices.get(v)) {
|
|
33
|
+
const scc = [];
|
|
34
|
+
let w;
|
|
35
|
+
do {
|
|
36
|
+
w = stack.pop();
|
|
37
|
+
onStack.delete(w);
|
|
38
|
+
scc.push(w);
|
|
39
|
+
} while (w !== v);
|
|
40
|
+
if (scc.length > 1) sccs.push(scc);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const id of graph.nodeIds()) {
|
|
45
|
+
if (!indices.has(id)) strongconnect(id);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return sccs;
|
|
49
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a CodeGraph from the SQLite database.
|
|
3
|
+
* Replaces inline graph construction in cycles.js, communities.js, viewer.js, export.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getCallableNodes,
|
|
8
|
+
getCallEdges,
|
|
9
|
+
getFileNodesAll,
|
|
10
|
+
getImportEdges,
|
|
11
|
+
Repository,
|
|
12
|
+
} from '../../db/index.js';
|
|
13
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
14
|
+
import { CodeGraph } from '../model.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} dbOrRepo - Open better-sqlite3 database (readonly) or a Repository instance
|
|
18
|
+
* @param {object} [opts]
|
|
19
|
+
* @param {boolean} [opts.fileLevel=true] - File-level (imports) or function-level (calls)
|
|
20
|
+
* @param {boolean} [opts.noTests=false] - Exclude test files
|
|
21
|
+
* @param {number} [opts.minConfidence] - Minimum edge confidence (function-level only)
|
|
22
|
+
* @returns {CodeGraph}
|
|
23
|
+
*/
|
|
24
|
+
export function buildDependencyGraph(dbOrRepo, opts = {}) {
|
|
25
|
+
const fileLevel = opts.fileLevel !== false;
|
|
26
|
+
const noTests = opts.noTests || false;
|
|
27
|
+
|
|
28
|
+
if (fileLevel) {
|
|
29
|
+
return buildFileLevelGraph(dbOrRepo, noTests);
|
|
30
|
+
}
|
|
31
|
+
return buildFunctionLevelGraph(dbOrRepo, noTests, opts.minConfidence);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildFileLevelGraph(dbOrRepo, noTests) {
|
|
35
|
+
const graph = new CodeGraph();
|
|
36
|
+
const isRepo = dbOrRepo instanceof Repository;
|
|
37
|
+
|
|
38
|
+
let nodes = isRepo ? dbOrRepo.getFileNodesAll() : getFileNodesAll(dbOrRepo);
|
|
39
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
40
|
+
|
|
41
|
+
const nodeIds = new Set();
|
|
42
|
+
for (const n of nodes) {
|
|
43
|
+
graph.addNode(String(n.id), { label: n.file, file: n.file, dbId: n.id });
|
|
44
|
+
nodeIds.add(n.id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const edges = isRepo ? dbOrRepo.getImportEdges() : getImportEdges(dbOrRepo);
|
|
48
|
+
for (const e of edges) {
|
|
49
|
+
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
50
|
+
const src = String(e.source_id);
|
|
51
|
+
const tgt = String(e.target_id);
|
|
52
|
+
if (src === tgt) continue;
|
|
53
|
+
if (!graph.hasEdge(src, tgt)) {
|
|
54
|
+
graph.addEdge(src, tgt, { kind: 'imports' });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return graph;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildFunctionLevelGraph(dbOrRepo, noTests, minConfidence) {
|
|
62
|
+
const graph = new CodeGraph();
|
|
63
|
+
const isRepo = dbOrRepo instanceof Repository;
|
|
64
|
+
|
|
65
|
+
let nodes = isRepo ? dbOrRepo.getCallableNodes() : getCallableNodes(dbOrRepo);
|
|
66
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
67
|
+
|
|
68
|
+
const nodeIds = new Set();
|
|
69
|
+
for (const n of nodes) {
|
|
70
|
+
graph.addNode(String(n.id), {
|
|
71
|
+
label: n.name,
|
|
72
|
+
file: n.file,
|
|
73
|
+
kind: n.kind,
|
|
74
|
+
dbId: n.id,
|
|
75
|
+
});
|
|
76
|
+
nodeIds.add(n.id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let edges;
|
|
80
|
+
if (minConfidence != null) {
|
|
81
|
+
if (isRepo) {
|
|
82
|
+
// Trade-off: Repository.getCallEdges() returns all call edges, so we
|
|
83
|
+
// filter in JS. This is O(all call edges) rather than the SQL path's
|
|
84
|
+
// indexed WHERE clause. Acceptable for current data sizes; a dedicated
|
|
85
|
+
// getCallEdgesByMinConfidence(threshold) method on the Repository
|
|
86
|
+
// interface would be the proper fix if this becomes a bottleneck.
|
|
87
|
+
edges = dbOrRepo
|
|
88
|
+
.getCallEdges()
|
|
89
|
+
.filter((e) => e.confidence != null && e.confidence >= minConfidence);
|
|
90
|
+
} else {
|
|
91
|
+
edges = dbOrRepo
|
|
92
|
+
.prepare("SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND confidence >= ?")
|
|
93
|
+
.all(minConfidence);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
edges = isRepo ? dbOrRepo.getCallEdges() : getCallEdges(dbOrRepo);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const e of edges) {
|
|
100
|
+
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
|
|
101
|
+
const src = String(e.source_id);
|
|
102
|
+
const tgt = String(e.target_id);
|
|
103
|
+
if (src === tgt) continue;
|
|
104
|
+
if (!graph.hasEdge(src, tgt)) {
|
|
105
|
+
graph.addEdge(src, tgt, { kind: 'calls' });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return graph;
|
|
110
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a containment graph (directory → file) from the SQLite database.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CodeGraph } from '../model.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} db - Open better-sqlite3 database (readonly)
|
|
9
|
+
* @returns {CodeGraph} Directed graph with directory→file containment edges
|
|
10
|
+
*/
|
|
11
|
+
export function buildStructureGraph(db) {
|
|
12
|
+
const graph = new CodeGraph();
|
|
13
|
+
|
|
14
|
+
const dirs = db.prepare("SELECT id, name FROM nodes WHERE kind = 'directory'").all();
|
|
15
|
+
|
|
16
|
+
for (const d of dirs) {
|
|
17
|
+
graph.addNode(String(d.id), { label: d.name, kind: 'directory' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const files = db.prepare("SELECT id, name, file FROM nodes WHERE kind = 'file'").all();
|
|
21
|
+
|
|
22
|
+
for (const f of files) {
|
|
23
|
+
graph.addNode(String(f.id), { label: f.name, kind: 'file', file: f.file });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const containsEdges = db
|
|
27
|
+
.prepare(`
|
|
28
|
+
SELECT e.source_id, e.target_id
|
|
29
|
+
FROM edges e
|
|
30
|
+
JOIN nodes n ON e.source_id = n.id
|
|
31
|
+
WHERE e.kind = 'contains' AND n.kind = 'directory'
|
|
32
|
+
`)
|
|
33
|
+
.all();
|
|
34
|
+
|
|
35
|
+
for (const e of containsEdges) {
|
|
36
|
+
graph.addEdge(String(e.source_id), String(e.target_id), { kind: 'contains' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return graph;
|
|
40
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a co-change (temporal) graph weighted by Jaccard similarity.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CodeGraph } from '../model.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} db - Open better-sqlite3 database (readonly)
|
|
9
|
+
* @param {{ minJaccard?: number }} [opts]
|
|
10
|
+
* @returns {CodeGraph} Undirected graph weighted by Jaccard similarity
|
|
11
|
+
*/
|
|
12
|
+
export function buildTemporalGraph(db, opts = {}) {
|
|
13
|
+
const minJaccard = opts.minJaccard ?? 0.0;
|
|
14
|
+
const graph = new CodeGraph({ directed: false });
|
|
15
|
+
|
|
16
|
+
// Check if co_changes table exists
|
|
17
|
+
const tableCheck = db
|
|
18
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='co_changes'")
|
|
19
|
+
.get();
|
|
20
|
+
if (!tableCheck) return graph;
|
|
21
|
+
|
|
22
|
+
const rows = db
|
|
23
|
+
.prepare('SELECT file_a, file_b, jaccard FROM co_changes WHERE jaccard >= ?')
|
|
24
|
+
.all(minJaccard);
|
|
25
|
+
|
|
26
|
+
for (const r of rows) {
|
|
27
|
+
if (!graph.hasNode(r.file_a)) graph.addNode(r.file_a, { label: r.file_a });
|
|
28
|
+
if (!graph.hasNode(r.file_b)) graph.addNode(r.file_b, { label: r.file_b });
|
|
29
|
+
graph.addEdge(r.file_a, r.file_b, { jaccard: r.jaccard });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return graph;
|
|
33
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Risk scoring — pure logic, no DB.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Weights sum to 1.0. Complexity gets the highest weight because cognitive load
|
|
6
|
+
// is the strongest predictor of defect density. Fan-in and churn are next as
|
|
7
|
+
// they reflect coupling and volatility. Role adds architectural context, and MI
|
|
8
|
+
// (maintainability index) is a weaker composite signal, so it gets the least.
|
|
9
|
+
export const DEFAULT_WEIGHTS = {
|
|
10
|
+
fanIn: 0.25,
|
|
11
|
+
complexity: 0.3,
|
|
12
|
+
churn: 0.2,
|
|
13
|
+
role: 0.15,
|
|
14
|
+
mi: 0.1,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Role weights reflect structural importance: core modules are central to the
|
|
18
|
+
// dependency graph, utilities are widely imported, entry points are API
|
|
19
|
+
// surfaces. Adapters bridge subsystems but are replaceable. Leaves and dead
|
|
20
|
+
// code have minimal downstream impact.
|
|
21
|
+
export const ROLE_WEIGHTS = {
|
|
22
|
+
core: 1.0,
|
|
23
|
+
utility: 0.9,
|
|
24
|
+
entry: 0.8,
|
|
25
|
+
adapter: 0.5,
|
|
26
|
+
leaf: 0.2,
|
|
27
|
+
dead: 0.1,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DEFAULT_ROLE_WEIGHT = 0.5;
|
|
31
|
+
|
|
32
|
+
/** Min-max normalize an array of numbers. All-equal → all zeros. */
|
|
33
|
+
export function minMaxNormalize(values) {
|
|
34
|
+
const min = Math.min(...values);
|
|
35
|
+
const max = Math.max(...values);
|
|
36
|
+
if (max === min) return values.map(() => 0);
|
|
37
|
+
const range = max - min;
|
|
38
|
+
return values.map((v) => (v - min) / range);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function round4(n) {
|
|
42
|
+
return Math.round(n * 10000) / 10000;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Score risk for a list of items.
|
|
47
|
+
*
|
|
48
|
+
* @param {{ fan_in: number, cognitive: number, churn: number, mi: number, role: string|null }[]} items
|
|
49
|
+
* @param {object} [weights] - Override DEFAULT_WEIGHTS
|
|
50
|
+
* @returns {{ normFanIn: number, normComplexity: number, normChurn: number, normMI: number, roleWeight: number, riskScore: number }[]}
|
|
51
|
+
* Parallel array with risk metrics for each input item.
|
|
52
|
+
*/
|
|
53
|
+
export function scoreRisk(items, weights = {}) {
|
|
54
|
+
const w = { ...DEFAULT_WEIGHTS, ...weights };
|
|
55
|
+
|
|
56
|
+
const fanIns = items.map((r) => r.fan_in);
|
|
57
|
+
const cognitives = items.map((r) => r.cognitive);
|
|
58
|
+
const churns = items.map((r) => r.churn);
|
|
59
|
+
const mis = items.map((r) => r.mi);
|
|
60
|
+
|
|
61
|
+
const normFanIns = minMaxNormalize(fanIns);
|
|
62
|
+
const normCognitives = minMaxNormalize(cognitives);
|
|
63
|
+
const normChurns = minMaxNormalize(churns);
|
|
64
|
+
const normMIsRaw = minMaxNormalize(mis);
|
|
65
|
+
const normMIs = normMIsRaw.map((v) => round4(1 - v));
|
|
66
|
+
|
|
67
|
+
return items.map((r, i) => {
|
|
68
|
+
const roleWeight = ROLE_WEIGHTS[r.role] ?? DEFAULT_ROLE_WEIGHT;
|
|
69
|
+
const riskScore =
|
|
70
|
+
w.fanIn * normFanIns[i] +
|
|
71
|
+
w.complexity * normCognitives[i] +
|
|
72
|
+
w.churn * normChurns[i] +
|
|
73
|
+
w.role * roleWeight +
|
|
74
|
+
w.mi * normMIs[i];
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
normFanIn: round4(normFanIns[i]),
|
|
78
|
+
normComplexity: round4(normCognitives[i]),
|
|
79
|
+
normChurn: round4(normChurns[i]),
|
|
80
|
+
normMI: round4(normMIs[i]),
|
|
81
|
+
roleWeight,
|
|
82
|
+
riskScore: round4(riskScore),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node role classification — pure logic, no DB.
|
|
3
|
+
*
|
|
4
|
+
* Roles: entry, core, utility, adapter, leaf, dead
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const FRAMEWORK_ENTRY_PREFIXES = ['route:', 'event:', 'command:'];
|
|
8
|
+
|
|
9
|
+
function median(sorted) {
|
|
10
|
+
if (sorted.length === 0) return 0;
|
|
11
|
+
const mid = Math.floor(sorted.length / 2);
|
|
12
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Classify nodes into architectural roles based on fan-in/fan-out metrics.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ id: string, name: string, fanIn: number, fanOut: number, isExported: boolean }[]} nodes
|
|
19
|
+
* @returns {Map<string, string>} nodeId → role
|
|
20
|
+
*/
|
|
21
|
+
export function classifyRoles(nodes) {
|
|
22
|
+
if (nodes.length === 0) return new Map();
|
|
23
|
+
|
|
24
|
+
const nonZeroFanIn = nodes
|
|
25
|
+
.filter((n) => n.fanIn > 0)
|
|
26
|
+
.map((n) => n.fanIn)
|
|
27
|
+
.sort((a, b) => a - b);
|
|
28
|
+
const nonZeroFanOut = nodes
|
|
29
|
+
.filter((n) => n.fanOut > 0)
|
|
30
|
+
.map((n) => n.fanOut)
|
|
31
|
+
.sort((a, b) => a - b);
|
|
32
|
+
|
|
33
|
+
const medFanIn = median(nonZeroFanIn);
|
|
34
|
+
const medFanOut = median(nonZeroFanOut);
|
|
35
|
+
|
|
36
|
+
const result = new Map();
|
|
37
|
+
|
|
38
|
+
for (const node of nodes) {
|
|
39
|
+
const highIn = node.fanIn >= medFanIn && node.fanIn > 0;
|
|
40
|
+
const highOut = node.fanOut >= medFanOut && node.fanOut > 0;
|
|
41
|
+
|
|
42
|
+
let role;
|
|
43
|
+
const isFrameworkEntry = FRAMEWORK_ENTRY_PREFIXES.some((p) => node.name.startsWith(p));
|
|
44
|
+
if (isFrameworkEntry) {
|
|
45
|
+
role = 'entry';
|
|
46
|
+
} else if (node.fanIn === 0 && !node.isExported) {
|
|
47
|
+
role = 'dead';
|
|
48
|
+
} else if (node.fanIn === 0 && node.isExported) {
|
|
49
|
+
role = 'entry';
|
|
50
|
+
} else if (highIn && !highOut) {
|
|
51
|
+
role = 'core';
|
|
52
|
+
} else if (highIn && highOut) {
|
|
53
|
+
role = 'utility';
|
|
54
|
+
} else if (!highIn && highOut) {
|
|
55
|
+
role = 'adapter';
|
|
56
|
+
} else {
|
|
57
|
+
role = 'leaf';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
result.set(node.id, role);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Graph subsystem barrel export
|
|
2
|
+
|
|
3
|
+
export { bfs, fanInOut, louvainCommunities, shortestPath, tarjan } from './algorithms/index.js';
|
|
4
|
+
export { buildDependencyGraph, buildStructureGraph, buildTemporalGraph } from './builders/index.js';
|
|
5
|
+
export {
|
|
6
|
+
classifyRoles,
|
|
7
|
+
DEFAULT_WEIGHTS,
|
|
8
|
+
FRAMEWORK_ENTRY_PREFIXES,
|
|
9
|
+
minMaxNormalize,
|
|
10
|
+
ROLE_WEIGHTS,
|
|
11
|
+
scoreRisk,
|
|
12
|
+
} from './classifiers/index.js';
|
|
13
|
+
export { CodeGraph } from './model.js';
|