@optave/codegraph 3.1.5 → 3.2.0
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 +3 -2
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +252 -258
- package/src/ast-analysis/shared.js +0 -12
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +2 -1
- package/src/cli/commands/audit.js +2 -1
- package/src/cli/commands/batch.js +2 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/cfg.js +2 -1
- package/src/cli/commands/check.js +20 -23
- package/src/cli/commands/children.js +6 -1
- package/src/cli/commands/complexity.js +2 -1
- package/src/cli/commands/context.js +6 -1
- package/src/cli/commands/dataflow.js +2 -1
- package/src/cli/commands/deps.js +8 -3
- package/src/cli/commands/flow.js +2 -1
- package/src/cli/commands/fn-impact.js +6 -1
- package/src/cli/commands/owners.js +4 -2
- package/src/cli/commands/query.js +6 -1
- package/src/cli/commands/roles.js +2 -1
- package/src/cli/commands/search.js +8 -2
- package/src/cli/commands/sequence.js +2 -1
- package/src/cli/commands/triage.js +38 -27
- package/src/db/connection.js +18 -12
- package/src/db/migrations.js +41 -64
- package/src/db/query-builder.js +60 -4
- package/src/db/repository/in-memory-repository.js +27 -16
- package/src/db/repository/nodes.js +8 -10
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +174 -190
- package/src/domain/analysis/dependencies.js +200 -146
- package/src/domain/analysis/exports.js +3 -2
- package/src/domain/analysis/impact.js +267 -152
- package/src/domain/analysis/module-map.js +247 -221
- package/src/domain/analysis/roles.js +8 -5
- package/src/domain/analysis/symbol-lookup.js +7 -5
- package/src/domain/graph/builder/helpers.js +1 -1
- package/src/domain/graph/builder/incremental.js +116 -90
- package/src/domain/graph/builder/pipeline.js +106 -80
- package/src/domain/graph/builder/stages/build-edges.js +318 -239
- package/src/domain/graph/builder/stages/detect-changes.js +198 -177
- package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
- package/src/domain/graph/watcher.js +2 -2
- package/src/domain/parser.js +20 -11
- package/src/domain/queries.js +1 -0
- package/src/domain/search/search/filters.js +9 -5
- package/src/domain/search/search/keyword.js +12 -5
- package/src/domain/search/search/prepare.js +13 -5
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +274 -304
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/features/ast.js +5 -3
- package/src/features/audit.js +4 -2
- package/src/features/boundaries.js +98 -83
- package/src/features/cfg.js +134 -143
- package/src/features/communities.js +68 -53
- package/src/features/complexity.js +143 -132
- package/src/features/dataflow.js +146 -149
- package/src/features/export.js +3 -3
- package/src/features/graph-enrichment.js +2 -2
- package/src/features/manifesto.js +9 -6
- package/src/features/owners.js +4 -3
- package/src/features/sequence.js +152 -141
- package/src/features/shared/find-nodes.js +31 -0
- package/src/features/structure.js +130 -99
- package/src/features/triage.js +83 -68
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.js +1 -0
- package/src/mcp/server.js +65 -56
- package/src/mcp/tool-registry.js +13 -0
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/index.js +2 -0
- package/src/presentation/brief.js +51 -0
- package/src/presentation/queries-cli/exports.js +21 -14
- package/src/presentation/queries-cli/impact.js +55 -39
- package/src/presentation/queries-cli/inspect.js +184 -189
- package/src/presentation/queries-cli/overview.js +57 -58
- package/src/presentation/queries-cli/path.js +36 -29
- package/src/presentation/table.js +0 -8
- package/src/shared/generators.js +7 -3
- package/src/shared/kinds.js +1 -1
|
@@ -46,6 +46,61 @@ export function fileDepsData(file, customDbPath, opts = {}) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* BFS transitive caller traversal starting from `callers` of `nodeId`.
|
|
51
|
+
* Returns an object keyed by depth (2..depth) → array of caller descriptors.
|
|
52
|
+
*/
|
|
53
|
+
function buildTransitiveCallers(db, callers, nodeId, depth, noTests) {
|
|
54
|
+
const transitiveCallers = {};
|
|
55
|
+
if (depth <= 1) return transitiveCallers;
|
|
56
|
+
|
|
57
|
+
const visited = new Set([nodeId]);
|
|
58
|
+
let frontier = callers
|
|
59
|
+
.map((c) => {
|
|
60
|
+
const row = db
|
|
61
|
+
.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?')
|
|
62
|
+
.get(c.name, c.kind, c.file, c.line);
|
|
63
|
+
return row ? { ...c, id: row.id } : null;
|
|
64
|
+
})
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
|
|
67
|
+
for (let d = 2; d <= depth; d++) {
|
|
68
|
+
const nextFrontier = [];
|
|
69
|
+
for (const f of frontier) {
|
|
70
|
+
if (visited.has(f.id)) continue;
|
|
71
|
+
visited.add(f.id);
|
|
72
|
+
const upstream = db
|
|
73
|
+
.prepare(`
|
|
74
|
+
SELECT n.name, n.kind, n.file, n.line
|
|
75
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
76
|
+
WHERE e.target_id = ? AND e.kind = 'calls'
|
|
77
|
+
`)
|
|
78
|
+
.all(f.id);
|
|
79
|
+
for (const u of upstream) {
|
|
80
|
+
if (noTests && isTestFile(u.file)) continue;
|
|
81
|
+
const uid = db
|
|
82
|
+
.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?')
|
|
83
|
+
.get(u.name, u.kind, u.file, u.line)?.id;
|
|
84
|
+
if (uid && !visited.has(uid)) {
|
|
85
|
+
nextFrontier.push({ ...u, id: uid });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (nextFrontier.length > 0) {
|
|
90
|
+
transitiveCallers[d] = nextFrontier.map((n) => ({
|
|
91
|
+
name: n.name,
|
|
92
|
+
kind: n.kind,
|
|
93
|
+
file: n.file,
|
|
94
|
+
line: n.line,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
frontier = nextFrontier;
|
|
98
|
+
if (frontier.length === 0) break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return transitiveCallers;
|
|
102
|
+
}
|
|
103
|
+
|
|
49
104
|
export function fnDepsData(name, customDbPath, opts = {}) {
|
|
50
105
|
const db = openReadonlyOrFail(customDbPath);
|
|
51
106
|
try {
|
|
@@ -75,55 +130,7 @@ export function fnDepsData(name, customDbPath, opts = {}) {
|
|
|
75
130
|
}
|
|
76
131
|
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
77
132
|
|
|
78
|
-
|
|
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
|
-
}
|
|
133
|
+
const transitiveCallers = buildTransitiveCallers(db, callers, node.id, depth, noTests);
|
|
127
134
|
|
|
128
135
|
return {
|
|
129
136
|
...normalizeSymbol(node, db, hc),
|
|
@@ -151,37 +158,40 @@ export function fnDepsData(name, customDbPath, opts = {}) {
|
|
|
151
158
|
}
|
|
152
159
|
}
|
|
153
160
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Resolve from/to symbol names to node records.
|
|
163
|
+
* Returns { sourceNode, targetNode, fromCandidates, toCandidates } on success,
|
|
164
|
+
* or { earlyResult } when a caller-facing error/not-found response should be returned immediately.
|
|
165
|
+
*/
|
|
166
|
+
function resolveEndpoints(db, from, to, opts) {
|
|
167
|
+
const { noTests = false } = opts;
|
|
161
168
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
const fromNodes = findMatchingNodes(db, from, {
|
|
170
|
+
noTests,
|
|
171
|
+
file: opts.fromFile,
|
|
172
|
+
kind: opts.kind,
|
|
173
|
+
});
|
|
174
|
+
if (fromNodes.length === 0) {
|
|
175
|
+
return {
|
|
176
|
+
earlyResult: {
|
|
169
177
|
from,
|
|
170
178
|
to,
|
|
171
179
|
found: false,
|
|
172
180
|
error: `No symbol matching "${from}"`,
|
|
173
181
|
fromCandidates: [],
|
|
174
182
|
toCandidates: [],
|
|
175
|
-
}
|
|
176
|
-
}
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
177
186
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
const toNodes = findMatchingNodes(db, to, {
|
|
188
|
+
noTests,
|
|
189
|
+
file: opts.toFile,
|
|
190
|
+
kind: opts.kind,
|
|
191
|
+
});
|
|
192
|
+
if (toNodes.length === 0) {
|
|
193
|
+
return {
|
|
194
|
+
earlyResult: {
|
|
185
195
|
from,
|
|
186
196
|
to,
|
|
187
197
|
found: false,
|
|
@@ -190,18 +200,118 @@ export function pathData(from, to, customDbPath, opts = {}) {
|
|
|
190
200
|
.slice(0, 5)
|
|
191
201
|
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
|
|
192
202
|
toCandidates: [],
|
|
193
|
-
}
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const fromCandidates = fromNodes
|
|
208
|
+
.slice(0, 5)
|
|
209
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
210
|
+
const toCandidates = toNodes
|
|
211
|
+
.slice(0, 5)
|
|
212
|
+
.map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line }));
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
sourceNode: fromNodes[0],
|
|
216
|
+
targetNode: toNodes[0],
|
|
217
|
+
fromCandidates,
|
|
218
|
+
toCandidates,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* BFS from sourceId toward targetId.
|
|
224
|
+
* Returns { found, parent, alternateCount, foundDepth }.
|
|
225
|
+
* `parent` maps nodeId → { parentId, edgeKind }.
|
|
226
|
+
*/
|
|
227
|
+
function bfsShortestPath(db, sourceId, targetId, edgeKinds, reverse, maxDepth, noTests) {
|
|
228
|
+
const kindPlaceholders = edgeKinds.map(() => '?').join(', ');
|
|
229
|
+
|
|
230
|
+
// Forward: source_id → target_id (A calls... calls B)
|
|
231
|
+
// Reverse: target_id → source_id (B is called by... called by A)
|
|
232
|
+
const neighborQuery = reverse
|
|
233
|
+
? `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
234
|
+
FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
235
|
+
WHERE e.target_id = ? AND e.kind IN (${kindPlaceholders})`
|
|
236
|
+
: `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
|
|
237
|
+
FROM edges e JOIN nodes n ON e.target_id = n.id
|
|
238
|
+
WHERE e.source_id = ? AND e.kind IN (${kindPlaceholders})`;
|
|
239
|
+
const neighborStmt = db.prepare(neighborQuery);
|
|
240
|
+
|
|
241
|
+
const visited = new Set([sourceId]);
|
|
242
|
+
const parent = new Map();
|
|
243
|
+
let queue = [sourceId];
|
|
244
|
+
let found = false;
|
|
245
|
+
let alternateCount = 0;
|
|
246
|
+
let foundDepth = -1;
|
|
247
|
+
|
|
248
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
249
|
+
const nextQueue = [];
|
|
250
|
+
for (const currentId of queue) {
|
|
251
|
+
const neighbors = neighborStmt.all(currentId, ...edgeKinds);
|
|
252
|
+
for (const n of neighbors) {
|
|
253
|
+
if (noTests && isTestFile(n.file)) continue;
|
|
254
|
+
if (n.id === targetId) {
|
|
255
|
+
if (!found) {
|
|
256
|
+
found = true;
|
|
257
|
+
foundDepth = depth;
|
|
258
|
+
parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
|
|
259
|
+
}
|
|
260
|
+
alternateCount++;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (!visited.has(n.id)) {
|
|
264
|
+
visited.add(n.id);
|
|
265
|
+
parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
|
|
266
|
+
nextQueue.push(n.id);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
194
269
|
}
|
|
270
|
+
if (found) break;
|
|
271
|
+
queue = nextQueue;
|
|
272
|
+
if (queue.length === 0) break;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { found, parent, alternateCount, foundDepth };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Walk the parent map from targetId back to sourceId and return an ordered
|
|
280
|
+
* array of node IDs source → target.
|
|
281
|
+
*/
|
|
282
|
+
function reconstructPath(db, pathIds, parent) {
|
|
283
|
+
const nodeCache = new Map();
|
|
284
|
+
const getNode = (id) => {
|
|
285
|
+
if (nodeCache.has(id)) return nodeCache.get(id);
|
|
286
|
+
const row = db.prepare('SELECT name, kind, file, line FROM nodes WHERE id = ?').get(id);
|
|
287
|
+
nodeCache.set(id, row);
|
|
288
|
+
return row;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return pathIds.map((id, idx) => {
|
|
292
|
+
const node = getNode(id);
|
|
293
|
+
const edgeKind = idx === 0 ? null : parent.get(id).edgeKind;
|
|
294
|
+
return { name: node.name, kind: node.kind, file: node.file, line: node.line, edgeKind };
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function pathData(from, to, customDbPath, opts = {}) {
|
|
299
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
300
|
+
try {
|
|
301
|
+
const noTests = opts.noTests || false;
|
|
302
|
+
const maxDepth = opts.maxDepth || 10;
|
|
303
|
+
const edgeKinds = opts.edgeKinds || ['calls'];
|
|
304
|
+
const reverse = opts.reverse || false;
|
|
195
305
|
|
|
196
|
-
const
|
|
197
|
-
|
|
306
|
+
const resolved = resolveEndpoints(db, from, to, {
|
|
307
|
+
noTests,
|
|
308
|
+
fromFile: opts.fromFile,
|
|
309
|
+
toFile: opts.toFile,
|
|
310
|
+
kind: opts.kind,
|
|
311
|
+
});
|
|
312
|
+
if (resolved.earlyResult) return resolved.earlyResult;
|
|
198
313
|
|
|
199
|
-
const fromCandidates =
|
|
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 }));
|
|
314
|
+
const { sourceNode, targetNode, fromCandidates, toCandidates } = resolved;
|
|
205
315
|
|
|
206
316
|
// Self-path
|
|
207
317
|
if (sourceNode.id === targetNode.id) {
|
|
@@ -228,55 +338,12 @@ export function pathData(from, to, customDbPath, opts = {}) {
|
|
|
228
338
|
};
|
|
229
339
|
}
|
|
230
340
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
}
|
|
341
|
+
const {
|
|
342
|
+
found,
|
|
343
|
+
parent,
|
|
344
|
+
alternateCount: rawAlternateCount,
|
|
345
|
+
foundDepth,
|
|
346
|
+
} = bfsShortestPath(db, sourceNode.id, targetNode.id, edgeKinds, reverse, maxDepth, noTests);
|
|
280
347
|
|
|
281
348
|
if (!found) {
|
|
282
349
|
return {
|
|
@@ -294,8 +361,8 @@ export function pathData(from, to, customDbPath, opts = {}) {
|
|
|
294
361
|
};
|
|
295
362
|
}
|
|
296
363
|
|
|
297
|
-
//
|
|
298
|
-
alternateCount = Math.max(0,
|
|
364
|
+
// rawAlternateCount includes the one we kept; subtract 1 for "alternates"
|
|
365
|
+
const alternateCount = Math.max(0, rawAlternateCount - 1);
|
|
299
366
|
|
|
300
367
|
// Reconstruct path from target back to source
|
|
301
368
|
const pathIds = [targetNode.id];
|
|
@@ -307,20 +374,7 @@ export function pathData(from, to, customDbPath, opts = {}) {
|
|
|
307
374
|
}
|
|
308
375
|
pathIds.reverse();
|
|
309
376
|
|
|
310
|
-
|
|
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
|
-
});
|
|
377
|
+
const resultPath = reconstructPath(db, pathIds, parent);
|
|
324
378
|
|
|
325
379
|
return {
|
|
326
380
|
from,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
findNodesByFile,
|
|
7
7
|
openReadonlyOrFail,
|
|
8
8
|
} from '../../db/index.js';
|
|
9
|
+
import { debug } from '../../infrastructure/logger.js';
|
|
9
10
|
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
10
11
|
import {
|
|
11
12
|
createFileLinesReader,
|
|
@@ -60,8 +61,8 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused) {
|
|
|
60
61
|
try {
|
|
61
62
|
db.prepare('SELECT exported FROM nodes LIMIT 0').raw();
|
|
62
63
|
hasExportedCol = true;
|
|
63
|
-
} catch {
|
|
64
|
-
|
|
64
|
+
} catch (e) {
|
|
65
|
+
debug(`exported column not available, using fallback: ${e.message}`);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
return fileNodes.map((fn) => {
|