@optave/codegraph 3.1.3 → 3.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +38 -84
  2. package/package.json +13 -8
  3. package/src/ast-analysis/engine.js +32 -12
  4. package/src/ast-analysis/shared.js +6 -5
  5. package/src/cli/commands/ast.js +22 -0
  6. package/src/cli/commands/audit.js +45 -0
  7. package/src/cli/commands/batch.js +68 -0
  8. package/src/cli/commands/branch-compare.js +21 -0
  9. package/src/cli/commands/build.js +26 -0
  10. package/src/cli/commands/cfg.js +26 -0
  11. package/src/cli/commands/check.js +74 -0
  12. package/src/cli/commands/children.js +28 -0
  13. package/src/cli/commands/co-change.js +67 -0
  14. package/src/cli/commands/communities.js +19 -0
  15. package/src/cli/commands/complexity.js +46 -0
  16. package/src/cli/commands/context.js +30 -0
  17. package/src/cli/commands/cycles.js +32 -0
  18. package/src/cli/commands/dataflow.js +28 -0
  19. package/src/cli/commands/deps.js +12 -0
  20. package/src/cli/commands/diff-impact.js +26 -0
  21. package/src/cli/commands/embed.js +30 -0
  22. package/src/cli/commands/export.js +78 -0
  23. package/src/cli/commands/exports.js +14 -0
  24. package/src/cli/commands/flow.js +32 -0
  25. package/src/cli/commands/fn-impact.js +26 -0
  26. package/src/cli/commands/impact.js +12 -0
  27. package/src/cli/commands/info.js +76 -0
  28. package/src/cli/commands/map.js +19 -0
  29. package/src/cli/commands/mcp.js +18 -0
  30. package/src/cli/commands/models.js +19 -0
  31. package/src/cli/commands/owners.js +25 -0
  32. package/src/cli/commands/path.js +36 -0
  33. package/src/cli/commands/plot.js +89 -0
  34. package/src/cli/commands/query.js +45 -0
  35. package/src/cli/commands/registry.js +100 -0
  36. package/src/cli/commands/roles.js +30 -0
  37. package/src/cli/commands/search.js +42 -0
  38. package/src/cli/commands/sequence.js +28 -0
  39. package/src/cli/commands/snapshot.js +66 -0
  40. package/src/cli/commands/stats.js +15 -0
  41. package/src/cli/commands/structure.js +33 -0
  42. package/src/cli/commands/triage.js +78 -0
  43. package/src/cli/commands/watch.js +12 -0
  44. package/src/cli/commands/where.js +20 -0
  45. package/src/cli/index.js +124 -0
  46. package/src/cli/shared/open-graph.js +13 -0
  47. package/src/cli/shared/options.js +59 -0
  48. package/src/cli/shared/output.js +1 -0
  49. package/src/cli.js +11 -1522
  50. package/src/db/connection.js +130 -7
  51. package/src/{db.js → db/index.js} +17 -5
  52. package/src/db/migrations.js +42 -1
  53. package/src/db/query-builder.js +20 -12
  54. package/src/db/repository/base.js +201 -0
  55. package/src/db/repository/graph-read.js +7 -4
  56. package/src/db/repository/in-memory-repository.js +575 -0
  57. package/src/db/repository/index.js +5 -1
  58. package/src/db/repository/nodes.js +60 -6
  59. package/src/db/repository/sqlite-repository.js +219 -0
  60. package/src/domain/analysis/context.js +408 -0
  61. package/src/domain/analysis/dependencies.js +341 -0
  62. package/src/domain/analysis/exports.js +134 -0
  63. package/src/domain/analysis/impact.js +466 -0
  64. package/src/domain/analysis/module-map.js +322 -0
  65. package/src/domain/analysis/roles.js +45 -0
  66. package/src/domain/analysis/symbol-lookup.js +238 -0
  67. package/src/domain/graph/builder/context.js +85 -0
  68. package/src/domain/graph/builder/helpers.js +218 -0
  69. package/src/domain/graph/builder/incremental.js +178 -0
  70. package/src/domain/graph/builder/pipeline.js +130 -0
  71. package/src/domain/graph/builder/stages/build-edges.js +297 -0
  72. package/src/domain/graph/builder/stages/build-structure.js +113 -0
  73. package/src/domain/graph/builder/stages/collect-files.js +44 -0
  74. package/src/domain/graph/builder/stages/detect-changes.js +413 -0
  75. package/src/domain/graph/builder/stages/finalize.js +139 -0
  76. package/src/domain/graph/builder/stages/insert-nodes.js +195 -0
  77. package/src/domain/graph/builder/stages/parse-files.js +28 -0
  78. package/src/domain/graph/builder/stages/resolve-imports.js +143 -0
  79. package/src/domain/graph/builder/stages/run-analyses.js +44 -0
  80. package/src/domain/graph/builder.js +11 -0
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/domain/graph/cycles.js +82 -0
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +3 -3
  85. package/src/{watcher.js → domain/graph/watcher.js} +10 -150
  86. package/src/{parser.js → domain/parser.js} +5 -5
  87. package/src/domain/queries.js +48 -0
  88. package/src/domain/search/generator.js +163 -0
  89. package/src/domain/search/index.js +13 -0
  90. package/src/domain/search/models.js +218 -0
  91. package/src/domain/search/search/cli-formatter.js +151 -0
  92. package/src/domain/search/search/filters.js +46 -0
  93. package/src/domain/search/search/hybrid.js +121 -0
  94. package/src/domain/search/search/keyword.js +68 -0
  95. package/src/domain/search/search/prepare.js +66 -0
  96. package/src/domain/search/search/semantic.js +145 -0
  97. package/src/domain/search/stores/fts5.js +27 -0
  98. package/src/domain/search/stores/sqlite-blob.js +24 -0
  99. package/src/domain/search/strategies/source.js +14 -0
  100. package/src/domain/search/strategies/structured.js +43 -0
  101. package/src/domain/search/strategies/text-utils.js +43 -0
  102. package/src/extractors/csharp.js +10 -2
  103. package/src/extractors/go.js +3 -1
  104. package/src/extractors/helpers.js +71 -0
  105. package/src/extractors/java.js +9 -2
  106. package/src/extractors/javascript.js +39 -2
  107. package/src/extractors/php.js +3 -1
  108. package/src/extractors/python.js +14 -3
  109. package/src/extractors/rust.js +3 -1
  110. package/src/{ast.js → features/ast.js} +8 -8
  111. package/src/{audit.js → features/audit.js} +16 -44
  112. package/src/{batch.js → features/batch.js} +6 -5
  113. package/src/{boundaries.js → features/boundaries.js} +2 -2
  114. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  115. package/src/{cfg.js → features/cfg.js} +11 -12
  116. package/src/{check.js → features/check.js} +13 -30
  117. package/src/{cochange.js → features/cochange.js} +5 -5
  118. package/src/{communities.js → features/communities.js} +18 -90
  119. package/src/{complexity.js → features/complexity.js} +13 -13
  120. package/src/{dataflow.js → features/dataflow.js} +12 -13
  121. package/src/features/export.js +378 -0
  122. package/src/{flow.js → features/flow.js} +4 -4
  123. package/src/features/graph-enrichment.js +327 -0
  124. package/src/{manifesto.js → features/manifesto.js} +6 -6
  125. package/src/{owners.js → features/owners.js} +2 -2
  126. package/src/{sequence.js → features/sequence.js} +16 -52
  127. package/src/{snapshot.js → features/snapshot.js} +8 -7
  128. package/src/{structure.js → features/structure.js} +20 -45
  129. package/src/{triage.js → features/triage.js} +27 -79
  130. package/src/graph/algorithms/bfs.js +49 -0
  131. package/src/graph/algorithms/centrality.js +16 -0
  132. package/src/graph/algorithms/index.js +5 -0
  133. package/src/graph/algorithms/louvain.js +26 -0
  134. package/src/graph/algorithms/shortest-path.js +41 -0
  135. package/src/graph/algorithms/tarjan.js +49 -0
  136. package/src/graph/builders/dependency.js +110 -0
  137. package/src/graph/builders/index.js +3 -0
  138. package/src/graph/builders/structure.js +40 -0
  139. package/src/graph/builders/temporal.js +33 -0
  140. package/src/graph/classifiers/index.js +2 -0
  141. package/src/graph/classifiers/risk.js +85 -0
  142. package/src/graph/classifiers/roles.js +64 -0
  143. package/src/graph/index.js +13 -0
  144. package/src/graph/model.js +230 -0
  145. package/src/index.cjs +16 -0
  146. package/src/index.js +42 -219
  147. package/src/{native.js → infrastructure/native.js} +3 -1
  148. package/src/infrastructure/result-formatter.js +2 -21
  149. package/src/mcp/index.js +2 -0
  150. package/src/mcp/middleware.js +26 -0
  151. package/src/mcp/server.js +128 -0
  152. package/src/{mcp.js → mcp/tool-registry.js} +6 -675
  153. package/src/mcp/tools/ast-query.js +14 -0
  154. package/src/mcp/tools/audit.js +21 -0
  155. package/src/mcp/tools/batch-query.js +11 -0
  156. package/src/mcp/tools/branch-compare.js +12 -0
  157. package/src/mcp/tools/cfg.js +21 -0
  158. package/src/mcp/tools/check.js +43 -0
  159. package/src/mcp/tools/co-changes.js +20 -0
  160. package/src/mcp/tools/code-owners.js +12 -0
  161. package/src/mcp/tools/communities.js +15 -0
  162. package/src/mcp/tools/complexity.js +18 -0
  163. package/src/mcp/tools/context.js +17 -0
  164. package/src/mcp/tools/dataflow.js +26 -0
  165. package/src/mcp/tools/diff-impact.js +24 -0
  166. package/src/mcp/tools/execution-flow.js +26 -0
  167. package/src/mcp/tools/export-graph.js +57 -0
  168. package/src/mcp/tools/file-deps.js +12 -0
  169. package/src/mcp/tools/file-exports.js +13 -0
  170. package/src/mcp/tools/find-cycles.js +15 -0
  171. package/src/mcp/tools/fn-impact.js +15 -0
  172. package/src/mcp/tools/impact-analysis.js +12 -0
  173. package/src/mcp/tools/index.js +71 -0
  174. package/src/mcp/tools/list-functions.js +14 -0
  175. package/src/mcp/tools/list-repos.js +11 -0
  176. package/src/mcp/tools/module-map.js +6 -0
  177. package/src/mcp/tools/node-roles.js +14 -0
  178. package/src/mcp/tools/path.js +12 -0
  179. package/src/mcp/tools/query.js +30 -0
  180. package/src/mcp/tools/semantic-search.js +65 -0
  181. package/src/mcp/tools/sequence.js +17 -0
  182. package/src/mcp/tools/structure.js +15 -0
  183. package/src/mcp/tools/symbol-children.js +14 -0
  184. package/src/mcp/tools/triage.js +35 -0
  185. package/src/mcp/tools/where.js +13 -0
  186. package/src/{commands → presentation}/audit.js +2 -2
  187. package/src/{commands → presentation}/batch.js +1 -1
  188. package/src/{commands → presentation}/branch-compare.js +2 -2
  189. package/src/{commands → presentation}/cfg.js +1 -1
  190. package/src/{commands → presentation}/check.js +6 -6
  191. package/src/presentation/colors.js +44 -0
  192. package/src/{commands → presentation}/communities.js +1 -1
  193. package/src/{commands → presentation}/complexity.js +1 -1
  194. package/src/{commands → presentation}/dataflow.js +1 -1
  195. package/src/presentation/export.js +444 -0
  196. package/src/{commands → presentation}/flow.js +2 -2
  197. package/src/{commands → presentation}/manifesto.js +4 -4
  198. package/src/{commands → presentation}/owners.js +1 -1
  199. package/src/presentation/queries-cli/exports.js +46 -0
  200. package/src/presentation/queries-cli/impact.js +198 -0
  201. package/src/presentation/queries-cli/index.js +5 -0
  202. package/src/presentation/queries-cli/inspect.js +334 -0
  203. package/src/presentation/queries-cli/overview.js +197 -0
  204. package/src/presentation/queries-cli/path.js +58 -0
  205. package/src/presentation/queries-cli.js +27 -0
  206. package/src/{commands → presentation}/query.js +1 -1
  207. package/src/presentation/result-formatter.js +144 -0
  208. package/src/presentation/sequence-renderer.js +43 -0
  209. package/src/{commands → presentation}/sequence.js +2 -2
  210. package/src/{commands → presentation}/structure.js +2 -2
  211. package/src/presentation/table.js +47 -0
  212. package/src/{commands → presentation}/triage.js +1 -1
  213. package/src/{viewer.js → presentation/viewer.js} +68 -382
  214. package/src/{constants.js → shared/constants.js} +1 -1
  215. package/src/shared/errors.js +78 -0
  216. package/src/shared/file-utils.js +153 -0
  217. package/src/shared/generators.js +125 -0
  218. package/src/shared/hierarchy.js +27 -0
  219. package/src/shared/normalize.js +59 -0
  220. package/src/builder.js +0 -1486
  221. package/src/cycles.js +0 -137
  222. package/src/embedder.js +0 -1097
  223. package/src/export.js +0 -681
  224. package/src/queries-cli.js +0 -866
  225. package/src/queries.js +0 -2289
  226. /package/src/{config.js → infrastructure/config.js} +0 -0
  227. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  228. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  229. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  230. /package/src/{commands → presentation}/cochange.js +0 -0
  231. /package/src/{kinds.js → shared/kinds.js} +0 -0
  232. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -0,0 +1,341 @@
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
+ 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,134 @@
1
+ import path from 'node:path';
2
+ import {
3
+ findCrossFileCallTargets,
4
+ findDbPath,
5
+ findFileNodes,
6
+ findNodesByFile,
7
+ openReadonlyOrFail,
8
+ } from '../../db/index.js';
9
+ import { isTestFile } from '../../infrastructure/test-filter.js';
10
+ import {
11
+ createFileLinesReader,
12
+ extractSignature,
13
+ extractSummary,
14
+ } from '../../shared/file-utils.js';
15
+ import { paginateResult } from '../../shared/paginate.js';
16
+
17
+ export function exportsData(file, customDbPath, opts = {}) {
18
+ const db = openReadonlyOrFail(customDbPath);
19
+ try {
20
+ const noTests = opts.noTests || false;
21
+
22
+ const dbFilePath = findDbPath(customDbPath);
23
+ const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
24
+
25
+ const getFileLines = createFileLinesReader(repoRoot);
26
+
27
+ const unused = opts.unused || false;
28
+ const fileResults = exportsFileImpl(db, file, noTests, getFileLines, unused);
29
+
30
+ if (fileResults.length === 0) {
31
+ return paginateResult(
32
+ { file, results: [], reexports: [], totalExported: 0, totalInternal: 0, totalUnused: 0 },
33
+ 'results',
34
+ { limit: opts.limit, offset: opts.offset },
35
+ );
36
+ }
37
+
38
+ // For single-file match return flat; for multi-match return first (like explainData)
39
+ const first = fileResults[0];
40
+ const base = {
41
+ file: first.file,
42
+ results: first.results,
43
+ reexports: first.reexports,
44
+ totalExported: first.totalExported,
45
+ totalInternal: first.totalInternal,
46
+ totalUnused: first.totalUnused,
47
+ };
48
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
49
+ } finally {
50
+ db.close();
51
+ }
52
+ }
53
+
54
+ function exportsFileImpl(db, target, noTests, getFileLines, unused) {
55
+ const fileNodes = findFileNodes(db, `%${target}%`);
56
+ if (fileNodes.length === 0) return [];
57
+
58
+ // Detect whether exported column exists
59
+ let hasExportedCol = false;
60
+ try {
61
+ db.prepare('SELECT exported FROM nodes LIMIT 0').raw();
62
+ hasExportedCol = true;
63
+ } catch {
64
+ /* old DB without exported column */
65
+ }
66
+
67
+ return fileNodes.map((fn) => {
68
+ const symbols = findNodesByFile(db, fn.file);
69
+
70
+ let exported;
71
+ if (hasExportedCol) {
72
+ // Use the exported column populated during build
73
+ exported = db
74
+ .prepare(
75
+ "SELECT * FROM nodes WHERE file = ? AND kind != 'file' AND exported = 1 ORDER BY line",
76
+ )
77
+ .all(fn.file);
78
+ } else {
79
+ // Fallback: symbols that have incoming calls from other files
80
+ const exportedIds = findCrossFileCallTargets(db, fn.file);
81
+ exported = symbols.filter((s) => exportedIds.has(s.id));
82
+ }
83
+ const internalCount = symbols.length - exported.length;
84
+
85
+ const results = exported.map((s) => {
86
+ const fileLines = getFileLines(fn.file);
87
+
88
+ let consumers = db
89
+ .prepare(
90
+ `SELECT n.name, n.file, n.line FROM edges e JOIN nodes n ON e.source_id = n.id
91
+ WHERE e.target_id = ? AND e.kind = 'calls'`,
92
+ )
93
+ .all(s.id);
94
+ if (noTests) consumers = consumers.filter((c) => !isTestFile(c.file));
95
+
96
+ return {
97
+ name: s.name,
98
+ kind: s.kind,
99
+ line: s.line,
100
+ endLine: s.end_line ?? null,
101
+ role: s.role || null,
102
+ signature: fileLines ? extractSignature(fileLines, s.line) : null,
103
+ summary: fileLines ? extractSummary(fileLines, s.line) : null,
104
+ consumers: consumers.map((c) => ({ name: c.name, file: c.file, line: c.line })),
105
+ consumerCount: consumers.length,
106
+ };
107
+ });
108
+
109
+ const totalUnused = results.filter((r) => r.consumerCount === 0).length;
110
+
111
+ // Files that re-export this file (barrel → this file)
112
+ const reexports = db
113
+ .prepare(
114
+ `SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
115
+ WHERE e.target_id = ? AND e.kind = 'reexports'`,
116
+ )
117
+ .all(fn.id)
118
+ .map((r) => ({ file: r.file }));
119
+
120
+ let filteredResults = results;
121
+ if (unused) {
122
+ filteredResults = results.filter((r) => r.consumerCount === 0);
123
+ }
124
+
125
+ return {
126
+ file: fn.file,
127
+ results: filteredResults,
128
+ reexports,
129
+ totalExported: exported.length,
130
+ totalInternal: internalCount,
131
+ totalUnused,
132
+ };
133
+ });
134
+ }