@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,49 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive HTML viewer — presentation layer.
|
|
3
|
+
*
|
|
4
|
+
* Exports two concerns:
|
|
5
|
+
* - renderPlotHTML(): pure data → HTML transform (no I/O) that receives
|
|
6
|
+
* prepared graph data and config, returns a self-contained HTML string
|
|
7
|
+
* with vis-network. All graph data must be pre-loaded via prepareGraphData().
|
|
8
|
+
* - loadPlotConfig(): reads .plotDotCfg / .plotDotCfg.json files from disk
|
|
9
|
+
* and merges them with defaults. This performs filesystem I/O.
|
|
10
|
+
*
|
|
11
|
+
* Color constants are defined in ./colors.js and re-exported here for
|
|
12
|
+
* backward compatibility.
|
|
13
|
+
*/
|
|
14
|
+
|
|
1
15
|
import fs from 'node:fs';
|
|
2
16
|
import path from 'node:path';
|
|
3
|
-
import
|
|
4
|
-
import louvain from 'graphology-communities-louvain';
|
|
5
|
-
import { isTestFile } from './infrastructure/test-filter.js';
|
|
6
|
-
|
|
7
|
-
const DEFAULT_MIN_CONFIDENCE = 0.5;
|
|
8
|
-
|
|
9
|
-
const DEFAULT_NODE_COLORS = {
|
|
10
|
-
function: '#4CAF50',
|
|
11
|
-
method: '#66BB6A',
|
|
12
|
-
class: '#2196F3',
|
|
13
|
-
interface: '#42A5F5',
|
|
14
|
-
type: '#7E57C2',
|
|
15
|
-
struct: '#FF7043',
|
|
16
|
-
enum: '#FFA726',
|
|
17
|
-
trait: '#26A69A',
|
|
18
|
-
record: '#EC407A',
|
|
19
|
-
module: '#78909C',
|
|
20
|
-
file: '#90A4AE',
|
|
21
|
-
};
|
|
17
|
+
import { COMMUNITY_COLORS, DEFAULT_NODE_COLORS, DEFAULT_ROLE_COLORS } from './colors.js';
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
core: '#e3f2fd',
|
|
26
|
-
utility: '#f5f5f5',
|
|
27
|
-
dead: '#ffebee',
|
|
28
|
-
leaf: '#fffde7',
|
|
29
|
-
};
|
|
19
|
+
// Re-export color constants so existing consumers are unaffected
|
|
20
|
+
export { COMMUNITY_COLORS, DEFAULT_NODE_COLORS, DEFAULT_ROLE_COLORS };
|
|
30
21
|
|
|
31
|
-
const
|
|
32
|
-
'#4CAF50',
|
|
33
|
-
'#2196F3',
|
|
34
|
-
'#FF9800',
|
|
35
|
-
'#9C27B0',
|
|
36
|
-
'#F44336',
|
|
37
|
-
'#00BCD4',
|
|
38
|
-
'#CDDC39',
|
|
39
|
-
'#E91E63',
|
|
40
|
-
'#3F51B5',
|
|
41
|
-
'#FF5722',
|
|
42
|
-
'#009688',
|
|
43
|
-
'#795548',
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
const DEFAULT_CONFIG = {
|
|
22
|
+
export const DEFAULT_CONFIG = {
|
|
47
23
|
layout: { algorithm: 'hierarchical', direction: 'LR' },
|
|
48
24
|
physics: { enabled: true, nodeDistance: 150 },
|
|
49
25
|
nodeColors: DEFAULT_NODE_COLORS,
|
|
@@ -60,6 +36,8 @@ const DEFAULT_CONFIG = {
|
|
|
60
36
|
riskThresholds: { highBlastRadius: 10, lowMI: 40 },
|
|
61
37
|
};
|
|
62
38
|
|
|
39
|
+
// ─── Config Loading ──────────────────────────────────────────────────
|
|
40
|
+
|
|
63
41
|
/**
|
|
64
42
|
* Load .plotDotCfg or .plotDotCfg.json from given directory.
|
|
65
43
|
* Returns merged config with defaults.
|
|
@@ -105,310 +83,66 @@ export function loadPlotConfig(dir) {
|
|
|
105
83
|
return { ...DEFAULT_CONFIG };
|
|
106
84
|
}
|
|
107
85
|
|
|
108
|
-
// ───
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Prepare enriched graph data for the HTML viewer.
|
|
112
|
-
*/
|
|
113
|
-
export function prepareGraphData(db, opts = {}) {
|
|
114
|
-
const fileLevel = opts.fileLevel !== false;
|
|
115
|
-
const noTests = opts.noTests || false;
|
|
116
|
-
const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
117
|
-
const cfg = opts.config || DEFAULT_CONFIG;
|
|
118
|
-
|
|
119
|
-
return fileLevel
|
|
120
|
-
? prepareFileLevelData(db, noTests, minConf, cfg)
|
|
121
|
-
: prepareFunctionLevelData(db, noTests, minConf, cfg);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function prepareFunctionLevelData(db, noTests, minConf, cfg) {
|
|
125
|
-
let edges = db
|
|
126
|
-
.prepare(
|
|
127
|
-
`
|
|
128
|
-
SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
|
|
129
|
-
n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
|
|
130
|
-
n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
|
|
131
|
-
n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
|
|
132
|
-
e.kind AS edge_kind
|
|
133
|
-
FROM edges e
|
|
134
|
-
JOIN nodes n1 ON e.source_id = n1.id
|
|
135
|
-
JOIN nodes n2 ON e.target_id = n2.id
|
|
136
|
-
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
137
|
-
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
138
|
-
AND e.kind = 'calls'
|
|
139
|
-
AND e.confidence >= ?
|
|
140
|
-
`,
|
|
141
|
-
)
|
|
142
|
-
.all(minConf);
|
|
143
|
-
if (noTests)
|
|
144
|
-
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
145
|
-
|
|
146
|
-
if (cfg.filter.kinds) {
|
|
147
|
-
const kinds = new Set(cfg.filter.kinds);
|
|
148
|
-
edges = edges.filter((e) => kinds.has(e.source_kind) && kinds.has(e.target_kind));
|
|
149
|
-
}
|
|
150
|
-
if (cfg.filter.files) {
|
|
151
|
-
const patterns = cfg.filter.files;
|
|
152
|
-
edges = edges.filter(
|
|
153
|
-
(e) =>
|
|
154
|
-
patterns.some((p) => e.source_file.includes(p)) &&
|
|
155
|
-
patterns.some((p) => e.target_file.includes(p)),
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const nodeMap = new Map();
|
|
160
|
-
for (const e of edges) {
|
|
161
|
-
if (!nodeMap.has(e.source_id)) {
|
|
162
|
-
nodeMap.set(e.source_id, {
|
|
163
|
-
id: e.source_id,
|
|
164
|
-
name: e.source_name,
|
|
165
|
-
kind: e.source_kind,
|
|
166
|
-
file: e.source_file,
|
|
167
|
-
line: e.source_line,
|
|
168
|
-
role: e.source_role,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
if (!nodeMap.has(e.target_id)) {
|
|
172
|
-
nodeMap.set(e.target_id, {
|
|
173
|
-
id: e.target_id,
|
|
174
|
-
name: e.target_name,
|
|
175
|
-
kind: e.target_kind,
|
|
176
|
-
file: e.target_file,
|
|
177
|
-
line: e.target_line,
|
|
178
|
-
role: e.target_role,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (cfg.filter.roles) {
|
|
184
|
-
const roles = new Set(cfg.filter.roles);
|
|
185
|
-
for (const [id, n] of nodeMap) {
|
|
186
|
-
if (!roles.has(n.role)) nodeMap.delete(id);
|
|
187
|
-
}
|
|
188
|
-
const nodeIds = new Set(nodeMap.keys());
|
|
189
|
-
edges = edges.filter((e) => nodeIds.has(e.source_id) && nodeIds.has(e.target_id));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Complexity data
|
|
193
|
-
const complexityMap = new Map();
|
|
194
|
-
try {
|
|
195
|
-
const rows = db
|
|
196
|
-
.prepare(
|
|
197
|
-
'SELECT node_id, cognitive, cyclomatic, max_nesting, maintainability_index FROM function_complexity',
|
|
198
|
-
)
|
|
199
|
-
.all();
|
|
200
|
-
for (const r of rows) {
|
|
201
|
-
complexityMap.set(r.node_id, {
|
|
202
|
-
cognitive: r.cognitive,
|
|
203
|
-
cyclomatic: r.cyclomatic,
|
|
204
|
-
maintainabilityIndex: r.maintainability_index,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
} catch {
|
|
208
|
-
// table may not exist in old DBs
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Fan-in / fan-out
|
|
212
|
-
const fanInMap = new Map();
|
|
213
|
-
const fanOutMap = new Map();
|
|
214
|
-
const fanInRows = db
|
|
215
|
-
.prepare(
|
|
216
|
-
"SELECT target_id AS node_id, COUNT(*) AS fan_in FROM edges WHERE kind = 'calls' GROUP BY target_id",
|
|
217
|
-
)
|
|
218
|
-
.all();
|
|
219
|
-
for (const r of fanInRows) fanInMap.set(r.node_id, r.fan_in);
|
|
220
|
-
|
|
221
|
-
const fanOutRows = db
|
|
222
|
-
.prepare(
|
|
223
|
-
"SELECT source_id AS node_id, COUNT(*) AS fan_out FROM edges WHERE kind = 'calls' GROUP BY source_id",
|
|
224
|
-
)
|
|
225
|
-
.all();
|
|
226
|
-
for (const r of fanOutRows) fanOutMap.set(r.node_id, r.fan_out);
|
|
227
|
-
|
|
228
|
-
// Communities (Louvain)
|
|
229
|
-
const communityMap = new Map();
|
|
230
|
-
if (nodeMap.size > 0) {
|
|
231
|
-
try {
|
|
232
|
-
const graph = new Graph({ type: 'undirected' });
|
|
233
|
-
for (const [id] of nodeMap) graph.addNode(String(id));
|
|
234
|
-
for (const e of edges) {
|
|
235
|
-
const src = String(e.source_id);
|
|
236
|
-
const tgt = String(e.target_id);
|
|
237
|
-
if (src !== tgt && !graph.hasEdge(src, tgt)) graph.addEdge(src, tgt);
|
|
238
|
-
}
|
|
239
|
-
const communities = louvain(graph);
|
|
240
|
-
for (const [nid, cid] of Object.entries(communities)) communityMap.set(Number(nid), cid);
|
|
241
|
-
} catch {
|
|
242
|
-
// louvain can fail on disconnected graphs
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Build enriched nodes
|
|
247
|
-
const visNodes = [...nodeMap.values()].map((n) => {
|
|
248
|
-
const cx = complexityMap.get(n.id) || null;
|
|
249
|
-
const fanIn = fanInMap.get(n.id) || 0;
|
|
250
|
-
const fanOut = fanOutMap.get(n.id) || 0;
|
|
251
|
-
const community = communityMap.get(n.id) ?? null;
|
|
252
|
-
const directory = path.dirname(n.file);
|
|
253
|
-
const risk = [];
|
|
254
|
-
if (n.role === 'dead') risk.push('dead-code');
|
|
255
|
-
if (fanIn >= (cfg.riskThresholds?.highBlastRadius ?? 10)) risk.push('high-blast-radius');
|
|
256
|
-
if (cx && cx.maintainabilityIndex < (cfg.riskThresholds?.lowMI ?? 40)) risk.push('low-mi');
|
|
257
|
-
|
|
258
|
-
const color =
|
|
259
|
-
cfg.colorBy === 'role' && n.role
|
|
260
|
-
? cfg.roleColors[n.role] || DEFAULT_ROLE_COLORS[n.role] || '#ccc'
|
|
261
|
-
: cfg.colorBy === 'community' && community !== null
|
|
262
|
-
? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length]
|
|
263
|
-
: cfg.nodeColors[n.kind] || DEFAULT_NODE_COLORS[n.kind] || '#ccc';
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
id: n.id,
|
|
267
|
-
label: n.name,
|
|
268
|
-
title: `${n.file}:${n.line} (${n.kind}${n.role ? `, ${n.role}` : ''})`,
|
|
269
|
-
color,
|
|
270
|
-
kind: n.kind,
|
|
271
|
-
role: n.role || '',
|
|
272
|
-
file: n.file,
|
|
273
|
-
line: n.line,
|
|
274
|
-
community,
|
|
275
|
-
cognitive: cx?.cognitive ?? null,
|
|
276
|
-
cyclomatic: cx?.cyclomatic ?? null,
|
|
277
|
-
maintainabilityIndex: cx?.maintainabilityIndex ?? null,
|
|
278
|
-
fanIn,
|
|
279
|
-
fanOut,
|
|
280
|
-
directory,
|
|
281
|
-
risk,
|
|
282
|
-
};
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
const visEdges = edges.map((e, i) => ({
|
|
286
|
-
id: `e${i}`,
|
|
287
|
-
from: e.source_id,
|
|
288
|
-
to: e.target_id,
|
|
289
|
-
}));
|
|
290
|
-
|
|
291
|
-
// Seed strategy
|
|
292
|
-
let seedNodeIds;
|
|
293
|
-
if (cfg.seedStrategy === 'top-fanin') {
|
|
294
|
-
const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
|
|
295
|
-
seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
|
|
296
|
-
} else if (cfg.seedStrategy === 'entry') {
|
|
297
|
-
seedNodeIds = visNodes.filter((n) => n.role === 'entry').map((n) => n.id);
|
|
298
|
-
} else {
|
|
299
|
-
seedNodeIds = visNodes.map((n) => n.id);
|
|
300
|
-
}
|
|
86
|
+
// ─── Internal Helpers ────────────────────────────────────────────────
|
|
301
87
|
|
|
302
|
-
|
|
88
|
+
export function escapeHtml(s) {
|
|
89
|
+
return String(s)
|
|
90
|
+
.replace(/&/g, '&')
|
|
91
|
+
.replace(/</g, '<')
|
|
92
|
+
.replace(/>/g, '>')
|
|
93
|
+
.replace(/"/g, '"');
|
|
303
94
|
}
|
|
304
95
|
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
for (const f of files) fileIds.set(f, idx++);
|
|
329
|
-
|
|
330
|
-
// Fan-in/fan-out
|
|
331
|
-
const fanInCount = new Map();
|
|
332
|
-
const fanOutCount = new Map();
|
|
333
|
-
for (const { source, target } of edges) {
|
|
334
|
-
fanOutCount.set(source, (fanOutCount.get(source) || 0) + 1);
|
|
335
|
-
fanInCount.set(target, (fanInCount.get(target) || 0) + 1);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Communities
|
|
339
|
-
const communityMap = new Map();
|
|
340
|
-
if (files.size > 0) {
|
|
341
|
-
try {
|
|
342
|
-
const graph = new Graph({ type: 'undirected' });
|
|
343
|
-
for (const f of files) graph.addNode(f);
|
|
344
|
-
for (const { source, target } of edges) {
|
|
345
|
-
if (source !== target && !graph.hasEdge(source, target)) graph.addEdge(source, target);
|
|
346
|
-
}
|
|
347
|
-
const communities = louvain(graph);
|
|
348
|
-
for (const [file, cid] of Object.entries(communities)) communityMap.set(file, cid);
|
|
349
|
-
} catch {
|
|
350
|
-
// ignore
|
|
351
|
-
}
|
|
352
|
-
}
|
|
96
|
+
export function buildLayoutOptions(cfg) {
|
|
97
|
+
const opts = {
|
|
98
|
+
nodes: {
|
|
99
|
+
shape: 'box',
|
|
100
|
+
font: { face: 'monospace', size: 12 },
|
|
101
|
+
},
|
|
102
|
+
edges: {
|
|
103
|
+
arrows: 'to',
|
|
104
|
+
color: cfg.edgeStyle.color || '#666',
|
|
105
|
+
smooth: cfg.edgeStyle.smooth !== false,
|
|
106
|
+
},
|
|
107
|
+
physics: {
|
|
108
|
+
enabled: cfg.physics.enabled !== false,
|
|
109
|
+
barnesHut: {
|
|
110
|
+
gravitationalConstant: -3000,
|
|
111
|
+
springLength: cfg.physics.nodeDistance || 150,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
interaction: {
|
|
115
|
+
tooltipDelay: 200,
|
|
116
|
+
hover: true,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
353
119
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
? COMMUNITY_COLORS[community % COMMUNITY_COLORS.length]
|
|
363
|
-
: cfg.nodeColors.file || DEFAULT_NODE_COLORS.file;
|
|
364
|
-
|
|
365
|
-
return {
|
|
366
|
-
id,
|
|
367
|
-
label: path.basename(f),
|
|
368
|
-
title: f,
|
|
369
|
-
color,
|
|
370
|
-
kind: 'file',
|
|
371
|
-
role: '',
|
|
372
|
-
file: f,
|
|
373
|
-
line: 0,
|
|
374
|
-
community,
|
|
375
|
-
cognitive: null,
|
|
376
|
-
cyclomatic: null,
|
|
377
|
-
maintainabilityIndex: null,
|
|
378
|
-
fanIn,
|
|
379
|
-
fanOut,
|
|
380
|
-
directory,
|
|
381
|
-
risk: [],
|
|
120
|
+
if (cfg.layout.algorithm === 'hierarchical') {
|
|
121
|
+
opts.layout = {
|
|
122
|
+
hierarchical: {
|
|
123
|
+
enabled: true,
|
|
124
|
+
direction: cfg.layout.direction || 'LR',
|
|
125
|
+
sortMethod: 'directed',
|
|
126
|
+
nodeSpacing: cfg.physics.nodeDistance || 150,
|
|
127
|
+
},
|
|
382
128
|
};
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
const visEdges = edges.map(({ source, target }, i) => ({
|
|
386
|
-
id: `e${i}`,
|
|
387
|
-
from: fileIds.get(source),
|
|
388
|
-
to: fileIds.get(target),
|
|
389
|
-
}));
|
|
390
|
-
|
|
391
|
-
let seedNodeIds;
|
|
392
|
-
if (cfg.seedStrategy === 'top-fanin') {
|
|
393
|
-
const sorted = [...visNodes].sort((a, b) => b.fanIn - a.fanIn);
|
|
394
|
-
seedNodeIds = sorted.slice(0, cfg.seedCount || 30).map((n) => n.id);
|
|
395
|
-
} else if (cfg.seedStrategy === 'entry') {
|
|
396
|
-
seedNodeIds = visNodes.map((n) => n.id);
|
|
397
|
-
} else {
|
|
398
|
-
seedNodeIds = visNodes.map((n) => n.id);
|
|
399
129
|
}
|
|
400
130
|
|
|
401
|
-
return
|
|
131
|
+
return opts;
|
|
402
132
|
}
|
|
403
133
|
|
|
404
|
-
// ─── HTML
|
|
134
|
+
// ─── HTML Renderer ───────────────────────────────────────────────────
|
|
405
135
|
|
|
406
136
|
/**
|
|
407
|
-
*
|
|
137
|
+
* Render a self-contained interactive HTML file with vis-network.
|
|
138
|
+
*
|
|
139
|
+
* Pure transform: prepared graph data + config → HTML string.
|
|
140
|
+
*
|
|
141
|
+
* @param {{ nodes: Array, edges: Array, seedNodeIds: Array }} data - From prepareGraphData()
|
|
142
|
+
* @param {object} cfg - Viewer config (from loadPlotConfig or DEFAULT_CONFIG)
|
|
143
|
+
* @returns {string} Complete HTML document
|
|
408
144
|
*/
|
|
409
|
-
export function
|
|
410
|
-
const cfg = opts.config || DEFAULT_CONFIG;
|
|
411
|
-
const data = prepareGraphData(db, opts);
|
|
145
|
+
export function renderPlotHTML(data, cfg) {
|
|
412
146
|
const layoutOpts = buildLayoutOptions(cfg);
|
|
413
147
|
const title = cfg.title || 'Codegraph';
|
|
414
148
|
|
|
@@ -898,51 +632,3 @@ ${(cfg.clusterBy || 'none') !== 'none' ? `applyClusterBy(${JSON.stringify(cfg.cl
|
|
|
898
632
|
</body>
|
|
899
633
|
</html>`;
|
|
900
634
|
}
|
|
901
|
-
|
|
902
|
-
// ─── Internal Helpers ─────────────────────────────────────────────────
|
|
903
|
-
|
|
904
|
-
function escapeHtml(s) {
|
|
905
|
-
return String(s)
|
|
906
|
-
.replace(/&/g, '&')
|
|
907
|
-
.replace(/</g, '<')
|
|
908
|
-
.replace(/>/g, '>')
|
|
909
|
-
.replace(/"/g, '"');
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function buildLayoutOptions(cfg) {
|
|
913
|
-
const opts = {
|
|
914
|
-
nodes: {
|
|
915
|
-
shape: 'box',
|
|
916
|
-
font: { face: 'monospace', size: 12 },
|
|
917
|
-
},
|
|
918
|
-
edges: {
|
|
919
|
-
arrows: 'to',
|
|
920
|
-
color: cfg.edgeStyle.color || '#666',
|
|
921
|
-
smooth: cfg.edgeStyle.smooth !== false,
|
|
922
|
-
},
|
|
923
|
-
physics: {
|
|
924
|
-
enabled: cfg.physics.enabled !== false,
|
|
925
|
-
barnesHut: {
|
|
926
|
-
gravitationalConstant: -3000,
|
|
927
|
-
springLength: cfg.physics.nodeDistance || 150,
|
|
928
|
-
},
|
|
929
|
-
},
|
|
930
|
-
interaction: {
|
|
931
|
-
tooltipDelay: 200,
|
|
932
|
-
hover: true,
|
|
933
|
-
},
|
|
934
|
-
};
|
|
935
|
-
|
|
936
|
-
if (cfg.layout.algorithm === 'hierarchical') {
|
|
937
|
-
opts.layout = {
|
|
938
|
-
hierarchical: {
|
|
939
|
-
enabled: true,
|
|
940
|
-
direction: cfg.layout.direction || 'LR',
|
|
941
|
-
sortMethod: 'directed',
|
|
942
|
-
nodeSpacing: cfg.physics.nodeDistance || 150,
|
|
943
|
-
},
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
return opts;
|
|
948
|
-
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain error hierarchy for codegraph.
|
|
3
|
+
*
|
|
4
|
+
* Library code throws these instead of calling process.exit() or throwing
|
|
5
|
+
* bare Error instances. The CLI top-level catch formats them for humans;
|
|
6
|
+
* MCP returns structured { isError, code } responses.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export class CodegraphError extends Error {
|
|
10
|
+
/** @type {string} */
|
|
11
|
+
code;
|
|
12
|
+
|
|
13
|
+
/** @type {string|undefined} */
|
|
14
|
+
file;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} message
|
|
18
|
+
* @param {object} [opts]
|
|
19
|
+
* @param {string} [opts.code]
|
|
20
|
+
* @param {string} [opts.file] - Related file path, if applicable
|
|
21
|
+
* @param {Error} [opts.cause] - Original error that triggered this one
|
|
22
|
+
*/
|
|
23
|
+
constructor(message, { code = 'CODEGRAPH_ERROR', file, cause } = {}) {
|
|
24
|
+
super(message, { cause });
|
|
25
|
+
this.name = 'CodegraphError';
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.file = file;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ParseError extends CodegraphError {
|
|
32
|
+
constructor(message, opts = {}) {
|
|
33
|
+
super(message, { code: 'PARSE_FAILED', ...opts });
|
|
34
|
+
this.name = 'ParseError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class DbError extends CodegraphError {
|
|
39
|
+
constructor(message, opts = {}) {
|
|
40
|
+
super(message, { code: 'DB_ERROR', ...opts });
|
|
41
|
+
this.name = 'DbError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class ConfigError extends CodegraphError {
|
|
46
|
+
constructor(message, opts = {}) {
|
|
47
|
+
super(message, { code: 'CONFIG_INVALID', ...opts });
|
|
48
|
+
this.name = 'ConfigError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ResolutionError extends CodegraphError {
|
|
53
|
+
constructor(message, opts = {}) {
|
|
54
|
+
super(message, { code: 'RESOLUTION_FAILED', ...opts });
|
|
55
|
+
this.name = 'ResolutionError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class EngineError extends CodegraphError {
|
|
60
|
+
constructor(message, opts = {}) {
|
|
61
|
+
super(message, { code: 'ENGINE_UNAVAILABLE', ...opts });
|
|
62
|
+
this.name = 'EngineError';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class AnalysisError extends CodegraphError {
|
|
67
|
+
constructor(message, opts = {}) {
|
|
68
|
+
super(message, { code: 'ANALYSIS_FAILED', ...opts });
|
|
69
|
+
this.name = 'AnalysisError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class BoundaryError extends CodegraphError {
|
|
74
|
+
constructor(message, opts = {}) {
|
|
75
|
+
super(message, { code: 'BOUNDARY_VIOLATION', ...opts });
|
|
76
|
+
this.name = 'BoundaryError';
|
|
77
|
+
}
|
|
78
|
+
}
|