@optave/codegraph 3.1.4 → 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.
Files changed (210) hide show
  1. package/README.md +29 -72
  2. package/package.json +10 -8
  3. package/src/ast-analysis/engine.js +260 -246
  4. package/src/ast-analysis/shared.js +2 -14
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +4 -7
  9. package/src/cli/commands/audit.js +11 -11
  10. package/src/cli/commands/batch.js +6 -5
  11. package/src/cli/commands/branch-compare.js +1 -1
  12. package/src/cli/commands/brief.js +12 -0
  13. package/src/cli/commands/build.js +1 -1
  14. package/src/cli/commands/cfg.js +5 -8
  15. package/src/cli/commands/check.js +28 -36
  16. package/src/cli/commands/children.js +9 -7
  17. package/src/cli/commands/co-change.js +5 -3
  18. package/src/cli/commands/communities.js +2 -6
  19. package/src/cli/commands/complexity.js +5 -3
  20. package/src/cli/commands/context.js +9 -8
  21. package/src/cli/commands/cycles.js +12 -8
  22. package/src/cli/commands/dataflow.js +5 -8
  23. package/src/cli/commands/deps.js +9 -8
  24. package/src/cli/commands/diff-impact.js +2 -6
  25. package/src/cli/commands/embed.js +1 -1
  26. package/src/cli/commands/export.js +34 -31
  27. package/src/cli/commands/exports.js +2 -6
  28. package/src/cli/commands/flow.js +5 -8
  29. package/src/cli/commands/fn-impact.js +9 -8
  30. package/src/cli/commands/impact.js +2 -6
  31. package/src/cli/commands/info.js +2 -2
  32. package/src/cli/commands/map.js +1 -1
  33. package/src/cli/commands/mcp.js +1 -1
  34. package/src/cli/commands/models.js +1 -1
  35. package/src/cli/commands/owners.js +5 -3
  36. package/src/cli/commands/path.js +2 -2
  37. package/src/cli/commands/plot.js +40 -31
  38. package/src/cli/commands/query.js +9 -8
  39. package/src/cli/commands/registry.js +2 -2
  40. package/src/cli/commands/roles.js +5 -8
  41. package/src/cli/commands/search.js +9 -3
  42. package/src/cli/commands/sequence.js +5 -8
  43. package/src/cli/commands/snapshot.js +6 -1
  44. package/src/cli/commands/stats.js +1 -1
  45. package/src/cli/commands/structure.js +5 -4
  46. package/src/cli/commands/triage.js +41 -30
  47. package/src/cli/commands/watch.js +1 -1
  48. package/src/cli/commands/where.js +2 -6
  49. package/src/cli/index.js +11 -5
  50. package/src/cli/shared/open-graph.js +13 -0
  51. package/src/cli/shared/options.js +22 -2
  52. package/src/cli.js +1 -1
  53. package/src/db/connection.js +140 -11
  54. package/src/{db.js → db/index.js} +12 -5
  55. package/src/db/migrations.js +42 -65
  56. package/src/db/query-builder.js +72 -9
  57. package/src/db/repository/base.js +1 -1
  58. package/src/db/repository/graph-read.js +3 -3
  59. package/src/db/repository/in-memory-repository.js +30 -28
  60. package/src/db/repository/nodes.js +10 -17
  61. package/src/domain/analysis/brief.js +155 -0
  62. package/src/domain/analysis/context.js +392 -0
  63. package/src/domain/analysis/dependencies.js +395 -0
  64. package/src/{analysis → domain/analysis}/exports.js +11 -6
  65. package/src/domain/analysis/impact.js +581 -0
  66. package/src/domain/analysis/module-map.js +348 -0
  67. package/src/{analysis → domain/analysis}/roles.js +12 -9
  68. package/src/{analysis → domain/analysis}/symbol-lookup.js +19 -11
  69. package/src/{builder → domain/graph/builder}/helpers.js +4 -4
  70. package/src/{builder → domain/graph/builder}/incremental.js +119 -93
  71. package/src/domain/graph/builder/pipeline.js +156 -0
  72. package/src/domain/graph/builder/stages/build-edges.js +376 -0
  73. package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
  74. package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
  75. package/src/{builder → domain/graph/builder}/stages/detect-changes.js +204 -183
  76. package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
  77. package/src/domain/graph/builder/stages/insert-nodes.js +203 -0
  78. package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
  79. package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
  80. package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/{cycles.js → domain/graph/cycles.js} +4 -4
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +2 -2
  85. package/src/{watcher.js → domain/graph/watcher.js} +7 -7
  86. package/src/{parser.js → domain/parser.js} +24 -15
  87. package/src/{queries.js → domain/queries.js} +17 -16
  88. package/src/{embeddings → domain/search}/generator.js +3 -3
  89. package/src/{embeddings → domain/search}/models.js +2 -2
  90. package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
  91. package/src/{embeddings → domain/search}/search/filters.js +9 -5
  92. package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
  93. package/src/{embeddings → domain/search}/search/keyword.js +13 -6
  94. package/src/{embeddings → domain/search}/search/prepare.js +15 -7
  95. package/src/{embeddings → domain/search}/search/semantic.js +1 -1
  96. package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
  97. package/src/extractors/csharp.js +224 -207
  98. package/src/extractors/go.js +176 -172
  99. package/src/extractors/hcl.js +94 -78
  100. package/src/extractors/java.js +213 -207
  101. package/src/extractors/javascript.js +275 -305
  102. package/src/extractors/php.js +234 -221
  103. package/src/extractors/python.js +252 -250
  104. package/src/extractors/ruby.js +192 -185
  105. package/src/extractors/rust.js +182 -167
  106. package/src/{ast.js → features/ast.js} +13 -11
  107. package/src/{audit.js → features/audit.js} +20 -46
  108. package/src/{batch.js → features/batch.js} +5 -5
  109. package/src/{boundaries.js → features/boundaries.js} +100 -85
  110. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  111. package/src/{cfg.js → features/cfg.js} +141 -150
  112. package/src/{check.js → features/check.js} +13 -30
  113. package/src/{cochange.js → features/cochange.js} +5 -5
  114. package/src/{communities.js → features/communities.js} +72 -57
  115. package/src/{complexity.js → features/complexity.js} +154 -143
  116. package/src/{dataflow.js → features/dataflow.js} +155 -158
  117. package/src/{export.js → features/export.js} +6 -6
  118. package/src/{flow.js → features/flow.js} +4 -4
  119. package/src/{viewer.js → features/graph-enrichment.js} +8 -8
  120. package/src/{manifesto.js → features/manifesto.js} +15 -12
  121. package/src/{owners.js → features/owners.js} +6 -5
  122. package/src/features/sequence.js +300 -0
  123. package/src/features/shared/find-nodes.js +31 -0
  124. package/src/{snapshot.js → features/snapshot.js} +3 -3
  125. package/src/{structure.js → features/structure.js} +139 -108
  126. package/src/features/triage.js +141 -0
  127. package/src/graph/builders/dependency.js +33 -14
  128. package/src/graph/classifiers/risk.js +3 -2
  129. package/src/graph/classifiers/roles.js +6 -3
  130. package/src/index.cjs +16 -0
  131. package/src/index.js +40 -39
  132. package/src/{native.js → infrastructure/native.js} +1 -1
  133. package/src/mcp/middleware.js +1 -1
  134. package/src/mcp/server.js +68 -59
  135. package/src/mcp/tool-registry.js +15 -2
  136. package/src/mcp/tools/ast-query.js +1 -1
  137. package/src/mcp/tools/audit.js +1 -1
  138. package/src/mcp/tools/batch-query.js +1 -1
  139. package/src/mcp/tools/branch-compare.js +3 -1
  140. package/src/mcp/tools/brief.js +8 -0
  141. package/src/mcp/tools/cfg.js +1 -1
  142. package/src/mcp/tools/check.js +3 -3
  143. package/src/mcp/tools/co-changes.js +1 -1
  144. package/src/mcp/tools/code-owners.js +1 -1
  145. package/src/mcp/tools/communities.js +1 -1
  146. package/src/mcp/tools/complexity.js +1 -1
  147. package/src/mcp/tools/dataflow.js +2 -2
  148. package/src/mcp/tools/execution-flow.js +2 -2
  149. package/src/mcp/tools/export-graph.js +2 -2
  150. package/src/mcp/tools/find-cycles.js +2 -2
  151. package/src/mcp/tools/index.js +2 -0
  152. package/src/mcp/tools/list-repos.js +1 -1
  153. package/src/mcp/tools/sequence.js +1 -1
  154. package/src/mcp/tools/structure.js +1 -1
  155. package/src/mcp/tools/triage.js +2 -2
  156. package/src/{commands → presentation}/audit.js +2 -2
  157. package/src/{commands → presentation}/batch.js +1 -1
  158. package/src/{commands → presentation}/branch-compare.js +2 -2
  159. package/src/presentation/brief.js +51 -0
  160. package/src/{commands → presentation}/cfg.js +1 -1
  161. package/src/{commands → presentation}/check.js +2 -2
  162. package/src/{commands → presentation}/communities.js +1 -1
  163. package/src/{commands → presentation}/complexity.js +1 -1
  164. package/src/{commands → presentation}/dataflow.js +1 -1
  165. package/src/{commands → presentation}/flow.js +2 -2
  166. package/src/{commands → presentation}/manifesto.js +1 -1
  167. package/src/{commands → presentation}/owners.js +1 -1
  168. package/src/presentation/queries-cli/exports.js +53 -0
  169. package/src/presentation/queries-cli/impact.js +214 -0
  170. package/src/presentation/queries-cli/index.js +5 -0
  171. package/src/presentation/queries-cli/inspect.js +329 -0
  172. package/src/presentation/queries-cli/overview.js +196 -0
  173. package/src/presentation/queries-cli/path.js +65 -0
  174. package/src/presentation/queries-cli.js +27 -0
  175. package/src/{commands → presentation}/query.js +1 -1
  176. package/src/presentation/result-formatter.js +126 -3
  177. package/src/{commands → presentation}/sequence.js +2 -2
  178. package/src/{commands → presentation}/structure.js +1 -1
  179. package/src/presentation/table.js +0 -8
  180. package/src/{commands → presentation}/triage.js +1 -1
  181. package/src/{constants.js → shared/constants.js} +1 -1
  182. package/src/shared/file-utils.js +2 -2
  183. package/src/shared/generators.js +9 -5
  184. package/src/shared/hierarchy.js +1 -1
  185. package/src/{kinds.js → shared/kinds.js} +1 -1
  186. package/src/analysis/context.js +0 -408
  187. package/src/analysis/dependencies.js +0 -341
  188. package/src/analysis/impact.js +0 -463
  189. package/src/analysis/module-map.js +0 -322
  190. package/src/builder/pipeline.js +0 -130
  191. package/src/builder/stages/build-edges.js +0 -297
  192. package/src/builder/stages/insert-nodes.js +0 -195
  193. package/src/mcp.js +0 -2
  194. package/src/queries-cli.js +0 -866
  195. package/src/sequence.js +0 -289
  196. package/src/triage.js +0 -126
  197. /package/src/{builder → domain/graph/builder}/context.js +0 -0
  198. /package/src/{builder.js → domain/graph/builder.js} +0 -0
  199. /package/src/{embeddings → domain/search}/index.js +0 -0
  200. /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
  201. /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
  202. /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
  203. /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
  204. /package/src/{config.js → infrastructure/config.js} +0 -0
  205. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  206. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  207. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  208. /package/src/{commands → presentation}/cochange.js +0 -0
  209. /package/src/{errors.js → shared/errors.js} +0 -0
  210. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -0,0 +1,395 @@
