@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
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findCallees,
|
|
3
|
+
findCallers,
|
|
4
|
+
findFileNodes,
|
|
5
|
+
findImportSources,
|
|
6
|
+
findImportTargets,
|
|
7
|
+
findNodesByFile,
|
|
8
|
+
openReadonlyOrFail,
|
|
9
|
+
} from '../db.js';
|
|
10
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
11
|
+
import { paginateResult } from '../paginate.js';
|
|
12
|
+
import { resolveMethodViaHierarchy } from '../shared/hierarchy.js';
|
|
13
|
+
import { normalizeSymbol } from '../shared/normalize.js';
|
|
14
|
+
import { findMatchingNodes } from './symbol-lookup.js';
|
|
15
|
+
|
|
16
|
+
export function fileDepsData(file, customDbPath, opts = {}) {
|
|
17
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
18
|
+
try {
|
|
19
|
+
const noTests = opts.noTests || false;
|
|
20
|
+
const fileNodes = findFileNodes(db, `%${file}%`);
|
|
21
|
+
if (fileNodes.length === 0) {
|
|
22
|
+
return { file, results: [] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const results = fileNodes.map((fn) => {
|
|
26
|
+
let importsTo = findImportTargets(db, fn.id);
|
|
27
|
+
if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));
|
|
28
|
+
|
|
29
|
+
let importedBy = findImportSources(db, fn.id);
|
|
30
|
+
if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
|
|
31
|
+
|
|
32
|
+
const defs = findNodesByFile(db, fn.file);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
file: fn.file,
|
|
36
|
+
imports: importsTo.map((i) => ({ file: i.file, typeOnly: i.edge_kind === 'imports-type' })),
|
|
37
|
+
importedBy: importedBy.map((i) => ({ file: i.file })),
|
|
38
|
+
definitions: defs.map((d) => ({ name: d.name, kind: d.kind, line: d.line })),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const base = { file, results };
|
|
43
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
44
|
+
} finally {
|
|
45
|
+
db.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function fnDepsData(name, customDbPath, opts = {}) {
|
|
50
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
51
|
+
try {
|
|
52
|
+
const depth = opts.depth || 3;
|
|
53
|
+
const noTests = opts.noTests || false;
|
|
54
|
+
const hc = new Map();
|
|
55
|
+
|
|
56
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
57
|
+
if (nodes.length === 0) {
|
|
58
|
+
return { name, results: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const results = nodes.map((node) => {
|
|
62
|
+
const callees = findCallees(db, node.id);
|
|
63
|
+
const filteredCallees = noTests ? callees.filter((c) => !isTestFile(c.file)) : callees;
|
|
64
|
+
|
|
65
|
+
let callers = findCallers(db, node.id);
|
|
66
|
+
|
|
67
|
+
if (node.kind === 'method' && node.name.includes('.')) {
|
|
68
|
+
const methodName = node.name.split('.').pop();
|
|
69
|
+
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
70
|
+
for (const rm of relatedMethods) {
|
|
71
|
+
if (rm.id === node.id) continue;
|
|
72
|
+
const extraCallers = findCallers(db, rm.id);
|
|
73
|
+
callers.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
77
|
+
|
|
78
|
+
// Transitive callers
|
|
79
|
+
const transitiveCallers = {};
|
|
80
|
+
if (depth > 1) {
|
|
81
|
+
const visited = new Set([node.id]);
|
|
82
|
+
let frontier = callers
|
|
83
|
+
.map((c) => {
|
|
84
|
+
const row = db
|
|
85
|
+
.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?')
|
|
86
|
+
.get(c.name, c.kind, c.file, c.line);
|
|
87
|
+
return row ? { ...c, id: row.id } : null;
|
|
88
|
+
})
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
|
|
91
|
+
for (let d = 2; d <= depth; d++) {
|
|
92
|
+
const nextFrontier = [];
|
|
93
|
+
for (const f of frontier) {
|
|
94
|
+
if (visited.has(f.id)) continue;
|
|
95
|
+
visited.add(f.id);
|
|
96
|
+
const upstream = db
|
|
97
|
+
.prepare(`
|
|
98
|
+
SELECT n.name, n.kind, n.file, n.line
|
|
99
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
100
|
+
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
101
|
+
`)
|
|
102
|
+
.all(f.id);
|
|
103
|
+
for (const u of upstream) {
|
|
104
|
+
if (noTests && isTestFile(u.file)) continue;
|
|
105
|
+
const uid = db
|
|
106
|
+
.prepare(
|
|
107
|
+
'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
|
|
108
|
+
)
|
|
109
|
+
.get(u.name, u.kind, u.file, u.line)?.id;
|
|
110
|
+
if (uid && !visited.has(uid)) {
|
|
111
|
+
nextFrontier.push({ ...u, id: uid });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (nextFrontier.length > 0) {
|
|
116
|
+
transitiveCallers[d] = nextFrontier.map((n) => ({
|
|
117
|
+
name: n.name,
|
|
118
|
+
kind: n.kind,
|
|
119
|
+
file: n.file,
|
|
120
|
+
line: n.line,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
frontier = nextFrontier;
|
|
124
|
+
if (frontier.length === 0) break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...normalizeSymbol(node, db, hc),
|
|
130
|
+
callees: filteredCallees.map((c) => ({
|
|
131
|
+
name: c.name,
|
|
132
|
+
kind: c.kind,
|
|
133
|
+
file: c.file,
|
|
134
|
+
line: c.line,
|
|
135
|
+
})),
|
|
136
|
+
callers: callers.map((c) => ({
|
|
137
|
+
name: c.name,
|
|
138
|
+
kind: c.kind,
|
|
139
|
+
file: c.file,
|
|
140
|
+
line: c.line,
|
|
141
|
+
viaHierarchy: c.viaHierarchy || undefined,
|
|
142
|
+
})),
|
|
143
|
+
transitiveCallers,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const base = { name, results };
|
|
148
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
149
|
+
} finally {
|
|
150
|
+
db.close();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function pathData(from, to, customDbPath, opts = {}) {
|
|
155
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
156
|
+
try {
|
|
157
|
+
const noTests = opts.noTests || false;
|
|
158
|
+
const maxDepth = opts.maxDepth || 10;
|
|
159
|
+
const edgeKinds = opts.edgeKinds || ['calls'];
|
|
160
|
+
const reverse = opts.reverse || false;
|
|
161
|
+
|
|
162
|
+
const fromNodes = findMatchingNodes(db, from, {
|
|
163
|
+
noTests,
|
|
164
|
+
file: opts.fromFile,
|
|
165
|
+
kind: opts.kind,
|
|
166
|
+
});
|
|
167
|
+
if (fromNodes.length === 0) {
|
|
168
|
+
return {
|
|
169
|
+
from,
|
|
170
|
+
to,
|
|
171
|
+
found: false,
|
|
172
|
+
error: `No symbol matching "${from}"`,
|
|
173
|
+
fromCandidates: [],
|
|
174
|
+
toCandidates: [],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const toNodes = findMatchingNodes(db, to, {
|
|
179
|
+
noTests,
|
|
180
|
+
file: opts.toFile,
|
|
181
|
+
kind: opts.kind,
|
|
182
|
+
});
|
|
183
|
+
if (toNodes.length === 0) {
|
|
184
|
+
return {
|
|
185
|
+
from,
|
|
186
|
+
to,
|
|
187
|
+
found: false,
|
|
188
|
+
error: `No symbol matching "${to}"`,
|
|
189
|
+
fromCandidates: fromNodes
|
|
190
|
+
.slice(0, 5)
|
|
191
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
|
|
192
|
+
toCandidates: [],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const sourceNode = fromNodes[0];
|
|
197
|
+
const targetNode = toNodes[0];
|
|
198
|
+
|
|
199
|
+
const fromCandidates = fromNodes
|
|
200
|
+
.slice(0, 5)
|
|
201
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
202
|
+
const toCandidates = toNodes
|
|
203
|
+
.slice(0, 5)
|
|
204
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
205
|
+
|
|
206
|
+
// Self-path
|
|
207
|
+
if (sourceNode.id === targetNode.id) {
|
|
208
|
+
return {
|
|
209
|
+
from,
|
|
210
|
+
to,
|
|
211
|
+
fromCandidates,
|
|
212
|
+
toCandidates,
|
|
213
|
+
found: true,
|
|
214
|
+
hops: 0,
|
|
215
|
+
path: [
|
|
216
|
+
{
|
|
217
|
+
name: sourceNode.name,
|
|
218
|
+
kind: sourceNode.kind,
|
|
219
|
+
file: sourceNode.file,
|
|
220
|
+
line: sourceNode.line,
|
|
221
|
+
edgeKind: null,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
alternateCount: 0,
|
|
225
|
+
edgeKinds,
|
|
226
|
+
reverse,
|
|
227
|
+
maxDepth,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Build edge kind filter
|
|
232
|
+
const kindPlaceholders = edgeKinds.map(() => '?').join(', ');
|
|
233
|
+
|
|
234
|
+
// BFS — direction depends on `reverse` flag
|
|
235
|
+
// Forward: source_id → target_id (A calls... calls B)
|
|
236
|
+
// Reverse: target_id → source_id (B is called by... called by A)
|
|
237
|
+
const neighborQuery = reverse
|
|
238
|
+
? `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
239
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
240
|
+
WHERE e.target_id = ? AND e.kind IN (${kindPlaceholders})`
|
|
241
|
+
: `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
242
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
243
|
+
WHERE e.source_id = ? AND e.kind IN (${kindPlaceholders})`;
|
|
244
|
+
const neighborStmt = db.prepare(neighborQuery);
|
|
245
|
+
|
|
246
|
+
const visited = new Set([sourceNode.id]);
|
|
247
|
+
// parent map: nodeId → { parentId, edgeKind }
|
|
248
|
+
const parent = new Map();
|
|
249
|
+
let queue = [sourceNode.id];
|
|
250
|
+
let found = false;
|
|
251
|
+
let alternateCount = 0;
|
|
252
|
+
let foundDepth = -1;
|
|
253
|
+
|
|
254
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
255
|
+
const nextQueue = [];
|
|
256
|
+
for (const currentId of queue) {
|
|
257
|
+
const neighbors = neighborStmt.all(currentId, ...edgeKinds);
|
|
258
|
+
for (const n of neighbors) {
|
|
259
|
+
if (noTests && isTestFile(n.file)) continue;
|
|
260
|
+
if (n.id === targetNode.id) {
|
|
261
|
+
if (!found) {
|
|
262
|
+
found = true;
|
|
263
|
+
foundDepth = depth;
|
|
264
|
+
parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
|
|
265
|
+
}
|
|
266
|
+
alternateCount++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (!visited.has(n.id)) {
|
|
270
|
+
visited.add(n.id);
|
|
271
|
+
parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
|
|
272
|
+
nextQueue.push(n.id);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (found) break;
|
|
277
|
+
queue = nextQueue;
|
|
278
|
+
if (queue.length === 0) break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!found) {
|
|
282
|
+
return {
|
|
283
|
+
from,
|
|
284
|
+
to,
|
|
285
|
+
fromCandidates,
|
|
286
|
+
toCandidates,
|
|
287
|
+
found: false,
|
|
288
|
+
hops: null,
|
|
289
|
+
path: [],
|
|
290
|
+
alternateCount: 0,
|
|
291
|
+
edgeKinds,
|
|
292
|
+
reverse,
|
|
293
|
+
maxDepth,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// alternateCount includes the one we kept; subtract 1 for "alternates"
|
|
298
|
+
alternateCount = Math.max(0, alternateCount - 1);
|
|
299
|
+
|
|
300
|
+
// Reconstruct path from target back to source
|
|
301
|
+
const pathIds = [targetNode.id];
|
|
302
|
+
let cur = targetNode.id;
|
|
303
|
+
while (cur !== sourceNode.id) {
|
|
304
|
+
const p = parent.get(cur);
|
|
305
|
+
pathIds.push(p.parentId);
|
|
306
|
+
cur = p.parentId;
|
|
307
|
+
}
|
|
308
|
+
pathIds.reverse();
|
|
309
|
+
|
|
310
|
+
// Build path with node info
|
|
311
|
+
const nodeCache = new Map();
|
|
312
|
+
const getNode = (id) => {
|
|
313
|
+
if (nodeCache.has(id)) return nodeCache.get(id);
|
|
314
|
+
const row = db.prepare('SELECT name, kind, file, line FROM nodes WHERE id = ?').get(id);
|
|
315
|
+
nodeCache.set(id, row);
|
|
316
|
+
return row;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const resultPath = pathIds.map((id, idx) => {
|
|
320
|
+
const node = getNode(id);
|
|
321
|
+
const edgeKind = idx === 0 ? null : parent.get(id).edgeKind;
|
|
322
|
+
return { name: node.name, kind: node.kind, file: node.file, line: node.line, edgeKind };
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
from,
|
|
327
|
+
to,
|
|
328
|
+
fromCandidates,
|
|
329
|
+
toCandidates,
|
|
330
|
+
found: true,
|
|
331
|
+
hops: foundDepth,
|
|
332
|
+
path: resultPath,
|
|
333
|
+
alternateCount,
|
|
334
|
+
edgeKinds,
|
|
335
|
+
reverse,
|
|
336
|
+
maxDepth,
|
|
337
|
+
};
|
|
338
|
+
} finally {
|
|
339
|
+
db.close();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
findCrossFileCallTargets,
|
|
4
|
+
findDbPath,
|
|
5
|
+
findFileNodes,
|
|
6
|
+
findNodesByFile,
|
|
7
|
+
openReadonlyOrFail,
|
|
8
|
+
} from '../db.js';
|
|
9
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
10
|
+
import { paginateResult } from '../paginate.js';
|
|
11
|
+
import { createFileLinesReader, extractSignature, extractSummary } from '../shared/file-utils.js';
|
|
12
|
+
|
|
13
|
+
export function exportsData(file, customDbPath, opts = {}) {
|
|
14
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
15
|
+
try {
|
|
16
|
+
const noTests = opts.noTests || false;
|
|
17
|
+
|
|
18
|
+
const dbFilePath = findDbPath(customDbPath);
|
|
19
|
+
const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
|
|
20
|
+
|
|
21
|
+
const getFileLines = createFileLinesReader(repoRoot);
|
|
22
|
+
|
|
23
|
+
const unused = opts.unused || false;
|
|
24
|
+
const fileResults = exportsFileImpl(db, file, noTests, getFileLines, unused);
|
|
25
|
+
|
|
26
|
+
if (fileResults.length === 0) {
|
|
27
|
+
return paginateResult(
|
|
28
|
+
{ file, results: [], reexports: [], totalExported: 0, totalInternal: 0, totalUnused: 0 },
|
|
29
|
+
'results',
|
|
30
|
+
{ limit: opts.limit, offset: opts.offset },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// For single-file match return flat; for multi-match return first (like explainData)
|
|
35
|
+
const first = fileResults[0];
|
|
36
|
+
const base = {
|
|
37
|
+
file: first.file,
|
|
38
|
+
results: first.results,
|
|
39
|
+
reexports: first.reexports,
|
|
40
|
+
totalExported: first.totalExported,
|
|
41
|
+
totalInternal: first.totalInternal,
|
|
42
|
+
totalUnused: first.totalUnused,
|
|
43
|
+
};
|
|
44
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
45
|
+
} finally {
|
|
46
|
+
db.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function exportsFileImpl(db, target, noTests, getFileLines, unused) {
|
|
51
|
+
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
52
|
+
if (fileNodes.length === 0) return [];
|
|
53
|
+
|
|
54
|
+
// Detect whether exported column exists
|
|
55
|
+
let hasExportedCol = false;
|
|
56
|
+
try {
|
|
57
|
+
db.prepare('SELECT exported FROM nodes LIMIT 0').raw();
|
|
58
|
+
hasExportedCol = true;
|
|
59
|
+
} catch {
|
|
60
|
+
/* old DB without exported column */
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return fileNodes.map((fn) => {
|
|
64
|
+
const symbols = findNodesByFile(db, fn.file);
|
|
65
|
+
|
|
66
|
+
let exported;
|
|
67
|
+
if (hasExportedCol) {
|
|
68
|
+
// Use the exported column populated during build
|
|
69
|
+
exported = db
|
|
70
|
+
.prepare(
|
|
71
|
+
"SELECT * FROM nodes WHERE file = ? AND kind != 'file' AND exported = 1 ORDER BY line",
|
|
72
|
+
)
|
|
73
|
+
.all(fn.file);
|
|
74
|
+
} else {
|
|
75
|
+
// Fallback: symbols that have incoming calls from other files
|
|
76
|
+
const exportedIds = findCrossFileCallTargets(db, fn.file);
|
|
77
|
+
exported = symbols.filter((s) => exportedIds.has(s.id));
|
|
78
|
+
}
|
|
79
|
+
const internalCount = symbols.length - exported.length;
|
|
80
|
+
|
|
81
|
+
const results = exported.map((s) => {
|
|
82
|
+
const fileLines = getFileLines(fn.file);
|
|
83
|
+
|
|
84
|
+
let consumers = db
|
|
85
|
+
.prepare(
|
|
86
|
+
`SELECT n.name, n.file, n.line FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
87
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
88
|
+
)
|
|
89
|
+
.all(s.id);
|
|
90
|
+
if (noTests) consumers = consumers.filter((c) => !isTestFile(c.file));
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name: s.name,
|
|
94
|
+
kind: s.kind,
|
|
95
|
+
line: s.line,
|
|
96
|
+
endLine: s.end_line ?? null,
|
|
97
|
+
role: s.role || null,
|
|
98
|
+
signature: fileLines ? extractSignature(fileLines, s.line) : null,
|
|
99
|
+
summary: fileLines ? extractSummary(fileLines, s.line) : null,
|
|
100
|
+
consumers: consumers.map((c) => ({ name: c.name, file: c.file, line: c.line })),
|
|
101
|
+
consumerCount: consumers.length,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const totalUnused = results.filter((r) => r.consumerCount === 0).length;
|
|
106
|
+
|
|
107
|
+
// Files that re-export this file (barrel → this file)
|
|
108
|
+
const reexports = db
|
|
109
|
+
.prepare(
|
|
110
|
+
`SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
111
|
+
WHERE e.target_id = ? AND e.kind = 'reexports'`,
|
|
112
|
+
)
|
|
113
|
+
.all(fn.id)
|
|
114
|
+
.map((r) => ({ file: r.file }));
|
|
115
|
+
|
|
116
|
+
let filteredResults = results;
|
|
117
|
+
if (unused) {
|
|
118
|
+
filteredResults = results.filter((r) => r.consumerCount === 0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
file: fn.file,
|
|
123
|
+
results: filteredResults,
|
|
124
|
+
reexports,
|
|
125
|
+
totalExported: exported.length,
|
|
126
|
+
totalInternal: internalCount,
|
|
127
|
+
totalUnused,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|