@optave/codegraph 3.1.3 → 3.1.4
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 +17 -19
- package/package.json +10 -7
- package/src/analysis/context.js +408 -0
- package/src/analysis/dependencies.js +341 -0
- package/src/analysis/exports.js +130 -0
- package/src/analysis/impact.js +463 -0
- package/src/analysis/module-map.js +322 -0
- package/src/analysis/roles.js +45 -0
- package/src/analysis/symbol-lookup.js +232 -0
- package/src/ast-analysis/shared.js +5 -4
- package/src/batch.js +2 -1
- package/src/builder/context.js +85 -0
- package/src/builder/helpers.js +218 -0
- package/src/builder/incremental.js +178 -0
- package/src/builder/pipeline.js +130 -0
- package/src/builder/stages/build-edges.js +297 -0
- package/src/builder/stages/build-structure.js +113 -0
- package/src/builder/stages/collect-files.js +44 -0
- package/src/builder/stages/detect-changes.js +413 -0
- package/src/builder/stages/finalize.js +139 -0
- package/src/builder/stages/insert-nodes.js +195 -0
- package/src/builder/stages/parse-files.js +28 -0
- package/src/builder/stages/resolve-imports.js +143 -0
- package/src/builder/stages/run-analyses.js +44 -0
- package/src/builder.js +10 -1485
- package/src/cfg.js +1 -2
- package/src/cli/commands/ast.js +26 -0
- package/src/cli/commands/audit.js +46 -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 +30 -0
- package/src/cli/commands/check.js +79 -0
- package/src/cli/commands/children.js +31 -0
- package/src/cli/commands/co-change.js +65 -0
- package/src/cli/commands/communities.js +23 -0
- package/src/cli/commands/complexity.js +45 -0
- package/src/cli/commands/context.js +34 -0
- package/src/cli/commands/cycles.js +28 -0
- package/src/cli/commands/dataflow.js +32 -0
- package/src/cli/commands/deps.js +16 -0
- package/src/cli/commands/diff-impact.js +30 -0
- package/src/cli/commands/embed.js +30 -0
- package/src/cli/commands/export.js +75 -0
- package/src/cli/commands/exports.js +18 -0
- package/src/cli/commands/flow.js +36 -0
- package/src/cli/commands/fn-impact.js +30 -0
- package/src/cli/commands/impact.js +16 -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 +80 -0
- package/src/cli/commands/query.js +49 -0
- package/src/cli/commands/registry.js +100 -0
- package/src/cli/commands/roles.js +34 -0
- package/src/cli/commands/search.js +42 -0
- package/src/cli/commands/sequence.js +32 -0
- package/src/cli/commands/snapshot.js +61 -0
- package/src/cli/commands/stats.js +15 -0
- package/src/cli/commands/structure.js +32 -0
- package/src/cli/commands/triage.js +78 -0
- package/src/cli/commands/watch.js +12 -0
- package/src/cli/commands/where.js +24 -0
- package/src/cli/index.js +118 -0
- package/src/cli/shared/options.js +39 -0
- package/src/cli/shared/output.js +1 -0
- package/src/cli.js +11 -1522
- package/src/commands/check.js +5 -5
- package/src/commands/manifesto.js +3 -3
- package/src/commands/structure.js +1 -1
- package/src/communities.js +15 -87
- package/src/cycles.js +30 -85
- package/src/dataflow.js +1 -2
- package/src/db/connection.js +4 -4
- package/src/db/migrations.js +41 -0
- package/src/db/query-builder.js +6 -5
- package/src/db/repository/base.js +201 -0
- package/src/db/repository/graph-read.js +5 -2
- package/src/db/repository/in-memory-repository.js +584 -0
- package/src/db/repository/index.js +5 -1
- package/src/db/repository/nodes.js +63 -4
- package/src/db/repository/sqlite-repository.js +219 -0
- package/src/db.js +5 -0
- package/src/embeddings/generator.js +163 -0
- package/src/embeddings/index.js +13 -0
- package/src/embeddings/models.js +218 -0
- package/src/embeddings/search/cli-formatter.js +151 -0
- package/src/embeddings/search/filters.js +46 -0
- package/src/embeddings/search/hybrid.js +121 -0
- package/src/embeddings/search/keyword.js +68 -0
- package/src/embeddings/search/prepare.js +66 -0
- package/src/embeddings/search/semantic.js +145 -0
- package/src/embeddings/stores/fts5.js +27 -0
- package/src/embeddings/stores/sqlite-blob.js +24 -0
- package/src/embeddings/strategies/source.js +14 -0
- package/src/embeddings/strategies/structured.js +43 -0
- package/src/embeddings/strategies/text-utils.js +43 -0
- package/src/errors.js +78 -0
- package/src/export.js +217 -520
- 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 +38 -1
- package/src/extractors/php.js +3 -1
- package/src/extractors/python.js +14 -3
- package/src/extractors/rust.js +3 -1
- 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 +91 -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.js +33 -210
- 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/tool-registry.js +801 -0
- 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 +10 -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/mcp.js +2 -1470
- package/src/native.js +3 -1
- package/src/presentation/colors.js +44 -0
- package/src/presentation/export.js +444 -0
- package/src/presentation/result-formatter.js +21 -0
- package/src/presentation/sequence-renderer.js +43 -0
- package/src/presentation/table.js +47 -0
- package/src/presentation/viewer.js +634 -0
- package/src/queries.js +35 -2276
- package/src/resolve.js +1 -1
- package/src/sequence.js +2 -38
- 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/snapshot.js +6 -5
- package/src/structure.js +15 -40
- package/src/triage.js +20 -72
- package/src/viewer.js +35 -656
- package/src/watcher.js +8 -148
- package/src/embedder.js +0 -1097
package/src/export.js
CHANGED
|
@@ -1,368 +1,254 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { isTestFile } from './infrastructure/test-filter.js';
|
|
3
3
|
import { paginateResult } from './paginate.js';
|
|
4
|
+
import {
|
|
5
|
+
renderFileLevelDOT,
|
|
6
|
+
renderFileLevelGraphML,
|
|
7
|
+
renderFileLevelMermaid,
|
|
8
|
+
renderFileLevelNeo4jCSV,
|
|
9
|
+
renderFunctionLevelDOT,
|
|
10
|
+
renderFunctionLevelGraphML,
|
|
11
|
+
renderFunctionLevelMermaid,
|
|
12
|
+
renderFunctionLevelNeo4jCSV,
|
|
13
|
+
} from './presentation/export.js';
|
|
4
14
|
|
|
5
15
|
const DEFAULT_MIN_CONFIDENCE = 0.5;
|
|
6
16
|
|
|
7
|
-
|
|
8
|
-
function escapeXml(s) {
|
|
9
|
-
return String(s)
|
|
10
|
-
.replace(/&/g, '&')
|
|
11
|
-
.replace(/</g, '<')
|
|
12
|
-
.replace(/>/g, '>')
|
|
13
|
-
.replace(/"/g, '"')
|
|
14
|
-
.replace(/'/g, ''');
|
|
15
|
-
}
|
|
17
|
+
// ─── Shared data loaders ─────────────────────────────────────────────
|
|
16
18
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Load file-level edges from DB with filtering.
|
|
21
|
+
* @param {object} db
|
|
22
|
+
* @param {object} opts
|
|
23
|
+
* @param {boolean} [opts.includeKind] - Include edge_kind in SELECT DISTINCT
|
|
24
|
+
* @param {boolean} [opts.includeConfidence] - Include confidence (adds a column to DISTINCT — use only when needed)
|
|
25
|
+
* @returns {{ edges: Array, totalEdges: number }}
|
|
26
|
+
*/
|
|
27
|
+
function loadFileLevelEdges(
|
|
28
|
+
db,
|
|
29
|
+
{ noTests, minConfidence, limit, includeKind = false, includeConfidence = false },
|
|
30
|
+
) {
|
|
31
|
+
const minConf = minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
32
|
+
const kindClause = includeKind ? ', e.kind AS edge_kind' : '';
|
|
33
|
+
const confidenceClause = includeConfidence ? ', e.confidence' : '';
|
|
34
|
+
let edges = db
|
|
35
|
+
.prepare(
|
|
36
|
+
`
|
|
37
|
+
SELECT DISTINCT n1.file AS source, n2.file AS target${kindClause}${confidenceClause}
|
|
38
|
+
FROM edges e
|
|
39
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
40
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
41
|
+
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
|
|
42
|
+
AND e.confidence >= ?
|
|
43
|
+
`,
|
|
44
|
+
)
|
|
45
|
+
.all(minConf);
|
|
46
|
+
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
47
|
+
const totalEdges = edges.length;
|
|
48
|
+
if (limit && edges.length > limit) edges = edges.slice(0, limit);
|
|
49
|
+
return { edges, totalEdges };
|
|
24
50
|
}
|
|
25
51
|
|
|
26
52
|
/**
|
|
27
|
-
*
|
|
53
|
+
* Load function-level edges from DB with filtering.
|
|
54
|
+
* Returns the maximal field set needed by any serializer.
|
|
55
|
+
* @returns {{ edges: Array, totalEdges: number }}
|
|
28
56
|
*/
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'',
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
if (fileLevel) {
|
|
43
|
-
let edges = db
|
|
44
|
-
.prepare(`
|
|
45
|
-
SELECT DISTINCT n1.file AS source, n2.file AS target
|
|
57
|
+
function loadFunctionLevelEdges(db, { noTests, minConfidence, limit }) {
|
|
58
|
+
const minConf = minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
59
|
+
let edges = db
|
|
60
|
+
.prepare(
|
|
61
|
+
`
|
|
62
|
+
SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
|
|
63
|
+
n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
|
|
64
|
+
n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
|
|
65
|
+
n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
|
|
66
|
+
e.kind AS edge_kind, e.confidence
|
|
46
67
|
FROM edges e
|
|
47
68
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
48
69
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
49
|
-
WHERE n1.
|
|
70
|
+
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
71
|
+
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
72
|
+
AND e.kind = 'calls'
|
|
50
73
|
AND e.confidence >= ?
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
`,
|
|
75
|
+
)
|
|
76
|
+
.all(minConf);
|
|
77
|
+
if (noTests)
|
|
78
|
+
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
79
|
+
const totalEdges = edges.length;
|
|
80
|
+
if (limit && edges.length > limit) edges = edges.slice(0, limit);
|
|
81
|
+
return { edges, totalEdges };
|
|
82
|
+
}
|
|
56
83
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Load directory groupings for file-level graphs.
|
|
86
|
+
* Uses DB directory nodes if available, falls back to path.dirname().
|
|
87
|
+
* @returns {Array<{ name: string, files: Array<{ path: string, basename: string }>, cohesion: number|null }>}
|
|
88
|
+
*/
|
|
89
|
+
function loadDirectoryGroups(db, allFiles) {
|
|
90
|
+
const hasDirectoryNodes =
|
|
91
|
+
db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'directory'").get().c > 0;
|
|
60
92
|
|
|
61
|
-
|
|
62
|
-
const allFiles = new Set();
|
|
63
|
-
for (const { source, target } of edges) {
|
|
64
|
-
allFiles.add(source);
|
|
65
|
-
allFiles.add(target);
|
|
66
|
-
}
|
|
93
|
+
const dirs = new Map();
|
|
67
94
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
95
|
+
if (hasDirectoryNodes) {
|
|
96
|
+
const dbDirs = db
|
|
97
|
+
.prepare(`
|
|
98
|
+
SELECT n.id, n.name, nm.cohesion
|
|
99
|
+
FROM nodes n
|
|
100
|
+
LEFT JOIN node_metrics nm ON n.id = nm.node_id
|
|
101
|
+
WHERE n.kind = 'directory'
|
|
102
|
+
`)
|
|
103
|
+
.all();
|
|
104
|
+
|
|
105
|
+
for (const d of dbDirs) {
|
|
106
|
+
const containedFiles = db
|
|
71
107
|
.prepare(`
|
|
72
|
-
SELECT n.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
WHERE n.kind = 'directory'
|
|
108
|
+
SELECT n.name FROM edges e
|
|
109
|
+
JOIN nodes n ON e.target_id = n.id
|
|
110
|
+
WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
|
|
76
111
|
`)
|
|
77
|
-
.all()
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const containedFiles = db
|
|
81
|
-
.prepare(`
|
|
82
|
-
SELECT n.name FROM edges e
|
|
83
|
-
JOIN nodes n ON e.target_id = n.id
|
|
84
|
-
WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
|
|
85
|
-
`)
|
|
86
|
-
.all(d.id)
|
|
87
|
-
.map((r) => r.name)
|
|
88
|
-
.filter((f) => allFiles.has(f));
|
|
89
|
-
|
|
90
|
-
if (containedFiles.length > 0) {
|
|
91
|
-
dirs.set(d.name, { files: containedFiles, cohesion: d.cohesion });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
// Fallback: reconstruct from path.dirname()
|
|
96
|
-
for (const file of allFiles) {
|
|
97
|
-
const dir = path.dirname(file) || '.';
|
|
98
|
-
if (!dirs.has(dir)) dirs.set(dir, { files: [], cohesion: null });
|
|
99
|
-
dirs.get(dir).files.push(file);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
112
|
+
.all(d.id)
|
|
113
|
+
.map((r) => r.name)
|
|
114
|
+
.filter((f) => allFiles.has(f));
|
|
102
115
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
lines.push(` subgraph cluster_${clusterIdx++} {`);
|
|
106
|
-
const cohLabel = info.cohesion !== null ? ` (cohesion: ${info.cohesion.toFixed(2)})` : '';
|
|
107
|
-
lines.push(` label="${dir}${cohLabel}";`);
|
|
108
|
-
lines.push(` style=dashed;`);
|
|
109
|
-
lines.push(` color="#999999";`);
|
|
110
|
-
for (const f of info.files) {
|
|
111
|
-
const label = path.basename(f);
|
|
112
|
-
lines.push(` "${f}" [label="${label}"];`);
|
|
116
|
+
if (containedFiles.length > 0) {
|
|
117
|
+
dirs.set(d.name, { files: containedFiles, cohesion: d.cohesion ?? null });
|
|
113
118
|
}
|
|
114
|
-
lines.push(` }`);
|
|
115
|
-
lines.push('');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
for (const { source, target } of edges) {
|
|
119
|
-
lines.push(` "${source}" -> "${target}";`);
|
|
120
|
-
}
|
|
121
|
-
if (edgeLimit && totalFileEdges > edgeLimit) {
|
|
122
|
-
lines.push(` // Truncated: showing ${edges.length} of ${totalFileEdges} edges`);
|
|
123
119
|
}
|
|
124
120
|
} else {
|
|
125
|
-
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
e.kind AS edge_kind
|
|
130
|
-
FROM edges e
|
|
131
|
-
JOIN nodes n1 ON e.source_id = n1.id
|
|
132
|
-
JOIN nodes n2 ON e.target_id = n2.id
|
|
133
|
-
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
134
|
-
AND e.kind = 'calls'
|
|
135
|
-
AND e.confidence >= ?
|
|
136
|
-
`)
|
|
137
|
-
.all(minConf);
|
|
138
|
-
if (noTests)
|
|
139
|
-
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
140
|
-
const totalFnEdges = edges.length;
|
|
141
|
-
if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
|
|
142
|
-
|
|
143
|
-
for (const e of edges) {
|
|
144
|
-
const sId = `${e.source_file}:${e.source_name}`.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
145
|
-
const tId = `${e.target_file}:${e.target_name}`.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
146
|
-
lines.push(` ${sId} [label="${e.source_name}\\n${path.basename(e.source_file)}"];`);
|
|
147
|
-
lines.push(` ${tId} [label="${e.target_name}\\n${path.basename(e.target_file)}"];`);
|
|
148
|
-
lines.push(` ${sId} -> ${tId};`);
|
|
149
|
-
}
|
|
150
|
-
if (edgeLimit && totalFnEdges > edgeLimit) {
|
|
151
|
-
lines.push(` // Truncated: showing ${edges.length} of ${totalFnEdges} edges`);
|
|
121
|
+
for (const file of allFiles) {
|
|
122
|
+
const dir = path.dirname(file) || '.';
|
|
123
|
+
if (!dirs.has(dir)) dirs.set(dir, { files: [], cohesion: null });
|
|
124
|
+
dirs.get(dir).files.push(file);
|
|
152
125
|
}
|
|
153
126
|
}
|
|
154
127
|
|
|
155
|
-
|
|
156
|
-
|
|
128
|
+
return [...dirs]
|
|
129
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
130
|
+
.map(([name, info]) => ({
|
|
131
|
+
name,
|
|
132
|
+
files: info.files.map((f) => ({ path: f, basename: path.basename(f) })),
|
|
133
|
+
cohesion: info.cohesion,
|
|
134
|
+
}));
|
|
157
135
|
}
|
|
158
136
|
|
|
159
|
-
/**
|
|
160
|
-
|
|
161
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Load directory groupings for Mermaid file-level graphs (simplified — no cohesion, string arrays).
|
|
139
|
+
*/
|
|
140
|
+
function loadMermaidDirectoryGroups(db, allFiles) {
|
|
141
|
+
const hasDirectoryNodes =
|
|
142
|
+
db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'directory'").get().c > 0;
|
|
143
|
+
|
|
144
|
+
const dirs = new Map();
|
|
145
|
+
|
|
146
|
+
if (hasDirectoryNodes) {
|
|
147
|
+
const dbDirs = db.prepare("SELECT id, name FROM nodes WHERE kind = 'directory'").all();
|
|
148
|
+
for (const d of dbDirs) {
|
|
149
|
+
const containedFiles = db
|
|
150
|
+
.prepare(`
|
|
151
|
+
SELECT n.name FROM edges e
|
|
152
|
+
JOIN nodes n ON e.target_id = n.id
|
|
153
|
+
WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
|
|
154
|
+
`)
|
|
155
|
+
.all(d.id)
|
|
156
|
+
.map((r) => r.name)
|
|
157
|
+
.filter((f) => allFiles.has(f));
|
|
158
|
+
if (containedFiles.length > 0) dirs.set(d.name, containedFiles);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
for (const file of allFiles) {
|
|
162
|
+
const dir = path.dirname(file) || '.';
|
|
163
|
+
if (!dirs.has(dir)) dirs.set(dir, []);
|
|
164
|
+
dirs.get(dir).push(file);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return [...dirs]
|
|
169
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
170
|
+
.map(([name, files]) => ({ name, files }));
|
|
162
171
|
}
|
|
163
172
|
|
|
164
|
-
/**
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Load node roles for Mermaid function-level styling.
|
|
175
|
+
* @returns {Map<string, string>} "file::name" → role
|
|
176
|
+
*/
|
|
177
|
+
function loadNodeRoles(db, edges) {
|
|
178
|
+
const roles = new Map();
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
for (const e of edges) {
|
|
181
|
+
for (const [file, name] of [
|
|
182
|
+
[e.source_file, e.source_name],
|
|
183
|
+
[e.target_file, e.target_name],
|
|
184
|
+
]) {
|
|
185
|
+
const key = `${file}::${name}`;
|
|
186
|
+
if (seen.has(key)) continue;
|
|
187
|
+
seen.add(key);
|
|
188
|
+
const row = db
|
|
189
|
+
.prepare('SELECT role FROM nodes WHERE file = ? AND name = ? AND role IS NOT NULL LIMIT 1')
|
|
190
|
+
.get(file, name);
|
|
191
|
+
if (row?.role) roles.set(key, row.role);
|
|
192
|
+
}
|
|
183
193
|
}
|
|
194
|
+
return roles;
|
|
184
195
|
}
|
|
185
196
|
|
|
186
|
-
|
|
187
|
-
const ROLE_STYLES = {
|
|
188
|
-
entry: 'fill:#e8f5e9,stroke:#4caf50',
|
|
189
|
-
core: 'fill:#e3f2fd,stroke:#2196f3',
|
|
190
|
-
utility: 'fill:#f5f5f5,stroke:#9e9e9e',
|
|
191
|
-
dead: 'fill:#ffebee,stroke:#f44336',
|
|
192
|
-
leaf: 'fill:#fffde7,stroke:#fdd835',
|
|
193
|
-
};
|
|
197
|
+
// ─── Public API ──────────────────────────────────────────────────────
|
|
194
198
|
|
|
195
199
|
/**
|
|
196
|
-
* Export the dependency graph in
|
|
200
|
+
* Export the dependency graph in DOT (Graphviz) format.
|
|
197
201
|
*/
|
|
198
|
-
export function
|
|
202
|
+
export function exportDOT(db, opts = {}) {
|
|
199
203
|
const fileLevel = opts.fileLevel !== false;
|
|
200
204
|
const noTests = opts.noTests || false;
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const edgeLimit = opts.limit;
|
|
204
|
-
const lines = [`flowchart ${direction}`];
|
|
205
|
-
|
|
206
|
-
let nodeCounter = 0;
|
|
207
|
-
const nodeIdMap = new Map();
|
|
208
|
-
function nodeId(key) {
|
|
209
|
-
if (!nodeIdMap.has(key)) nodeIdMap.set(key, `n${nodeCounter++}`);
|
|
210
|
-
return nodeIdMap.get(key);
|
|
211
|
-
}
|
|
205
|
+
const minConfidence = opts.minConfidence;
|
|
206
|
+
const limit = opts.limit;
|
|
212
207
|
|
|
213
208
|
if (fileLevel) {
|
|
214
|
-
|
|
215
|
-
.prepare(`
|
|
216
|
-
SELECT DISTINCT n1.file AS source, n2.file AS target, e.kind AS edge_kind
|
|
217
|
-
FROM edges e
|
|
218
|
-
JOIN nodes n1 ON e.source_id = n1.id
|
|
219
|
-
JOIN nodes n2 ON e.target_id = n2.id
|
|
220
|
-
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
|
|
221
|
-
AND e.confidence >= ?
|
|
222
|
-
`)
|
|
223
|
-
.all(minConf);
|
|
224
|
-
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
225
|
-
const totalMermaidFileEdges = edges.length;
|
|
226
|
-
if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
|
|
227
|
-
|
|
228
|
-
// Collect all files referenced in edges
|
|
209
|
+
const { edges, totalEdges } = loadFileLevelEdges(db, { noTests, minConfidence, limit });
|
|
229
210
|
const allFiles = new Set();
|
|
230
211
|
for (const { source, target } of edges) {
|
|
231
212
|
allFiles.add(source);
|
|
232
213
|
allFiles.add(target);
|
|
233
214
|
}
|
|
215
|
+
const dirs = loadDirectoryGroups(db, allFiles);
|
|
216
|
+
return renderFileLevelDOT({ dirs, edges, totalEdges, limit });
|
|
217
|
+
}
|
|
234
218
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'directory'").get().c > 0;
|
|
239
|
-
|
|
240
|
-
if (hasDirectoryNodes) {
|
|
241
|
-
const dbDirs = db.prepare("SELECT id, name FROM nodes WHERE kind = 'directory'").all();
|
|
242
|
-
for (const d of dbDirs) {
|
|
243
|
-
const containedFiles = db
|
|
244
|
-
.prepare(`
|
|
245
|
-
SELECT n.name FROM edges e
|
|
246
|
-
JOIN nodes n ON e.target_id = n.id
|
|
247
|
-
WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
|
|
248
|
-
`)
|
|
249
|
-
.all(d.id)
|
|
250
|
-
.map((r) => r.name)
|
|
251
|
-
.filter((f) => allFiles.has(f));
|
|
252
|
-
if (containedFiles.length > 0) dirs.set(d.name, containedFiles);
|
|
253
|
-
}
|
|
254
|
-
} else {
|
|
255
|
-
for (const file of allFiles) {
|
|
256
|
-
const dir = path.dirname(file) || '.';
|
|
257
|
-
if (!dirs.has(dir)) dirs.set(dir, []);
|
|
258
|
-
dirs.get(dir).push(file);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Emit subgraphs
|
|
263
|
-
for (const [dir, files] of [...dirs].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
264
|
-
const sgId = dir.replace(/[^a-zA-Z0-9]/g, '_');
|
|
265
|
-
lines.push(` subgraph ${sgId}["${escapeLabel(dir)}"]`);
|
|
266
|
-
for (const f of files) {
|
|
267
|
-
const nId = nodeId(f);
|
|
268
|
-
lines.push(` ${nId}["${escapeLabel(path.basename(f))}"]`);
|
|
269
|
-
}
|
|
270
|
-
lines.push(' end');
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Deduplicate edges per source-target pair, collecting all distinct kinds
|
|
274
|
-
const edgeMap = new Map();
|
|
275
|
-
for (const { source, target, edge_kind } of edges) {
|
|
276
|
-
const key = `${source}|${target}`;
|
|
277
|
-
const label = edge_kind === 'imports-type' ? 'imports' : edge_kind;
|
|
278
|
-
if (!edgeMap.has(key)) edgeMap.set(key, { source, target, labels: new Set() });
|
|
279
|
-
edgeMap.get(key).labels.add(label);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
for (const { source, target, labels } of edgeMap.values()) {
|
|
283
|
-
lines.push(` ${nodeId(source)} -->|${[...labels].join(', ')}| ${nodeId(target)}`);
|
|
284
|
-
}
|
|
285
|
-
if (edgeLimit && totalMermaidFileEdges > edgeLimit) {
|
|
286
|
-
lines.push(` %% Truncated: showing ${edges.length} of ${totalMermaidFileEdges} edges`);
|
|
287
|
-
}
|
|
288
|
-
} else {
|
|
289
|
-
let edges = db
|
|
290
|
-
.prepare(`
|
|
291
|
-
SELECT n1.name AS source_name, n1.kind AS source_kind, n1.file AS source_file,
|
|
292
|
-
n2.name AS target_name, n2.kind AS target_kind, n2.file AS target_file,
|
|
293
|
-
e.kind AS edge_kind
|
|
294
|
-
FROM edges e
|
|
295
|
-
JOIN nodes n1 ON e.source_id = n1.id
|
|
296
|
-
JOIN nodes n2 ON e.target_id = n2.id
|
|
297
|
-
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
298
|
-
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
299
|
-
AND e.kind = 'calls'
|
|
300
|
-
AND e.confidence >= ?
|
|
301
|
-
`)
|
|
302
|
-
.all(minConf);
|
|
303
|
-
if (noTests)
|
|
304
|
-
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
305
|
-
const totalMermaidFnEdges = edges.length;
|
|
306
|
-
if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
|
|
307
|
-
|
|
308
|
-
// Group nodes by file for subgraphs
|
|
309
|
-
const fileNodes = new Map();
|
|
310
|
-
const nodeKinds = new Map();
|
|
311
|
-
for (const e of edges) {
|
|
312
|
-
const sKey = `${e.source_file}::${e.source_name}`;
|
|
313
|
-
const tKey = `${e.target_file}::${e.target_name}`;
|
|
314
|
-
nodeId(sKey);
|
|
315
|
-
nodeId(tKey);
|
|
316
|
-
nodeKinds.set(sKey, e.source_kind);
|
|
317
|
-
nodeKinds.set(tKey, e.target_kind);
|
|
318
|
-
|
|
319
|
-
if (!fileNodes.has(e.source_file)) fileNodes.set(e.source_file, new Map());
|
|
320
|
-
fileNodes.get(e.source_file).set(sKey, e.source_name);
|
|
321
|
-
|
|
322
|
-
if (!fileNodes.has(e.target_file)) fileNodes.set(e.target_file, new Map());
|
|
323
|
-
fileNodes.get(e.target_file).set(tKey, e.target_name);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Emit subgraphs grouped by file
|
|
327
|
-
for (const [file, nodes] of [...fileNodes].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
328
|
-
const sgId = file.replace(/[^a-zA-Z0-9]/g, '_');
|
|
329
|
-
lines.push(` subgraph ${sgId}["${escapeLabel(file)}"]`);
|
|
330
|
-
for (const [key, name] of nodes) {
|
|
331
|
-
const kind = nodeKinds.get(key);
|
|
332
|
-
lines.push(` ${nodeId(key)}${mermaidShape(kind, name)}`);
|
|
333
|
-
}
|
|
334
|
-
lines.push(' end');
|
|
335
|
-
}
|
|
219
|
+
const { edges, totalEdges } = loadFunctionLevelEdges(db, { noTests, minConfidence, limit });
|
|
220
|
+
return renderFunctionLevelDOT({ edges, totalEdges, limit });
|
|
221
|
+
}
|
|
336
222
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Export the dependency graph in Mermaid format.
|
|
225
|
+
*/
|
|
226
|
+
export function exportMermaid(db, opts = {}) {
|
|
227
|
+
const fileLevel = opts.fileLevel !== false;
|
|
228
|
+
const noTests = opts.noTests || false;
|
|
229
|
+
const minConfidence = opts.minConfidence;
|
|
230
|
+
const direction = opts.direction || 'LR';
|
|
231
|
+
const limit = opts.limit;
|
|
346
232
|
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (row?.role && ROLE_STYLES[row.role]) {
|
|
359
|
-
roleStyles.push(` style ${nodeIdMap.get(key)} ${ROLE_STYLES[row.role]}`);
|
|
360
|
-
}
|
|
233
|
+
if (fileLevel) {
|
|
234
|
+
const { edges, totalEdges } = loadFileLevelEdges(db, {
|
|
235
|
+
noTests,
|
|
236
|
+
minConfidence,
|
|
237
|
+
limit,
|
|
238
|
+
includeKind: true,
|
|
239
|
+
});
|
|
240
|
+
const allFiles = new Set();
|
|
241
|
+
for (const { source, target } of edges) {
|
|
242
|
+
allFiles.add(source);
|
|
243
|
+
allFiles.add(target);
|
|
361
244
|
}
|
|
362
|
-
|
|
245
|
+
const dirs = loadMermaidDirectoryGroups(db, allFiles);
|
|
246
|
+
return renderFileLevelMermaid({ direction, dirs, edges, totalEdges, limit });
|
|
363
247
|
}
|
|
364
248
|
|
|
365
|
-
|
|
249
|
+
const { edges, totalEdges } = loadFunctionLevelEdges(db, { noTests, minConfidence, limit });
|
|
250
|
+
const roles = loadNodeRoles(db, edges);
|
|
251
|
+
return renderFunctionLevelMermaid({ direction, edges, roles, totalEdges, limit });
|
|
366
252
|
}
|
|
367
253
|
|
|
368
254
|
/**
|
|
@@ -400,129 +286,16 @@ export function exportJSON(db, opts = {}) {
|
|
|
400
286
|
export function exportGraphML(db, opts = {}) {
|
|
401
287
|
const fileLevel = opts.fileLevel !== false;
|
|
402
288
|
const noTests = opts.noTests || false;
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
const lines = [
|
|
407
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
408
|
-
'<graphml xmlns="http://graphml.graphstruct.net/graphml">',
|
|
409
|
-
];
|
|
289
|
+
const minConfidence = opts.minConfidence;
|
|
290
|
+
const limit = opts.limit;
|
|
410
291
|
|
|
411
292
|
if (fileLevel) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
lines.push(' <key id="d2" for="edge" attr.name="kind" attr.type="string"/>');
|
|
415
|
-
lines.push(' <graph id="codegraph" edgedefault="directed">');
|
|
416
|
-
|
|
417
|
-
let edges = db
|
|
418
|
-
.prepare(`
|
|
419
|
-
SELECT DISTINCT n1.file AS source, n2.file AS target
|
|
420
|
-
FROM edges e
|
|
421
|
-
JOIN nodes n1 ON e.source_id = n1.id
|
|
422
|
-
JOIN nodes n2 ON e.target_id = n2.id
|
|
423
|
-
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
|
|
424
|
-
AND e.confidence >= ?
|
|
425
|
-
`)
|
|
426
|
-
.all(minConf);
|
|
427
|
-
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
428
|
-
if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
|
|
429
|
-
|
|
430
|
-
const files = new Set();
|
|
431
|
-
for (const { source, target } of edges) {
|
|
432
|
-
files.add(source);
|
|
433
|
-
files.add(target);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const fileIds = new Map();
|
|
437
|
-
let nIdx = 0;
|
|
438
|
-
for (const f of files) {
|
|
439
|
-
const id = `n${nIdx++}`;
|
|
440
|
-
fileIds.set(f, id);
|
|
441
|
-
lines.push(` <node id="${id}">`);
|
|
442
|
-
lines.push(` <data key="d0">${escapeXml(path.basename(f))}</data>`);
|
|
443
|
-
lines.push(` <data key="d1">${escapeXml(f)}</data>`);
|
|
444
|
-
lines.push(' </node>');
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
let eIdx = 0;
|
|
448
|
-
for (const { source, target } of edges) {
|
|
449
|
-
lines.push(
|
|
450
|
-
` <edge id="e${eIdx++}" source="${fileIds.get(source)}" target="${fileIds.get(target)}">`,
|
|
451
|
-
);
|
|
452
|
-
lines.push(' <data key="d2">imports</data>');
|
|
453
|
-
lines.push(' </edge>');
|
|
454
|
-
}
|
|
455
|
-
} else {
|
|
456
|
-
lines.push(' <key id="d0" for="node" attr.name="name" attr.type="string"/>');
|
|
457
|
-
lines.push(' <key id="d1" for="node" attr.name="kind" attr.type="string"/>');
|
|
458
|
-
lines.push(' <key id="d2" for="node" attr.name="file" attr.type="string"/>');
|
|
459
|
-
lines.push(' <key id="d3" for="node" attr.name="line" attr.type="int"/>');
|
|
460
|
-
lines.push(' <key id="d4" for="node" attr.name="role" attr.type="string"/>');
|
|
461
|
-
lines.push(' <key id="d5" for="edge" attr.name="kind" attr.type="string"/>');
|
|
462
|
-
lines.push(' <key id="d6" for="edge" attr.name="confidence" attr.type="double"/>');
|
|
463
|
-
lines.push(' <graph id="codegraph" edgedefault="directed">');
|
|
464
|
-
|
|
465
|
-
let edges = db
|
|
466
|
-
.prepare(`
|
|
467
|
-
SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
|
|
468
|
-
n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
|
|
469
|
-
n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
|
|
470
|
-
n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
|
|
471
|
-
e.kind AS edge_kind, e.confidence
|
|
472
|
-
FROM edges e
|
|
473
|
-
JOIN nodes n1 ON e.source_id = n1.id
|
|
474
|
-
JOIN nodes n2 ON e.target_id = n2.id
|
|
475
|
-
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
476
|
-
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
477
|
-
AND e.kind = 'calls'
|
|
478
|
-
AND e.confidence >= ?
|
|
479
|
-
`)
|
|
480
|
-
.all(minConf);
|
|
481
|
-
if (noTests)
|
|
482
|
-
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
483
|
-
if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
|
|
484
|
-
|
|
485
|
-
const emittedNodes = new Set();
|
|
486
|
-
function emitNode(id, name, kind, file, line, role) {
|
|
487
|
-
if (emittedNodes.has(id)) return;
|
|
488
|
-
emittedNodes.add(id);
|
|
489
|
-
lines.push(` <node id="n${id}">`);
|
|
490
|
-
lines.push(` <data key="d0">${escapeXml(name)}</data>`);
|
|
491
|
-
lines.push(` <data key="d1">${escapeXml(kind)}</data>`);
|
|
492
|
-
lines.push(` <data key="d2">${escapeXml(file)}</data>`);
|
|
493
|
-
lines.push(` <data key="d3">${line}</data>`);
|
|
494
|
-
if (role) lines.push(` <data key="d4">${escapeXml(role)}</data>`);
|
|
495
|
-
lines.push(' </node>');
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
let eIdx = 0;
|
|
499
|
-
for (const e of edges) {
|
|
500
|
-
emitNode(
|
|
501
|
-
e.source_id,
|
|
502
|
-
e.source_name,
|
|
503
|
-
e.source_kind,
|
|
504
|
-
e.source_file,
|
|
505
|
-
e.source_line,
|
|
506
|
-
e.source_role,
|
|
507
|
-
);
|
|
508
|
-
emitNode(
|
|
509
|
-
e.target_id,
|
|
510
|
-
e.target_name,
|
|
511
|
-
e.target_kind,
|
|
512
|
-
e.target_file,
|
|
513
|
-
e.target_line,
|
|
514
|
-
e.target_role,
|
|
515
|
-
);
|
|
516
|
-
lines.push(` <edge id="e${eIdx++}" source="n${e.source_id}" target="n${e.target_id}">`);
|
|
517
|
-
lines.push(` <data key="d5">${escapeXml(e.edge_kind)}</data>`);
|
|
518
|
-
lines.push(` <data key="d6">${e.confidence}</data>`);
|
|
519
|
-
lines.push(' </edge>');
|
|
520
|
-
}
|
|
293
|
+
const { edges } = loadFileLevelEdges(db, { noTests, minConfidence, limit });
|
|
294
|
+
return renderFileLevelGraphML({ edges });
|
|
521
295
|
}
|
|
522
296
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
return lines.join('\n');
|
|
297
|
+
const { edges } = loadFunctionLevelEdges(db, { noTests, minConfidence, limit });
|
|
298
|
+
return renderFunctionLevelGraphML({ edges });
|
|
526
299
|
}
|
|
527
300
|
|
|
528
301
|
/**
|
|
@@ -586,96 +359,20 @@ export function exportGraphSON(db, opts = {}) {
|
|
|
586
359
|
export function exportNeo4jCSV(db, opts = {}) {
|
|
587
360
|
const fileLevel = opts.fileLevel !== false;
|
|
588
361
|
const noTests = opts.noTests || false;
|
|
589
|
-
const
|
|
590
|
-
const
|
|
362
|
+
const minConfidence = opts.minConfidence;
|
|
363
|
+
const limit = opts.limit;
|
|
591
364
|
|
|
592
365
|
if (fileLevel) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
`)
|
|
602
|
-
.all(minConf);
|
|
603
|
-
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
604
|
-
if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
|
|
605
|
-
|
|
606
|
-
const files = new Map();
|
|
607
|
-
let idx = 0;
|
|
608
|
-
for (const { source, target } of edges) {
|
|
609
|
-
if (!files.has(source)) files.set(source, idx++);
|
|
610
|
-
if (!files.has(target)) files.set(target, idx++);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const nodeLines = ['nodeId:ID,name,file:string,:LABEL'];
|
|
614
|
-
for (const [file, id] of files) {
|
|
615
|
-
nodeLines.push(`${id},${escapeCsv(path.basename(file))},${escapeCsv(file)},File`);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
|
|
619
|
-
for (const e of edges) {
|
|
620
|
-
const edgeType = e.kind.toUpperCase().replace(/-/g, '_');
|
|
621
|
-
relLines.push(`${files.get(e.source)},${files.get(e.target)},${edgeType},${e.confidence}`);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
let edges = db
|
|
628
|
-
.prepare(`
|
|
629
|
-
SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
|
|
630
|
-
n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
|
|
631
|
-
n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
|
|
632
|
-
n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
|
|
633
|
-
e.kind AS edge_kind, e.confidence
|
|
634
|
-
FROM edges e
|
|
635
|
-
JOIN nodes n1 ON e.source_id = n1.id
|
|
636
|
-
JOIN nodes n2 ON e.target_id = n2.id
|
|
637
|
-
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
638
|
-
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
639
|
-
AND e.kind = 'calls'
|
|
640
|
-
AND e.confidence >= ?
|
|
641
|
-
`)
|
|
642
|
-
.all(minConf);
|
|
643
|
-
if (noTests)
|
|
644
|
-
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
645
|
-
if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
|
|
646
|
-
|
|
647
|
-
const emitted = new Set();
|
|
648
|
-
const nodeLines = ['nodeId:ID,name,kind,file:string,line:int,role,:LABEL'];
|
|
649
|
-
function emitNode(id, name, kind, file, line, role) {
|
|
650
|
-
if (emitted.has(id)) return;
|
|
651
|
-
emitted.add(id);
|
|
652
|
-
const label = kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
653
|
-
nodeLines.push(
|
|
654
|
-
`${id},${escapeCsv(name)},${escapeCsv(kind)},${escapeCsv(file)},${line},${escapeCsv(role || '')},${label}`,
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
|
|
659
|
-
for (const e of edges) {
|
|
660
|
-
emitNode(
|
|
661
|
-
e.source_id,
|
|
662
|
-
e.source_name,
|
|
663
|
-
e.source_kind,
|
|
664
|
-
e.source_file,
|
|
665
|
-
e.source_line,
|
|
666
|
-
e.source_role,
|
|
667
|
-
);
|
|
668
|
-
emitNode(
|
|
669
|
-
e.target_id,
|
|
670
|
-
e.target_name,
|
|
671
|
-
e.target_kind,
|
|
672
|
-
e.target_file,
|
|
673
|
-
e.target_line,
|
|
674
|
-
e.target_role,
|
|
675
|
-
);
|
|
676
|
-
const edgeType = e.edge_kind.toUpperCase().replace(/-/g, '_');
|
|
677
|
-
relLines.push(`${e.source_id},${e.target_id},${edgeType},${e.confidence}`);
|
|
366
|
+
const { edges } = loadFileLevelEdges(db, {
|
|
367
|
+
noTests,
|
|
368
|
+
minConfidence,
|
|
369
|
+
limit,
|
|
370
|
+
includeKind: true,
|
|
371
|
+
includeConfidence: true,
|
|
372
|
+
});
|
|
373
|
+
return renderFileLevelNeo4jCSV({ edges });
|
|
678
374
|
}
|
|
679
375
|
|
|
680
|
-
|
|
376
|
+
const { edges } = loadFunctionLevelEdges(db, { noTests, minConfidence, limit });
|
|
377
|
+
return renderFunctionLevelNeo4jCSV({ edges });
|
|
681
378
|
}
|