@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
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { louvainCommunities } from '../graph/algorithms/louvain.js';
|
|
3
|
+
import { CodeGraph } from '../graph/model.js';
|
|
4
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
5
|
+
import {
|
|
6
|
+
COMMUNITY_COLORS,
|
|
7
|
+
DEFAULT_NODE_COLORS,
|
|
8
|
+
DEFAULT_ROLE_COLORS,
|
|
9
|
+
} from '../presentation/colors.js';
|
|
10
|
+
import { DEFAULT_CONFIG, renderPlotHTML } from '../presentation/viewer.js';
|
|
11
|
+
|
|
12
|
+
// Re-export presentation utilities for backward compatibility
|
|
13
|
+
export { loadPlotConfig } from '../presentation/viewer.js';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_MIN_CONFIDENCE = 0.5;
|
|
16
|
+
|
|
17
|
+
// ─── Data Preparation ─────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Prepare enriched graph data for the HTML viewer.
|
|
21
|
+
*/
|
|
22
|
+
export function prepareGraphData(db, opts = {}) {
|
|
23
|
+
const fileLevel = opts.fileLevel !== false;
|
|
24
|
+
const noTests = opts.noTests || false;
|
|
25
|
+
const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
26
|
+
const cfg = opts.config || DEFAULT_CONFIG;
|
|
27
|
+
|
|
28
|
+
return fileLevel
|
|
29
|
+
? prepareFileLevelData(db, noTests, minConf, cfg)
|
|
30
|
+
: prepareFunctionLevelData(db, noTests, minConf, cfg);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function prepareFunctionLevelData(db, noTests, minConf, cfg) {
|
|
34
|
+
let edges = db
|
|
35
|
+
.prepare(
|
|
36
|
+
`
|
|
37
|
+
SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
|
|
38
|
+
n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
|
|
39
|
+
n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
|
|
40
|
+
n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
|
|
41
|
+
e.kind AS edge_kind
|
|
42
|
+
FROM edges e
|
|
43
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
44
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
45
|
+
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
46
|
+
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
47
|
+
AND e.kind = 'calls'
|
|
48
|
+
AND e.confidence >= ?
|
|
49
|
+
`,
|
|
50
|
+
)
|
|
51
|
+
.all(minConf);
|
|
52
|
+
if (noTests)
|
|
53
|
+
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
54
|
+
|
|
55
|
+
if (cfg.filter.kinds) {
|
|
56
|
+
const kinds = new Set(cfg.filter.kinds);
|
|
57
|
+
edges = edges.filter((e) => kinds.has(e.source_kind) && kinds.has(e.target_kind));
|
|
58
|
+
}
|
|
59
|
+
if (cfg.filter.files) {
|
|
60
|
+
const patterns = cfg.filter.files;
|
|
61
|
+
edges = edges.filter(
|
|
62
|
+
(e) =>
|
|
63
|
+
patterns.some((p) => e.source_file.includes(p)) &&
|
|
64
|
+
patterns.some((p) => e.target_file.includes(p)),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const nodeMap = new Map();
|
|
69
|
+
for (const e of edges) {
|
|
70
|
+
if (!nodeMap.has(e.source_id)) {
|
|
71
|
+
nodeMap.set(e.source_id, {
|
|
72
|
+
id: e.source_id,
|
|
73
|
+
name: e.source_name,
|
|
74
|
+
kind: e.source_kind,
|
|
75
|
+
file: e.source_file,
|
|
76
|
+
line: e.source_line,
|
|
77
|
+
role: e.source_role,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (!nodeMap.has(e.target_id)) {
|
|
81
|
+
nodeMap.set(e.target_id, {
|
|
82
|
+
id: e.target_id,
|
|
83
|
+
name: e.target_name,
|
|
84
|
+
kind: e.target_kind,
|
|
85
|
+
file: e.target_file,
|
|
86
|
+
line: e.target_line,
|
|
87
|
+
role: e.target_role,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (cfg.filter.roles) {
|
|
93
|
+
const roles = new Set(cfg.filter.roles);
|
|
94
|
+
for (const [id, n] of nodeMap) {
|
|
95
|
+
if (!roles.has(n.role)) nodeMap.delete(id);
|
|
96
|
+
}
|
|
97
|
+
const nodeIds = new Set(nodeMap.keys());
|
|
98
|
+
edges = edges.filter((e) => nodeIds.has(e.source_id) && nodeIds.has(e.target_id));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Complexity data
|
|
102
|
+
const complexityMap = new Map();
|
|
103
|
+
try {
|
|
104
|
+
const rows = db
|
|
105
|
+
.prepare(
|
|
106
|
+
'SELECT node_id, cognitive, cyclomatic, max_nesting, maintainability_index FROM function_complexity',
|
|
107
|
+
)
|
|
108
|
+
.all();
|
|
109
|
+
for (const r of rows) {
|
|
110
|
+
complexityMap.set(r.node_id, {
|
|
111
|
+
cognitive: r.cognitive,
|
|
112
|
+
cyclomatic: r.cyclomatic,
|
|
113
|
+
maintainabilityIndex: r.maintainability_index,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// table may not exist in old DBs
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fan-in / fan-out via graph subsystem
|
|
121
|
+
const fnGraph = new CodeGraph();
|
|
122
|
+
for (const [id] of nodeMap) fnGraph.addNode(String(id));
|
|
123
|
+
for (const e of edges) {
|
|
124
|
+
const src = String(e.source_id);
|
|
125
|
+
const tgt = String(e.target_id);
|
|
126
|
+
if (src !== tgt && !fnGraph.hasEdge(src, tgt)) fnGraph.addEdge(src, tgt);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Use DB-level fan-in/fan-out (counts ALL call edges, not just visible)
|
|
130
|
+
const fanInMap = new Map();
|
|
131
|
+
const fanOutMap = new Map();
|
|
132
|
+
const fanInRows = db
|
|
133
|
+
.prepare(
|
|
134
|
+
"SELECT target_id AS node_id, COUNT(*) AS fan_in FROM edges WHERE kind = 'calls' GROUP BY target_id",
|
|
135
|
+
)
|
|
136
|
+
.all();
|
|
137
|
+
for (const r of fanInRows) fanInMap.set(r.node_id, r.fan_in);
|
|
138
|
+
|
|
139
|
+
const fanOutRows = db
|
|
140
|
+
.prepare(
|
|
141
|
+
"SELECT source_id AS node_id, COUNT(*) AS fan_out FROM edges WHERE kind = 'calls' GROUP BY source_id",
|
|
142
|
+
)
|
|
143
|
+
.all();
|
|
144
|
+
for (const r of fanOutRows) fanOutMap.set(r.node_id, r.fan_out);
|
|
145
|
+
|
|
146
|
+
// Communities (Louvain) via graph subsystem
|
|
147
|
+
const communityMap = new Map();
|
|
148
|
+
if (nodeMap.size > 0) {
|
|
149
|
+
try {
|
|
150
|
+
const { assignments } = louvainCommunities(fnGraph);
|
|
151
|
+
for (const [nid, cid] of assignments) communityMap.set(Number(nid), cid);
|
|
152
|
+
} catch {
|
|
153
|
+
// louvain can fail on disconnected graphs
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Build enriched nodes
|
|
158
|
+
const visNodes = [...nodeMap.values()].map((n) => {
|
|
159
|
+
const cx = complexityMap.get(n.id) || null;
|
|
160
|
+
const fanIn = fanInMap.get(n.id) || 0;
|
|
161
|
+
const fanOut = fanOutMap.get(n.id) || 0;
|
|
162
|
+
const community = communityMap.get(n.id) ?? null;
|
|
163
|
+
const directory = path.dirname(n.file);
|
|
164
|
+
const risk = [];
|
|
165
|
+
if (n.role === 'dead') risk.push('dead-code');
|
|
166
|
+
if (fanIn >= (cfg.riskThresholds?.highBlastRadius ?? 10)) risk.push('high-blast-radius');
|
|
167
|
+
if (cx && cx.maintainabilityIndex < (cfg.riskThresholds?.lowMI ?? 40)) risk.push('low-mi');
|
|
168
|
+
|
|
169
|
+
const color =
|
|
170
|
+
cfg.colorBy === 'role' && n.role
|
|
171
|
+
? cfg.roleColors[n.role] || DEFAULT_ROLE_COLORS[n.role] || '#ccc'
|
|
172
|
+
: cfg.colorBy === 'community' && community !== null
|
|
173
|
+
? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length]
|
|
174
|
+
: cfg.nodeColors[n.kind] || DEFAULT_NODE_COLORS[n.kind] || '#ccc';
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
id: n.id,
|
|
178
|
+
label: n.name,
|
|
179
|
+
title: `${n.file}:${n.line} (${n.kind}${n.role ? `, ${n.role}` : ''})`,
|
|
180
|
+
color,
|
|
181
|
+
kind: n.kind,
|
|
182
|
+
role: n.role || '',
|
|
183
|
+
file: n.file,
|
|
184
|
+
line: n.line,
|
|
185
|
+
community,
|
|
186
|
+
cognitive: cx?.cognitive ?? null,
|
|
187
|
+
cyclomatic: cx?.cyclomatic ?? null,
|
|
188
|
+
maintainabilityIndex: cx?.maintainabilityIndex ?? null,
|
|
189
|
+
fanIn,
|
|
190
|
+
fanOut,
|
|
191
|
+
directory,
|
|
192
|
+
risk,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const visEdges = edges.map((e, i) => ({
|
|
197
|
+
id: `e${i}`,
|
|
198
|
+
from: e.source_id,
|
|
199
|
+
to: e.target_id,
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
// Seed strategy
|
|
203
|
+
let seedNodeIds;
|
|
204
|
+
if (cfg.seedStrategy === 'top-fanin') {
|
|
205
|
+
const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
|
|
206
|
+
seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
|
|
207
|
+
} else if (cfg.seedStrategy === 'entry') {
|
|
208
|
+
seedNodeIds = visNodes.filter((n) => n.role === 'entry').map((n) => n.id);
|
|
209
|
+
} else {
|
|
210
|
+
seedNodeIds = visNodes.map((n) => n.id);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { nodes: visNodes, edges: visEdges, seedNodeIds };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function prepareFileLevelData(db, noTests, minConf, cfg) {
|
|
217
|
+
let edges = db
|
|
218
|
+
.prepare(
|
|
219
|
+
`
|
|
220
|
+
SELECT DISTINCT n1.file AS source, n2.file AS target
|
|
221
|
+
FROM edges e
|
|
222
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
223
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
224
|
+
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
|
|
225
|
+
AND e.confidence >= ?
|
|
226
|
+
`,
|
|
227
|
+
)
|
|
228
|
+
.all(minConf);
|
|
229
|
+
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
230
|
+
|
|
231
|
+
const files = new Set();
|
|
232
|
+
for (const { source, target } of edges) {
|
|
233
|
+
files.add(source);
|
|
234
|
+
files.add(target);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const fileIds = new Map();
|
|
238
|
+
let idx = 0;
|
|
239
|
+
for (const f of files) fileIds.set(f, idx++);
|
|
240
|
+
|
|
241
|
+
// Fan-in/fan-out
|
|
242
|
+
const fanInCount = new Map();
|
|
243
|
+
const fanOutCount = new Map();
|
|
244
|
+
for (const { source, target } of edges) {
|
|
245
|
+
fanOutCount.set(source, (fanOutCount.get(source) || 0) + 1);
|
|
246
|
+
fanInCount.set(target, (fanInCount.get(target) || 0) + 1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Communities via graph subsystem
|
|
250
|
+
const communityMap = new Map();
|
|
251
|
+
if (files.size > 0) {
|
|
252
|
+
try {
|
|
253
|
+
const fileGraph = new CodeGraph();
|
|
254
|
+
for (const f of files) fileGraph.addNode(f);
|
|
255
|
+
for (const { source, target } of edges) {
|
|
256
|
+
if (source !== target && !fileGraph.hasEdge(source, target))
|
|
257
|
+
fileGraph.addEdge(source, target);
|
|
258
|
+
}
|
|
259
|
+
const { assignments } = louvainCommunities(fileGraph);
|
|
260
|
+
for (const [file, cid] of assignments) communityMap.set(file, cid);
|
|
261
|
+
} catch {
|
|
262
|
+
// ignore
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const visNodes = [...files].map((f) => {
|
|
267
|
+
const id = fileIds.get(f);
|
|
268
|
+
const community = communityMap.get(f) ?? null;
|
|
269
|
+
const fanIn = fanInCount.get(f) || 0;
|
|
270
|
+
const fanOut = fanOutCount.get(f) || 0;
|
|
271
|
+
const directory = path.dirname(f);
|
|
272
|
+
const color =
|
|
273
|
+
cfg.colorBy === 'community' && community !== null
|
|
274
|
+
? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length]
|
|
275
|
+
: cfg.nodeColors.file || DEFAULT_NODE_COLORS.file;
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
id,
|
|
279
|
+
label: path.basename(f),
|
|
280
|
+
title: f,
|
|
281
|
+
color,
|
|
282
|
+
kind: 'file',
|
|
283
|
+
role: '',
|
|
284
|
+
file: f,
|
|
285
|
+
line: 0,
|
|
286
|
+
community,
|
|
287
|
+
cognitive: null,
|
|
288
|
+
cyclomatic: null,
|
|
289
|
+
maintainabilityIndex: null,
|
|
290
|
+
fanIn,
|
|
291
|
+
fanOut,
|
|
292
|
+
directory,
|
|
293
|
+
risk: [],
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const visEdges = edges.map(({ source, target }, i) => ({
|
|
298
|
+
id: `e${i}`,
|
|
299
|
+
from: fileIds.get(source),
|
|
300
|
+
to: fileIds.get(target),
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
let seedNodeIds;
|
|
304
|
+
if (cfg.seedStrategy === 'top-fanin') {
|
|
305
|
+
const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
|
|
306
|
+
seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
|
|
307
|
+
} else if (cfg.seedStrategy === 'entry') {
|
|
308
|
+
seedNodeIds = visNodes.map((n) => n.id);
|
|
309
|
+
} else {
|
|
310
|
+
seedNodeIds = visNodes.map((n) => n.id);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { nodes: visNodes, edges: visEdges, seedNodeIds };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ─── HTML Generation (thin wrapper) ──────────────────────────────────
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Generate a self-contained interactive HTML file with vis-network.
|
|
320
|
+
*
|
|
321
|
+
* Loads graph data from the DB, then delegates to the presentation layer.
|
|
322
|
+
*/
|
|
323
|
+
export function generatePlotHTML(db, opts = {}) {
|
|
324
|
+
const cfg = opts.config || DEFAULT_CONFIG;
|
|
325
|
+
const data = prepareGraphData(db, opts);
|
|
326
|
+
return renderPlotHTML(data, cfg);
|
|
327
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { openReadonlyOrFail } from '../db/index.js';
|
|
2
|
+
import { findCycles } from '../domain/graph/cycles.js';
|
|
3
|
+
import { loadConfig } from '../infrastructure/config.js';
|
|
4
|
+
import { debug } from '../infrastructure/logger.js';
|
|
5
|
+
import { paginateResult } from '../shared/paginate.js';
|
|
1
6
|
import { evaluateBoundaries } from './boundaries.js';
|
|
2
|
-
import { loadConfig } from './config.js';
|
|
3
|
-
import { findCycles } from './cycles.js';
|
|
4
|
-
import { openReadonlyOrFail } from './db.js';
|
|
5
|
-
import { debug } from './logger.js';
|
|
6
|
-
import { paginateResult } from './paginate.js';
|
|
7
7
|
|
|
8
8
|
// ─── Rule Definitions ─────────────────────────────────────────────────
|
|
9
9
|
|
|
@@ -395,7 +395,7 @@ export function manifestoData(customDbPath, opts = {}) {
|
|
|
395
395
|
const db = openReadonlyOrFail(customDbPath);
|
|
396
396
|
|
|
397
397
|
try {
|
|
398
|
-
const config = loadConfig(process.cwd());
|
|
398
|
+
const config = opts.config || loadConfig(process.cwd());
|
|
399
399
|
const rules = resolveRules(config.manifesto?.rules);
|
|
400
400
|
|
|
401
401
|
const violations = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { findDbPath, openReadonlyOrFail } from '
|
|
4
|
-
import { isTestFile } from '
|
|
3
|
+
import { findDbPath, openReadonlyOrFail } from '../db/index.js';
|
|
4
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
5
5
|
|
|
6
6
|
// ─── CODEOWNERS Parsing ──────────────────────────────────────────────
|
|
7
7
|
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* sequence-diagram conventions.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import { openRepo } from '../db/index.js';
|
|
10
|
+
import { SqliteRepository } from '../db/repository/sqlite-repository.js';
|
|
11
|
+
import { findMatchingNodes } from '../domain/queries.js';
|
|
12
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
13
|
+
import { paginateResult } from '../shared/paginate.js';
|
|
13
14
|
import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
|
|
14
15
|
|
|
15
16
|
// ─── Alias generation ────────────────────────────────────────────────
|
|
@@ -85,19 +86,19 @@ function buildAliases(files) {
|
|
|
85
86
|
* @returns {{ entry, participants, messages, depth, totalMessages, truncated }}
|
|
86
87
|
*/
|
|
87
88
|
export function sequenceData(name, dbPath, opts = {}) {
|
|
88
|
-
const
|
|
89
|
+
const { repo, close } = openRepo(dbPath, opts);
|
|
89
90
|
try {
|
|
90
91
|
const maxDepth = opts.depth || 10;
|
|
91
92
|
const noTests = opts.noTests || false;
|
|
92
93
|
const withDataflow = opts.dataflow || false;
|
|
93
94
|
|
|
94
95
|
// Phase 1: Direct LIKE match
|
|
95
|
-
let matchNode = findMatchingNodes(
|
|
96
|
+
let matchNode = findMatchingNodes(repo, name, opts)[0] ?? null;
|
|
96
97
|
|
|
97
98
|
// Phase 2: Prefix-stripped matching
|
|
98
99
|
if (!matchNode) {
|
|
99
100
|
for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
|
|
100
|
-
matchNode = findMatchingNodes(
|
|
101
|
+
matchNode = findMatchingNodes(repo, `${prefix}${name}`, opts)[0] ?? null;
|
|
101
102
|
if (matchNode) break;
|
|
102
103
|
}
|
|
103
104
|
}
|
|
@@ -133,7 +134,7 @@ export function sequenceData(name, dbPath, opts = {}) {
|
|
|
133
134
|
const nextFrontier = [];
|
|
134
135
|
|
|
135
136
|
for (const fid of frontier) {
|
|
136
|
-
const callees = findCallees(
|
|
137
|
+
const callees = repo.findCallees(fid);
|
|
137
138
|
|
|
138
139
|
const caller = idToNode.get(fid);
|
|
139
140
|
|
|
@@ -163,18 +164,17 @@ export function sequenceData(name, dbPath, opts = {}) {
|
|
|
163
164
|
|
|
164
165
|
if (d === maxDepth && frontier.length > 0) {
|
|
165
166
|
// Only mark truncated if at least one frontier node has further callees
|
|
166
|
-
const hasMoreCalls = frontier.some((fid) => findCallees(
|
|
167
|
+
const hasMoreCalls = frontier.some((fid) => repo.findCallees(fid).length > 0);
|
|
167
168
|
if (hasMoreCalls) truncated = true;
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
// Dataflow annotations: add return arrows
|
|
172
173
|
if (withDataflow && messages.length > 0) {
|
|
173
|
-
const hasTable =
|
|
174
|
-
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='dataflow'")
|
|
175
|
-
.get();
|
|
174
|
+
const hasTable = repo.hasDataflowTable();
|
|
176
175
|
|
|
177
|
-
if (hasTable) {
|
|
176
|
+
if (hasTable && repo instanceof SqliteRepository) {
|
|
177
|
+
const db = repo.db;
|
|
178
178
|
// Build name|file lookup for O(1) target node access
|
|
179
179
|
const nodeByNameFile = new Map();
|
|
180
180
|
for (const n of idToNode.values()) {
|
|
@@ -281,45 +281,9 @@ export function sequenceData(name, dbPath, opts = {}) {
|
|
|
281
281
|
}
|
|
282
282
|
return result;
|
|
283
283
|
} finally {
|
|
284
|
-
|
|
284
|
+
close();
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Escape special Mermaid characters in labels.
|
|
292
|
-
*/
|
|
293
|
-
function escapeMermaid(str) {
|
|
294
|
-
return str
|
|
295
|
-
.replace(/</g, '<')
|
|
296
|
-
.replace(/>/g, '>')
|
|
297
|
-
.replace(/:/g, '#colon;')
|
|
298
|
-
.replace(/"/g, '#quot;');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Convert sequenceData result to Mermaid sequenceDiagram syntax.
|
|
303
|
-
* @param {{ participants, messages, truncated }} seqResult
|
|
304
|
-
* @returns {string}
|
|
305
|
-
*/
|
|
306
|
-
export function sequenceToMermaid(seqResult) {
|
|
307
|
-
const lines = ['sequenceDiagram'];
|
|
308
|
-
|
|
309
|
-
for (const p of seqResult.participants) {
|
|
310
|
-
lines.push(` participant ${p.id} as ${escapeMermaid(p.label)}`);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
for (const msg of seqResult.messages) {
|
|
314
|
-
const arrow = msg.type === 'return' ? '-->>' : '->>';
|
|
315
|
-
lines.push(` ${msg.from}${arrow}${msg.to}: ${escapeMermaid(msg.label)}`);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (seqResult.truncated && seqResult.participants.length > 0) {
|
|
319
|
-
lines.push(
|
|
320
|
-
` note right of ${seqResult.participants[0].id}: Truncated at depth ${seqResult.depth}`,
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return lines.join('\n');
|
|
325
|
-
}
|
|
288
|
+
// Re-export Mermaid renderer from presentation layer
|
|
289
|
+
export { sequenceToMermaid } from '../presentation/sequence-renderer.js';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import Database from 'better-sqlite3';
|
|
4
|
-
import { findDbPath } from '
|
|
5
|
-
import { debug } from '
|
|
4
|
+
import { findDbPath } from '../db/index.js';
|
|
5
|
+
import { debug } from '../infrastructure/logger.js';
|
|
6
|
+
import { ConfigError, DbError } from '../shared/errors.js';
|
|
6
7
|
|
|
7
8
|
const NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
8
9
|
|
|
@@ -12,7 +13,7 @@ const NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
|
12
13
|
*/
|
|
13
14
|
export function validateSnapshotName(name) {
|
|
14
15
|
if (!name || !NAME_RE.test(name)) {
|
|
15
|
-
throw new
|
|
16
|
+
throw new ConfigError(
|
|
16
17
|
`Invalid snapshot name "${name}". Use only letters, digits, hyphens, and underscores.`,
|
|
17
18
|
);
|
|
18
19
|
}
|
|
@@ -39,7 +40,7 @@ export function snapshotSave(name, options = {}) {
|
|
|
39
40
|
validateSnapshotName(name);
|
|
40
41
|
const dbPath = options.dbPath || findDbPath();
|
|
41
42
|
if (!fs.existsSync(dbPath)) {
|
|
42
|
-
throw new
|
|
43
|
+
throw new DbError(`Database not found: ${dbPath}`, { file: dbPath });
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
const dir = snapshotsDir(dbPath);
|
|
@@ -47,7 +48,7 @@ export function snapshotSave(name, options = {}) {
|
|
|
47
48
|
|
|
48
49
|
if (fs.existsSync(dest)) {
|
|
49
50
|
if (!options.force) {
|
|
50
|
-
throw new
|
|
51
|
+
throw new ConfigError(`Snapshot "${name}" already exists. Use --force to overwrite.`);
|
|
51
52
|
}
|
|
52
53
|
fs.unlinkSync(dest);
|
|
53
54
|
debug(`Deleted existing snapshot: ${dest}`);
|
|
@@ -82,7 +83,7 @@ export function snapshotRestore(name, options = {}) {
|
|
|
82
83
|
const src = path.join(dir, `${name}.db`);
|
|
83
84
|
|
|
84
85
|
if (!fs.existsSync(src)) {
|
|
85
|
-
throw new
|
|
86
|
+
throw new DbError(`Snapshot "${name}" not found at ${src}`, { file: src });
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
// Remove WAL/SHM sidecar files for a clean restore
|
|
@@ -141,7 +142,7 @@ export function snapshotDelete(name, options = {}) {
|
|
|
141
142
|
const target = path.join(dir, `${name}.db`);
|
|
142
143
|
|
|
143
144
|
if (!fs.existsSync(target)) {
|
|
144
|
-
throw new
|
|
145
|
+
throw new DbError(`Snapshot "${name}" not found at ${target}`, { file: target });
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
fs.unlinkSync(target);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { isTestFile } from '
|
|
5
|
-
import {
|
|
6
|
-
import { paginateResult } from '
|
|
2
|
+
import { getNodeId, openReadonlyOrFail, testFilterSQL } from '../db/index.js';
|
|
3
|
+
import { debug } from '../infrastructure/logger.js';
|
|
4
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
5
|
+
import { normalizePath } from '../shared/constants.js';
|
|
6
|
+
import { paginateResult } from '../shared/paginate.js';
|
|
7
7
|
|
|
8
8
|
// ─── Build-time: insert directory nodes, contains edges, and metrics ────
|
|
9
9
|
|
|
@@ -312,13 +312,10 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
312
312
|
|
|
313
313
|
// ─── Node role classification ─────────────────────────────────────────
|
|
314
314
|
|
|
315
|
-
export
|
|
315
|
+
// Re-export from classifier for backward compatibility
|
|
316
|
+
export { FRAMEWORK_ENTRY_PREFIXES } from '../graph/classifiers/roles.js';
|
|
316
317
|
|
|
317
|
-
|
|
318
|
-
if (sorted.length === 0) return 0;
|
|
319
|
-
const mid = Math.floor(sorted.length / 2);
|
|
320
|
-
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
321
|
-
}
|
|
318
|
+
import { classifyRoles } from '../graph/classifiers/roles.js';
|
|
322
319
|
|
|
323
320
|
export function classifyNodeRoles(db) {
|
|
324
321
|
const rows = db
|
|
@@ -354,44 +351,22 @@ export function classifyNodeRoles(db) {
|
|
|
354
351
|
.map((r) => r.target_id),
|
|
355
352
|
);
|
|
356
353
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
.
|
|
364
|
-
|
|
354
|
+
// Delegate classification to the pure-logic classifier
|
|
355
|
+
const classifierInput = rows.map((r) => ({
|
|
356
|
+
id: String(r.id),
|
|
357
|
+
name: r.name,
|
|
358
|
+
fanIn: r.fan_in,
|
|
359
|
+
fanOut: r.fan_out,
|
|
360
|
+
isExported: exportedIds.has(r.id),
|
|
361
|
+
}));
|
|
365
362
|
|
|
366
|
-
const
|
|
367
|
-
const medFanOut = median(nonZeroFanOut);
|
|
363
|
+
const roleMap = classifyRoles(classifierInput);
|
|
368
364
|
|
|
369
|
-
|
|
365
|
+
// Build summary and updates
|
|
370
366
|
const summary = { entry: 0, core: 0, utility: 0, adapter: 0, dead: 0, leaf: 0 };
|
|
371
|
-
|
|
367
|
+
const updates = [];
|
|
372
368
|
for (const row of rows) {
|
|
373
|
-
const
|
|
374
|
-
const highOut = row.fan_out >= medFanOut && row.fan_out > 0;
|
|
375
|
-
const isExported = exportedIds.has(row.id);
|
|
376
|
-
|
|
377
|
-
let role;
|
|
378
|
-
const isFrameworkEntry = FRAMEWORK_ENTRY_PREFIXES.some((p) => row.name.startsWith(p));
|
|
379
|
-
if (isFrameworkEntry) {
|
|
380
|
-
role = 'entry';
|
|
381
|
-
} else if (row.fan_in === 0 && !isExported) {
|
|
382
|
-
role = 'dead';
|
|
383
|
-
} else if (row.fan_in === 0 && isExported) {
|
|
384
|
-
role = 'entry';
|
|
385
|
-
} else if (highIn && !highOut) {
|
|
386
|
-
role = 'core';
|
|
387
|
-
} else if (highIn && highOut) {
|
|
388
|
-
role = 'utility';
|
|
389
|
-
} else if (!highIn && highOut) {
|
|
390
|
-
role = 'adapter';
|
|
391
|
-
} else {
|
|
392
|
-
role = 'leaf';
|
|
393
|
-
}
|
|
394
|
-
|
|
369
|
+
const role = roleMap.get(String(row.id)) || 'leaf';
|
|
395
370
|
updates.push({ id: row.id, role });
|
|
396
371
|
summary[role]++;
|
|
397
372
|
}
|