1
+ import {
2
+ findCallees,
3
+ findCallers,
4
+ findFileNodes,
5
+ findImportSources,
6
+ findImportTargets,
7
+ findNodesByFile,
8
+ openReadonlyOrFail,
9
+ } from '../../db/index.js';
10
+ import { isTestFile } from '../../infrastructure/test-filter.js';
11
+ import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
12
+ import { normalizeSymbol } from '../../shared/normalize.js';
13
+ import { paginateResult } from '../../shared/paginate.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
+ /**
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
+
104
+ export function fnDepsData(name, customDbPath, opts = {}) {
105
+ const db = openReadonlyOrFail(customDbPath);
106
+ try {
107
+ const depth = opts.depth || 3;
108
+ const noTests = opts.noTests || false;
109
+ const hc = new Map();
110
+
111
+ const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
112
+ if (nodes.length === 0) {
113
+ return { name, results: [] };
114
+ }
115
+
116
+ const results = nodes.map((node) => {
117
+ const callees = findCallees(db, node.id);
118
+ const filteredCallees = noTests ? callees.filter((c) => !isTestFile(c.file)) : callees;
119
+
120
+ let callers = findCallers(db, node.id);
121
+
122
+ if (node.kind === 'method' && node.name.includes('.')) {
123
+ const methodName = node.name.split('.').pop();
124
+ const relatedMethods = resolveMethodViaHierarchy(db, methodName);
125
+ for (const rm of relatedMethods) {
126
+ if (rm.id === node.id) continue;
127
+ const extraCallers = findCallers(db, rm.id);
128
+ callers.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
129
+ }
130
+ }
131
+ if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
132
+
133
+ const transitiveCallers = buildTransitiveCallers(db, callers, node.id, depth, noTests);
134
+
135
+ return {
136
+ ...normalizeSymbol(node, db, hc),
137
+ callees: filteredCallees.map((c) => ({
138
+ name: c.name,
139
+ kind: c.kind,
140
+ file: c.file,
141
+ line: c.line,
142
+ })),
143
+ callers: callers.map((c) => ({
144
+ name: c.name,
145
+ kind: c.kind,
146
+ file: c.file,
147
+ line: c.line,
148
+ viaHierarchy: c.viaHierarchy || undefined,
149
+ })),
150
+ transitiveCallers,
151
+ };
152
+ });
153
+
154
+ const base = { name, results };
155
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
156
+ } finally {
157
+ db.close();
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;
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: {
177
+ from,
178
+ to,
179
+ found: false,
180
+ error: `No symbol matching "${from}"`,
181
+ fromCandidates: [],
182
+ toCandidates: [],
183
+ },
184
+ };
185
+ }
186
+
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: {
195
+ from,
196
+ to,
197
+ found: false,
198
+ error: `No symbol matching "${to}"`,
199
+ fromCandidates: fromNodes
200
+ .slice(0, 5)
201
+ .map((n) => ({ name: n.name, kind: n.kind, file: n.file, line: n.line })),
202
+ toCandidates: [],
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
+ }
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;
305
+
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;
313
+
314
+ const { sourceNode, targetNode, fromCandidates, toCandidates } = resolved;
315
+
316
+ // Self-path
317
+ if (sourceNode.id === targetNode.id) {
318
+ return {
319
+ from,
320
+ to,
321
+ fromCandidates,
322
+ toCandidates,
323
+ found: true,
324
+ hops: 0,
325
+ path: [
326
+ {
327
+ name: sourceNode.name,
328
+ kind: sourceNode.kind,
329
+ file: sourceNode.file,
330
+ line: sourceNode.line,
331
+ edgeKind: null,
332
+ },
333
+ ],
334
+ alternateCount: 0,
335
+ edgeKinds,
336
+ reverse,
337
+ maxDepth,
338
+ };
339
+ }
340
+
341
+ const {
342
+ found,
343
+ parent,
344
+ alternateCount: rawAlternateCount,
345
+ foundDepth,
346
+ } = bfsShortestPath(db, sourceNode.id, targetNode.id, edgeKinds, reverse, maxDepth, noTests);
347
+
348
+ if (!found) {
349
+ return {
350
+ from,
351
+ to,
352
+ fromCandidates,
353
+ toCandidates,
354
+ found: false,
355
+ hops: null,
356
+ path: [],
357
+ alternateCount: 0,
358
+ edgeKinds,
359
+ reverse,
360
+ maxDepth,
361
+ };
362
+ }
363
+
364
+ // rawAlternateCount includes the one we kept; subtract 1 for "alternates"
365
+ const alternateCount = Math.max(0, rawAlternateCount - 1);
366
+
367
+ // Reconstruct path from target back to source
368
+ const pathIds = [targetNode.id];
369
+ let cur = targetNode.id;
370
+ while (cur !== sourceNode.id) {
371
+ const p = parent.get(cur);
372
+ pathIds.push(p.parentId);
373
+ cur = p.parentId;
374
+ }
375
+ pathIds.reverse();
376
+
377
+ const resultPath = reconstructPath(db, pathIds, parent);
378
+
379
+ return {
380
+ from,
381
+ to,
382
+ fromCandidates,
383
+ toCandidates,
384
+ found: true,
385
+ hops: foundDepth,
386
+ path: resultPath,
387
+ alternateCount,
388
+ edgeKinds,
389
+ reverse,
390
+ maxDepth,
391
+ };
392
+ } finally {
393
+ db.close();
394
+ }
395
+ }
@@ -5,10 +5,15 @@ import {
5
5
  findFileNodes,
6
6
  findNodesByFile,
7
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';
8
+ } from '../../db/index.js';
9
+ import { debug } from '../../infrastructure/logger.js';
10
+ import { isTestFile } from '../../infrastructure/test-filter.js';
11
+ import {
12
+ createFileLinesReader,
13
+ extractSignature,
14
+ extractSummary,
15
+ } from '../../shared/file-utils.js';
16
+ import { paginateResult } from '../../shared/paginate.js';
12
17
 
13
18
  export function exportsData(file, customDbPath, opts = {}) {
14
19
  const db = openReadonlyOrFail(customDbPath);
@@ -56,8 +61,8 @@ function exportsFileImpl(db, target, noTests, getFileLines, unused) {
56
61
  try {
57
62
  db.prepare('SELECT exported FROM nodes LIMIT 0').raw();
58
63
  hasExportedCol = true;
59
- } catch {
60
- /* old DB without exported column */
64
+ } catch (e) {
65
+ debug(`exported column not available, using fallback: ${e.message}`);
61
66
  }
62
67
 
63
68
  return fileNodes.map((fn) => {