@mka-rainmaker/ama 0.1.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 (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/dist/analyzers/baseline/analyzer.d.ts +47 -0
  4. package/dist/analyzers/baseline/analyzer.d.ts.map +1 -0
  5. package/dist/analyzers/baseline/analyzer.js +84 -0
  6. package/dist/analyzers/baseline/analyzer.js.map +1 -0
  7. package/dist/analyzers/baseline/c.d.ts +12 -0
  8. package/dist/analyzers/baseline/c.d.ts.map +1 -0
  9. package/dist/analyzers/baseline/c.js +56 -0
  10. package/dist/analyzers/baseline/c.js.map +1 -0
  11. package/dist/analyzers/baseline/config.d.ts +21 -0
  12. package/dist/analyzers/baseline/config.d.ts.map +1 -0
  13. package/dist/analyzers/baseline/config.js +32 -0
  14. package/dist/analyzers/baseline/config.js.map +1 -0
  15. package/dist/analyzers/baseline/csharp.d.ts +9 -0
  16. package/dist/analyzers/baseline/csharp.d.ts.map +1 -0
  17. package/dist/analyzers/baseline/csharp.js +107 -0
  18. package/dist/analyzers/baseline/csharp.js.map +1 -0
  19. package/dist/analyzers/baseline/go.d.ts +11 -0
  20. package/dist/analyzers/baseline/go.d.ts.map +1 -0
  21. package/dist/analyzers/baseline/go.js +66 -0
  22. package/dist/analyzers/baseline/go.js.map +1 -0
  23. package/dist/analyzers/baseline/java.d.ts +9 -0
  24. package/dist/analyzers/baseline/java.d.ts.map +1 -0
  25. package/dist/analyzers/baseline/java.js +50 -0
  26. package/dist/analyzers/baseline/java.js.map +1 -0
  27. package/dist/analyzers/baseline/javascript.d.ts +10 -0
  28. package/dist/analyzers/baseline/javascript.d.ts.map +1 -0
  29. package/dist/analyzers/baseline/javascript.js +55 -0
  30. package/dist/analyzers/baseline/javascript.js.map +1 -0
  31. package/dist/analyzers/baseline/kotlin.d.ts +11 -0
  32. package/dist/analyzers/baseline/kotlin.d.ts.map +1 -0
  33. package/dist/analyzers/baseline/kotlin.js +67 -0
  34. package/dist/analyzers/baseline/kotlin.js.map +1 -0
  35. package/dist/analyzers/baseline/paths.d.ts +6 -0
  36. package/dist/analyzers/baseline/paths.d.ts.map +1 -0
  37. package/dist/analyzers/baseline/paths.js +17 -0
  38. package/dist/analyzers/baseline/paths.js.map +1 -0
  39. package/dist/analyzers/baseline/php.d.ts +11 -0
  40. package/dist/analyzers/baseline/php.d.ts.map +1 -0
  41. package/dist/analyzers/baseline/php.js +76 -0
  42. package/dist/analyzers/baseline/php.js.map +1 -0
  43. package/dist/analyzers/baseline/python.d.ts +10 -0
  44. package/dist/analyzers/baseline/python.d.ts.map +1 -0
  45. package/dist/analyzers/baseline/python.js +63 -0
  46. package/dist/analyzers/baseline/python.js.map +1 -0
  47. package/dist/analyzers/baseline/rust.d.ts +10 -0
  48. package/dist/analyzers/baseline/rust.d.ts.map +1 -0
  49. package/dist/analyzers/baseline/rust.js +45 -0
  50. package/dist/analyzers/baseline/rust.js.map +1 -0
  51. package/dist/analyzers/baseline/swift.d.ts +11 -0
  52. package/dist/analyzers/baseline/swift.d.ts.map +1 -0
  53. package/dist/analyzers/baseline/swift.js +19 -0
  54. package/dist/analyzers/baseline/swift.js.map +1 -0
  55. package/dist/analyzers/baseline/treesitter.d.ts +11 -0
  56. package/dist/analyzers/baseline/treesitter.d.ts.map +1 -0
  57. package/dist/analyzers/baseline/treesitter.js +87 -0
  58. package/dist/analyzers/baseline/treesitter.js.map +1 -0
  59. package/dist/analyzers/baseline/walk.d.ts +26 -0
  60. package/dist/analyzers/baseline/walk.d.ts.map +1 -0
  61. package/dist/analyzers/baseline/walk.js +76 -0
  62. package/dist/analyzers/baseline/walk.js.map +1 -0
  63. package/dist/analyzers/registry.d.ts +19 -0
  64. package/dist/analyzers/registry.d.ts.map +1 -0
  65. package/dist/analyzers/registry.js +43 -0
  66. package/dist/analyzers/registry.js.map +1 -0
  67. package/dist/analyzers/sfc/analyzer.d.ts +17 -0
  68. package/dist/analyzers/sfc/analyzer.d.ts.map +1 -0
  69. package/dist/analyzers/sfc/analyzer.js +141 -0
  70. package/dist/analyzers/sfc/analyzer.js.map +1 -0
  71. package/dist/analyzers/sidecar/analyzer.d.ts +29 -0
  72. package/dist/analyzers/sidecar/analyzer.d.ts.map +1 -0
  73. package/dist/analyzers/sidecar/analyzer.js +114 -0
  74. package/dist/analyzers/sidecar/analyzer.js.map +1 -0
  75. package/dist/analyzers/sidecar/protocol.d.ts +508 -0
  76. package/dist/analyzers/sidecar/protocol.d.ts.map +1 -0
  77. package/dist/analyzers/sidecar/protocol.js +102 -0
  78. package/dist/analyzers/sidecar/protocol.js.map +1 -0
  79. package/dist/analyzers/types.d.ts +46 -0
  80. package/dist/analyzers/types.d.ts.map +1 -0
  81. package/dist/analyzers/types.js +2 -0
  82. package/dist/analyzers/types.js.map +1 -0
  83. package/dist/analyzers/typescript/analyzer.d.ts +126 -0
  84. package/dist/analyzers/typescript/analyzer.d.ts.map +1 -0
  85. package/dist/analyzers/typescript/analyzer.js +1600 -0
  86. package/dist/analyzers/typescript/analyzer.js.map +1 -0
  87. package/dist/cli/commands/cycles.d.ts +6 -0
  88. package/dist/cli/commands/cycles.d.ts.map +1 -0
  89. package/dist/cli/commands/cycles.js +27 -0
  90. package/dist/cli/commands/cycles.js.map +1 -0
  91. package/dist/cli/commands/files.d.ts +6 -0
  92. package/dist/cli/commands/files.d.ts.map +1 -0
  93. package/dist/cli/commands/files.js +33 -0
  94. package/dist/cli/commands/files.js.map +1 -0
  95. package/dist/cli/commands/impact.d.ts +18 -0
  96. package/dist/cli/commands/impact.d.ts.map +1 -0
  97. package/dist/cli/commands/impact.js +113 -0
  98. package/dist/cli/commands/impact.js.map +1 -0
  99. package/dist/cli/commands/lifecycle.d.ts +5 -0
  100. package/dist/cli/commands/lifecycle.d.ts.map +1 -0
  101. package/dist/cli/commands/lifecycle.js +83 -0
  102. package/dist/cli/commands/lifecycle.js.map +1 -0
  103. package/dist/cli/commands/query.d.ts +31 -0
  104. package/dist/cli/commands/query.d.ts.map +1 -0
  105. package/dist/cli/commands/query.js +187 -0
  106. package/dist/cli/commands/query.js.map +1 -0
  107. package/dist/cli/commands/search.d.ts +21 -0
  108. package/dist/cli/commands/search.d.ts.map +1 -0
  109. package/dist/cli/commands/search.js +160 -0
  110. package/dist/cli/commands/search.js.map +1 -0
  111. package/dist/cli/commands/status.d.ts +6 -0
  112. package/dist/cli/commands/status.d.ts.map +1 -0
  113. package/dist/cli/commands/status.js +63 -0
  114. package/dist/cli/commands/status.js.map +1 -0
  115. package/dist/cli/commands/sync.d.ts +6 -0
  116. package/dist/cli/commands/sync.d.ts.map +1 -0
  117. package/dist/cli/commands/sync.js +57 -0
  118. package/dist/cli/commands/sync.js.map +1 -0
  119. package/dist/cli/emit.d.ts +9 -0
  120. package/dist/cli/emit.d.ts.map +1 -0
  121. package/dist/cli/emit.js +10 -0
  122. package/dist/cli/emit.js.map +1 -0
  123. package/dist/cli/index.d.ts +37 -0
  124. package/dist/cli/index.d.ts.map +1 -0
  125. package/dist/cli/index.js +128 -0
  126. package/dist/cli/index.js.map +1 -0
  127. package/dist/cli/paths.d.ts +7 -0
  128. package/dist/cli/paths.d.ts.map +1 -0
  129. package/dist/cli/paths.js +10 -0
  130. package/dist/cli/paths.js.map +1 -0
  131. package/dist/cli/query-runner.d.ts +13 -0
  132. package/dist/cli/query-runner.d.ts.map +1 -0
  133. package/dist/cli/query-runner.js +33 -0
  134. package/dist/cli/query-runner.js.map +1 -0
  135. package/dist/graph/dispatch.d.ts +17 -0
  136. package/dist/graph/dispatch.d.ts.map +1 -0
  137. package/dist/graph/dispatch.js +82 -0
  138. package/dist/graph/dispatch.js.map +1 -0
  139. package/dist/graph/id.d.ts +19 -0
  140. package/dist/graph/id.d.ts.map +1 -0
  141. package/dist/graph/id.js +17 -0
  142. package/dist/graph/id.js.map +1 -0
  143. package/dist/graph/index.d.ts +6 -0
  144. package/dist/graph/index.d.ts.map +1 -0
  145. package/dist/graph/index.js +4 -0
  146. package/dist/graph/index.js.map +1 -0
  147. package/dist/graph/types.d.ts +71 -0
  148. package/dist/graph/types.d.ts.map +1 -0
  149. package/dist/graph/types.js +52 -0
  150. package/dist/graph/types.js.map +1 -0
  151. package/dist/indexer/debouncer.d.ts +32 -0
  152. package/dist/indexer/debouncer.d.ts.map +1 -0
  153. package/dist/indexer/debouncer.js +81 -0
  154. package/dist/indexer/debouncer.js.map +1 -0
  155. package/dist/indexer/ignore.d.ts +55 -0
  156. package/dist/indexer/ignore.d.ts.map +1 -0
  157. package/dist/indexer/ignore.js +170 -0
  158. package/dist/indexer/ignore.js.map +1 -0
  159. package/dist/indexer/indexer.d.ts +112 -0
  160. package/dist/indexer/indexer.d.ts.map +1 -0
  161. package/dist/indexer/indexer.js +392 -0
  162. package/dist/indexer/indexer.js.map +1 -0
  163. package/dist/indexer/watcher.d.ts +50 -0
  164. package/dist/indexer/watcher.d.ts.map +1 -0
  165. package/dist/indexer/watcher.js +86 -0
  166. package/dist/indexer/watcher.js.map +1 -0
  167. package/dist/mcp/build-info.d.ts +16 -0
  168. package/dist/mcp/build-info.d.ts.map +1 -0
  169. package/dist/mcp/build-info.js +54 -0
  170. package/dist/mcp/build-info.js.map +1 -0
  171. package/dist/mcp/http.d.ts +18 -0
  172. package/dist/mcp/http.d.ts.map +1 -0
  173. package/dist/mcp/http.js +145 -0
  174. package/dist/mcp/http.js.map +1 -0
  175. package/dist/mcp/server.d.ts +22 -0
  176. package/dist/mcp/server.d.ts.map +1 -0
  177. package/dist/mcp/server.js +401 -0
  178. package/dist/mcp/server.js.map +1 -0
  179. package/dist/mcp/session.d.ts +155 -0
  180. package/dist/mcp/session.d.ts.map +1 -0
  181. package/dist/mcp/session.js +319 -0
  182. package/dist/mcp/session.js.map +1 -0
  183. package/dist/query/service.d.ts +329 -0
  184. package/dist/query/service.d.ts.map +1 -0
  185. package/dist/query/service.js +959 -0
  186. package/dist/query/service.js.map +1 -0
  187. package/dist/runtime/entrypoint.d.ts +11 -0
  188. package/dist/runtime/entrypoint.d.ts.map +1 -0
  189. package/dist/runtime/entrypoint.js +22 -0
  190. package/dist/runtime/entrypoint.js.map +1 -0
  191. package/dist/runtime/quiet-sqlite-warning.d.ts +14 -0
  192. package/dist/runtime/quiet-sqlite-warning.d.ts.map +1 -0
  193. package/dist/runtime/quiet-sqlite-warning.js +26 -0
  194. package/dist/runtime/quiet-sqlite-warning.js.map +1 -0
  195. package/dist/runtime/wasm-tier.d.ts +2 -0
  196. package/dist/runtime/wasm-tier.d.ts.map +1 -0
  197. package/dist/runtime/wasm-tier.js +54 -0
  198. package/dist/runtime/wasm-tier.js.map +1 -0
  199. package/dist/store/memory.d.ts +54 -0
  200. package/dist/store/memory.d.ts.map +1 -0
  201. package/dist/store/memory.js +210 -0
  202. package/dist/store/memory.js.map +1 -0
  203. package/dist/store/sqlite.d.ts +38 -0
  204. package/dist/store/sqlite.d.ts.map +1 -0
  205. package/dist/store/sqlite.js +298 -0
  206. package/dist/store/sqlite.js.map +1 -0
  207. package/dist/store/types.d.ts +76 -0
  208. package/dist/store/types.d.ts.map +1 -0
  209. package/dist/store/types.js +2 -0
  210. package/dist/store/types.js.map +1 -0
  211. package/package.json +59 -0
@@ -0,0 +1,1600 @@
1
+ import * as path from "node:path";
2
+ import ts from "typescript";
3
+ import { deriveDispatchEdges, fileId, symbolId } from "../../graph/index.js";
4
+ /**
5
+ * Log a per-file analyzer failure to stderr and carry on. The deep TypeScript
6
+ * analyzer shares one ts.Program across the whole batch for cross-file
7
+ * resolution, so it can't give each file its own program the way the baseline
8
+ * analyzer isolates per file (ama-eww); instead each per-file *pass* is wrapped so
9
+ * one pathological file degrades to a skipped file rather than throwing out of
10
+ * analyze() — which the indexer's per-analyzer catch would turn into every .ts
11
+ * file vanishing from the graph. (ama-bm2)
12
+ */
13
+ function reportFileFailure(rel, phase, err) {
14
+ console.error(`[ama] typescript analyzer failed on ${rel} (${phase}); skipping it. ` +
15
+ `${err instanceof Error ? err.message : String(err)}`);
16
+ }
17
+ /**
18
+ * Deep TypeScript analyzer built on the TypeScript Compiler API.
19
+ *
20
+ * Two passes over each source file:
21
+ * 1. Structural — emit nodes (File, Function, Class, Interface, Enum, TypeAlias,
22
+ * Method, Property — see `describe` for the full set) and `Defines` edges,
23
+ * recording each declaration's AST node so later references link back to ids.
24
+ * 2. Resolution — through the type checker, emit `Calls` edges (enclosing
25
+ * function/method → callee), `Inherits`/`Implements` edges (class → base
26
+ * class / interface), `UsesType` edges (enclosing symbol → each named type
27
+ * used in a parameter, return, or property annotation), and `Imports` edges
28
+ * (file → each symbol it imports or re-exports). References to symbols
29
+ * outside the analyzed set (library code) resolve to no node and are
30
+ * skipped, so the graph only asserts edges it can actually back.
31
+ */
32
+ export class TypeScriptAnalyzer {
33
+ language = "typescript";
34
+ tier = "deep";
35
+ extensions = [".ts", ".tsx", ".mts", ".cts"];
36
+ analyze(root, files) {
37
+ const relByAbs = new Map();
38
+ for (const rel of files)
39
+ relByAbs.set(path.resolve(root, rel), rel);
40
+ const program = ts.createProgram([...relByAbs.keys()], {
41
+ target: ts.ScriptTarget.ES2022,
42
+ module: ts.ModuleKind.ESNext,
43
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
44
+ allowJs: false,
45
+ noEmit: true,
46
+ skipLibCheck: true,
47
+ });
48
+ const nodes = [];
49
+ const edges = [];
50
+ const resolution = { callsTotal: 0, callsResolved: 0, unresolved: {} };
51
+ /** AST declaration node -> graph node id, so resolved calls find their target. */
52
+ const declToId = new Map();
53
+ for (const [abs, rel] of relByAbs) {
54
+ const sf = program.getSourceFile(abs);
55
+ if (!sf)
56
+ continue;
57
+ try {
58
+ this.walkFile(sf, rel, nodes, edges, declToId);
59
+ }
60
+ catch (err) {
61
+ reportFileFailure(rel, "structure", err);
62
+ }
63
+ }
64
+ const checker = program.getTypeChecker();
65
+ // Mount pre-pass (all files first): map each router declaration to the path
66
+ // prefix it's mounted at (app.use("/api", router)), so route detection can
67
+ // prepend it. Cross-file — the checker follows imported router symbols.
68
+ const mountPrefixes = new Map();
69
+ for (const [abs, rel] of relByAbs) {
70
+ const sf = program.getSourceFile(abs);
71
+ if (!sf)
72
+ continue;
73
+ try {
74
+ this.collectMounts(sf, checker, mountPrefixes);
75
+ }
76
+ catch (err) {
77
+ reportFileFailure(rel, "mounts", err);
78
+ }
79
+ }
80
+ for (const [abs, rel] of relByAbs) {
81
+ const sf = program.getSourceFile(abs);
82
+ if (!sf)
83
+ continue;
84
+ try {
85
+ // Routes first: it registers inline-handler arrows in declToId, so the
86
+ // following collectCalls attributes each handler's body to its node.
87
+ this.collectRoutes(sf, rel, declToId, checker, nodes, edges, root, mountPrefixes);
88
+ // File-based routes: the URL comes from the file path, not a call. (ama-rme.7, ama-w7g)
89
+ this.collectFileRoutes(sf, rel, declToId, checker, nodes, edges, root);
90
+ // Then callback-argument handlers (tap("name", () => …)) — same trick:
91
+ // register the arrow before collectCalls so its body attributes to it.
92
+ this.collectCallbackHandlers(sf, undefined, rel, sf, declToId, nodes, edges);
93
+ // Events after callback-handler synthesis so inline `.on("ch", () => …)`
94
+ // arrows are already handler nodes it can connect an emit to. (ama-hft.14)
95
+ this.collectEvents(sf, declToId, checker, edges, root);
96
+ // A call at module top-level (an entry block, a module-init side effect)
97
+ // attributes to the File node rather than being dropped for lack of an
98
+ // enclosing symbol, so find_callers surfaces module-level wiring as
99
+ // Defines/Imports edges already do. The File is the fallback only at true
100
+ // file scope (atFileScope), NOT inside a transparent callback — else a
101
+ // top-level `describe(() => it(() => expect()))` would make the file
102
+ // "call" it/expect. (ama-53q)
103
+ this.collectCalls(sf, undefined, declToId, checker, edges, root, resolution, fileId(rel), true);
104
+ this.collectVarReferences(sf, undefined, declToId, checker, edges, root);
105
+ this.collectHeritage(sf, declToId, checker, edges, root);
106
+ this.collectTypeUsages(sf, undefined, declToId, checker, edges, root);
107
+ this.collectImports(sf, fileId(rel), declToId, checker, edges, root);
108
+ }
109
+ catch (err) {
110
+ reportFileFailure(rel, "resolution", err);
111
+ }
112
+ }
113
+ // Dispatch fan-out (interface/override) is a whole-graph derivation now shared
114
+ // with the indexer's incremental re-derivation — see graph/dispatch.ts and
115
+ // ama-tr1. Over the full batch (a full index) it's exact; the derived edges are
116
+ // tagged provenance:"dispatch" so the indexer can re-derive them on reindex.
117
+ edges.push(...deriveDispatchEdges(nodes, edges));
118
+ return { nodes, edges: accumulateCallSites(edges), resolution };
119
+ }
120
+ walkFile(sf, rel, nodes, edges, declToId) {
121
+ const id = fileId(rel);
122
+ nodes.push({
123
+ id,
124
+ kind: "File",
125
+ name: path.basename(rel),
126
+ file: rel,
127
+ qualifiedName: "",
128
+ tier: "deep",
129
+ // The whole file, line 1 to EOF — `rangeOf` would skip leading comments
130
+ // (getStart trims trivia), truncating a File snippet's header.
131
+ range: { ...rangeOf(sf, sf), startLine: 1 },
132
+ });
133
+ // Register the file itself so module references (namespace imports,
134
+ // star re-exports) — which alias to the SourceFile — resolve to this node.
135
+ declToId.set(sf, id);
136
+ sf.forEachChild((child) => this.visit(child, sf, rel, id, "", nodes, edges, declToId));
137
+ }
138
+ visit(node, sf, rel, containerId, prefix, nodes, edges, declToId) {
139
+ // A `const f = () => …` / `= function …` is a VariableStatement wrapping the
140
+ // declaration we actually emit a node for; recurse into its declarations.
141
+ if (ts.isVariableStatement(node)) {
142
+ for (const declaration of node.declarationList.declarations) {
143
+ this.visit(declaration, sf, rel, containerId, prefix, nodes, edges, declToId);
144
+ }
145
+ return;
146
+ }
147
+ const decl = describe(node);
148
+ if (!decl) {
149
+ // `const X = { m() {…}, p: () => {…} }`: X itself isn't a node (there's no
150
+ // object kind), but recurse into its members so function-valued ones become
151
+ // `X.m` Method nodes. Otherwise logic that lives in object literals — every
152
+ // CLI command's `run`, dispatch tables, config handlers — is invisible to
153
+ // the call graph (its calls attribute to nothing).
154
+ if (ts.isVariableDeclaration(node) &&
155
+ node.initializer !== undefined &&
156
+ ts.isObjectLiteralExpression(node.initializer) &&
157
+ ts.isIdentifier(node.name)) {
158
+ const objPrefix = prefix ? `${prefix}.${node.name.text}` : node.name.text;
159
+ // A *typed* object const (`const spec: LanguageSpec = {…}`) is a named,
160
+ // typed symbol worth a node — so it's queryable and its `: T` annotation
161
+ // has an owner to hang the UsesType edge on (collectTypeUsages keys off
162
+ // declToId). An *untyped* object literal stays node-less, so we don't emit
163
+ // a node per anonymous config/dispatch table. (ama-g73)
164
+ if (node.type) {
165
+ const id = symbolId({ file: rel, qualifiedName: objPrefix });
166
+ nodes.push({
167
+ id,
168
+ kind: "Variable",
169
+ name: node.name.text,
170
+ file: rel,
171
+ qualifiedName: objPrefix,
172
+ tier: "deep",
173
+ range: rangeOf(node, sf),
174
+ });
175
+ edges.push({ from: containerId, to: id, kind: "Defines" });
176
+ declToId.set(node, id);
177
+ }
178
+ for (const member of node.initializer.properties) {
179
+ this.visit(member, sf, rel, containerId, objPrefix, nodes, edges, declToId);
180
+ }
181
+ }
182
+ return;
183
+ }
184
+ const qualifiedName = prefix ? `${prefix}.${decl.name}` : decl.name;
185
+ const id = symbolId({ file: rel, qualifiedName });
186
+ nodes.push({
187
+ id,
188
+ kind: decl.kind,
189
+ name: decl.name,
190
+ file: rel,
191
+ qualifiedName,
192
+ tier: "deep",
193
+ range: rangeOf(node, sf),
194
+ });
195
+ edges.push({ from: containerId, to: id, kind: "Defines" });
196
+ declToId.set(node, id);
197
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
198
+ for (const member of node.members) {
199
+ this.visit(member, sf, rel, id, qualifiedName, nodes, edges, declToId);
200
+ }
201
+ }
202
+ // A namespace/module is a container: recurse into its body so members nest
203
+ // (`Geometry.area`) and don't collide with same-named top-level symbols. The
204
+ // body is a block of statements, or a nested namespace for `namespace A.B`. (ama-hft.13)
205
+ if (ts.isModuleDeclaration(node) && node.body) {
206
+ if (ts.isModuleBlock(node.body)) {
207
+ for (const stmt of node.body.statements) {
208
+ this.visit(stmt, sf, rel, id, qualifiedName, nodes, edges, declToId);
209
+ }
210
+ }
211
+ else if (ts.isModuleDeclaration(node.body)) {
212
+ this.visit(node.body, sf, rel, id, qualifiedName, nodes, edges, declToId);
213
+ }
214
+ }
215
+ // A constructor's parameter properties (`constructor(private readonly x: T)`)
216
+ // are real class members, but they're declared on the parameter rather than in
217
+ // the class body — emit a Property for each, under the class (`containerId`),
218
+ // so it's queryable like a declared field. The declToId entry lets references
219
+ // and the parameter's type annotation resolve to it. (ama-259)
220
+ if (ts.isConstructorDeclaration(node)) {
221
+ for (const param of node.parameters) {
222
+ if (!ts.isIdentifier(param.name) || !isParameterProperty(param))
223
+ continue;
224
+ const propName = param.name.text;
225
+ const propQn = prefix ? `${prefix}.${propName}` : propName;
226
+ const propId = symbolId({ file: rel, qualifiedName: propQn });
227
+ nodes.push({
228
+ id: propId,
229
+ kind: "Property",
230
+ name: propName,
231
+ file: rel,
232
+ qualifiedName: propQn,
233
+ tier: "deep",
234
+ range: rangeOf(param, sf),
235
+ });
236
+ edges.push({ from: containerId, to: propId, kind: "Defines" });
237
+ declToId.set(param, propId);
238
+ }
239
+ }
240
+ }
241
+ /** Walk a subtree, attributing each call to the nearest enclosing symbol — or,
242
+ * for a call at true module scope, to the File node so module-level wiring is
243
+ * queryable. `atFileScope` is true until the walk enters any function body.
244
+ * (ama-53q) */
245
+ collectCalls(node, enclosingId, declToId, checker, edges, root, counts, fileNodeId, atFileScope) {
246
+ node.forEachChild((child) => {
247
+ // Decorators are usage edges, not calls (collectTypeUsages emits a UsesType
248
+ // edge for each). Skip the whole decorator so `@log()` doesn't masquerade as
249
+ // the decorated symbol calling `log`, and so calls inside decorator arguments
250
+ // (decorator config, not the symbol's behaviour) aren't attributed to it.
251
+ if (ts.isDecorator(child))
252
+ return;
253
+ // The call's owner: the nearest enclosing symbol, or the File when the call
254
+ // sits at true module scope. A call inside a transparent (unregistered)
255
+ // callback has neither — it's dropped, so a top-level `describe(() => it(…))`
256
+ // doesn't make the file appear to call `it`/`expect`. (ama-53q)
257
+ const from = enclosingId ?? (atFileScope ? fileNodeId : undefined);
258
+ // A `new Foo()` is a construction call site, resolved the same way as a
259
+ // plain call (to Foo's class node), so `find_callers` sees constructions.
260
+ if ((ts.isCallExpression(child) || ts.isNewExpression(child)) && from) {
261
+ // A call site that can be attributed (has an owner) — count it, and whether
262
+ // it resolved, for the coverage metric. (ama-m8k.12)
263
+ counts.callsTotal++;
264
+ const callee = resolveCallee(child, checker, declToId, root);
265
+ if (callee) {
266
+ counts.callsResolved++;
267
+ // `new X()` is a construction — a distinct Instantiates edge, not Calls.
268
+ const kind = ts.isNewExpression(child) ? "Instantiates" : "Calls";
269
+ edges.push({ from, to: callee, kind, at: locationOf(child) });
270
+ }
271
+ else {
272
+ // Unresolved — record what it called (by root) so coverage is explainable. (ama-qbn)
273
+ const targetRoot = calleeRoot(child);
274
+ if (targetRoot)
275
+ counts.unresolved[targetRoot] = (counts.unresolved[targetRoot] ?? 0) + 1;
276
+ // An unresolved higher-order call (arr.map(fn), p.then(handler)) invokes
277
+ // its function argument; attribute a heuristic Calls edge to each named
278
+ // callback, since that control flow is otherwise invisible. (ama-hft.15)
279
+ if (ts.isCallExpression(child) &&
280
+ ts.isPropertyAccessExpression(child.expression) &&
281
+ HIGHER_ORDER_METHODS.has(child.expression.name.text)) {
282
+ for (const arg of child.arguments) {
283
+ if (!ts.isIdentifier(arg) && !ts.isPropertyAccessExpression(arg))
284
+ continue;
285
+ const cb = resolveValueRef(arg, checker, declToId, root);
286
+ if (cb && cb !== from) {
287
+ edges.push({
288
+ from,
289
+ to: cb,
290
+ kind: "Calls",
291
+ provenance: "heuristic",
292
+ at: locationOf(arg),
293
+ });
294
+ }
295
+ }
296
+ }
297
+ }
298
+ }
299
+ const childId = declToId.get(child);
300
+ // A function-valued `const` is a node (ama-4s2); descending into it makes
301
+ // it the enclosing symbol, so calls in its body attribute to the const.
302
+ // A function-valued object-literal property (ama-zkr) is a node too, so its
303
+ // body's calls attribute to the property rather than leaking to the file.
304
+ // An arrow/function-expression is only enclosing when something registered
305
+ // it in declToId (ama-gpe: inline route handlers) — so ordinary callbacks
306
+ // (.map, .then) stay transparent.
307
+ const nextEnclosing = childId &&
308
+ (ts.isFunctionDeclaration(child) ||
309
+ ts.isMethodDeclaration(child) ||
310
+ ts.isConstructorDeclaration(child) ||
311
+ ts.isVariableDeclaration(child) ||
312
+ ts.isPropertyAssignment(child) ||
313
+ ts.isArrowFunction(child) ||
314
+ ts.isFunctionExpression(child))
315
+ ? childId
316
+ : enclosingId;
317
+ // Once the walk enters any function/arrow/accessor body, calls deeper in this
318
+ // subtree are no longer at module scope, so the File fallback stops applying —
319
+ // a call in an unregistered callback there is dropped, not attributed to the
320
+ // file. (ama-53q)
321
+ const childAtFileScope = atFileScope && !ts.isFunctionLike(child);
322
+ this.collectCalls(child, nextEnclosing, declToId, checker, edges, root, counts, fileNodeId, childAtFileScope);
323
+ });
324
+ }
325
+ /**
326
+ * Emit a `References` edge from the enclosing symbol to a module-level Variable
327
+ * node (ama-hft.12) each time its value is read — so `find_callers("MAX_RETRIES")`
328
+ * answers "who reads this constant". Mirrors `collectCalls`' enclosing-tracking.
329
+ *
330
+ * `resolveModuleVarRef` restricts targets to top-level const/let/var
331
+ * declarations (by *decl kind*, not a batch-local id set), so reads of
332
+ * functions/classes don't become References — and a cross-file const still
333
+ * resolves in a single-file reindex, where the batch holds no other file's
334
+ * Variable nodes (ama-l6k). Most false positives filter themselves out: a
335
+ * property-access member name and an import specifier resolve away, and a
336
+ * declaration's own name is caught by the `to !== enclosingId` guard. (ama-6k0)
337
+ */
338
+ collectVarReferences(node, enclosingId, declToId, checker, edges, root) {
339
+ node.forEachChild((child) => {
340
+ if (ts.isIdentifier(child) && enclosingId) {
341
+ const parent = child.parent;
342
+ if (ts.isPropertyAccessExpression(parent) && parent.name === child) {
343
+ // The member side of `X.prop`. A method *call* (`X.m()`) is already a
344
+ // Calls edge, but a non-call read — a field, a parameter property, or a
345
+ // method used as a value — is a References to that member, so
346
+ // find_referrers on a property shows where it's used. Covers both
347
+ // `this.<prop>` (ama-qo3) and cross-instance `obj.<prop>` reads (ama-emb):
348
+ // resolveValueRef → nodeIdForDecl already filters targets to in-project
349
+ // top-level + class/interface members, so external reads (`console.log`)
350
+ // and locals add no edge — only the `X.m()` call form is excluded here.
351
+ if (!(ts.isCallExpression(parent.parent) && parent.parent.expression === parent)) {
352
+ const to = resolveValueRef(child, checker, declToId, root);
353
+ if (to && to !== enclosingId) {
354
+ edges.push({ from: enclosingId, to, kind: "References" });
355
+ }
356
+ }
357
+ }
358
+ else {
359
+ const to = resolveModuleVarRef(child, checker, declToId, root);
360
+ if (to && to !== enclosingId) {
361
+ edges.push({ from: enclosingId, to, kind: "References" });
362
+ }
363
+ }
364
+ }
365
+ else if (ts.isObjectBindingPattern(child) && enclosingId) {
366
+ // Destructuring (`const { a, b: c } = obj`, `({ a }: T) =>`) reads each element's
367
+ // property of the pattern's type — track it like a `.prop` read so find_referrers
368
+ // sees destructuring sites, not just `obj.prop`. A renamed `{ a: b }` reads `a`;
369
+ // the same nodeIdForDecl filtering keeps external/local targets out. (ama-eda)
370
+ const type = checker.getTypeAtLocation(child);
371
+ for (const el of child.elements) {
372
+ const prop = el.propertyName ?? el.name;
373
+ if (!ts.isIdentifier(prop))
374
+ continue; // skip computed keys / nested patterns
375
+ const to = resolveTypeMember(type, prop.text, declToId, root);
376
+ if (to && to !== enclosingId)
377
+ edges.push({ from: enclosingId, to, kind: "References" });
378
+ }
379
+ }
380
+ const childId = declToId.get(child);
381
+ const nextEnclosing = childId &&
382
+ (ts.isFunctionDeclaration(child) ||
383
+ ts.isMethodDeclaration(child) ||
384
+ ts.isConstructorDeclaration(child) ||
385
+ ts.isVariableDeclaration(child) ||
386
+ ts.isPropertyAssignment(child) ||
387
+ ts.isArrowFunction(child) ||
388
+ ts.isFunctionExpression(child))
389
+ ? childId
390
+ : enclosingId;
391
+ this.collectVarReferences(child, nextEnclosing, declToId, checker, edges, root);
392
+ });
393
+ }
394
+ /** Walk a subtree emitting an `Implements` edge for each `class … implements I`. */
395
+ collectHeritage(node, declToId, checker, edges, root) {
396
+ if (ts.isClassDeclaration(node)) {
397
+ const from = declToId.get(node);
398
+ for (const clause of node.heritageClauses ?? []) {
399
+ // On a class, `extends` is inheritance; `implements` is interface conformance.
400
+ const kind = clause.token === ts.SyntaxKind.ExtendsKeyword ? "Inherits" : "Implements";
401
+ for (const type of clause.types) {
402
+ const to = from && resolveHeritage(type.expression, checker, declToId, root);
403
+ if (from && to)
404
+ edges.push({ from, to, kind });
405
+ }
406
+ }
407
+ }
408
+ node.forEachChild((child) => this.collectHeritage(child, declToId, checker, edges, root));
409
+ }
410
+ /**
411
+ * Emit an `Imports` edge from a file to each symbol it imports, and from a
412
+ * re-exporting file (`export { x } from "./m.js"`) to the re-exported symbol.
413
+ * Import/re-export bindings are aliases, so the edge target is the symbol's
414
+ * original declaration — even through a chain of barrels.
415
+ */
416
+ collectImports(sf, fromId, declToId, checker, edges, root) {
417
+ // Type-only imports/exports (`import type`, `import { type X }`, `export
418
+ // type`) are erased at runtime, so they get an ImportsType edge — counted for
419
+ // dependents/affected but excluded from runtime analyses (circular_imports).
420
+ const link = (name, typeOnly) => {
421
+ const to = resolveImport(name, checker, declToId, root);
422
+ if (to)
423
+ edges.push({ from: fromId, to, kind: typeOnly ? "ImportsType" : "Imports" });
424
+ };
425
+ // Imports can only appear as top-level statements in an ES module.
426
+ for (const stmt of sf.statements) {
427
+ if (ts.isImportDeclaration(stmt) && stmt.importClause) {
428
+ const clause = stmt.importClause;
429
+ if (clause.name)
430
+ link(clause.name, clause.isTypeOnly); // default import
431
+ const { namedBindings } = clause;
432
+ if (namedBindings) {
433
+ if (ts.isNamedImports(namedBindings)) {
434
+ // `import type {…}` makes the whole clause type-only; `import { type X }`
435
+ // marks a single specifier.
436
+ for (const spec of namedBindings.elements) {
437
+ link(spec.name, clause.isTypeOnly || spec.isTypeOnly);
438
+ }
439
+ }
440
+ else {
441
+ link(namedBindings.name, clause.isTypeOnly); // `import * as ns`
442
+ }
443
+ }
444
+ }
445
+ else if (ts.isExportDeclaration(stmt) &&
446
+ stmt.moduleSpecifier // `export { x }` without a source is a local export, not a re-export
447
+ ) {
448
+ const { exportClause } = stmt;
449
+ if (!exportClause) {
450
+ link(stmt.moduleSpecifier, stmt.isTypeOnly); // `export * from`
451
+ }
452
+ else if (ts.isNamedExports(exportClause)) {
453
+ for (const spec of exportClause.elements) {
454
+ link(spec.name, stmt.isTypeOnly || spec.isTypeOnly);
455
+ }
456
+ }
457
+ else {
458
+ link(exportClause.name, stmt.isTypeOnly); // `export * as ns from`
459
+ }
460
+ }
461
+ }
462
+ }
463
+ /**
464
+ * Emit a `UsesType` edge for each named type referenced in a parameter, a
465
+ * function/method return type, a property type, or a generic instantiation's
466
+ * type arguments (`f<Widget>()`, `new Box<Widget>()`, `extends Base<Widget>`),
467
+ * attributed to the nearest enclosing emitted symbol. Composite annotations are
468
+ * walked, so `Widget[]` or `Map<K, Widget>` still link to `Widget`. Types
469
+ * outside the analyzed set (`number`, library types) resolve to no node and are
470
+ * skipped.
471
+ */
472
+ collectTypeUsages(node, enclosingId, declToId, checker, edges, root) {
473
+ const annotations = [];
474
+ const returnAnnotations = []; // → Returns, kept distinct (ama-37c)
475
+ if (ts.isParameter(node) ||
476
+ ts.isPropertyDeclaration(node) ||
477
+ ts.isPropertySignature(node) ||
478
+ ts.isVariableDeclaration(node)) {
479
+ if (node.type)
480
+ annotations.push(node.type);
481
+ }
482
+ else if (ts.isFunctionDeclaration(node) ||
483
+ ts.isMethodDeclaration(node) ||
484
+ ts.isMethodSignature(node) ||
485
+ ts.isGetAccessorDeclaration(node)) {
486
+ if (node.type)
487
+ returnAnnotations.push(node.type); // return type → Returns
488
+ }
489
+ else if (ts.isCallExpression(node) ||
490
+ ts.isNewExpression(node) ||
491
+ ts.isExpressionWithTypeArguments(node)) {
492
+ // Generic instantiation: `f<Widget>()`, `new Box<Widget>()`, `extends Base<Widget>`.
493
+ if (node.typeArguments)
494
+ annotations.push(...node.typeArguments);
495
+ }
496
+ if (enclosingId) {
497
+ for (const annotation of annotations) {
498
+ for (const ref of typeReferencesIn(annotation)) {
499
+ const to = resolveTypeRef(ref.typeName, checker, declToId, root);
500
+ // A type used inside its own declaration's signature is noise, not a usage.
501
+ if (to && to !== enclosingId)
502
+ edges.push({ from: enclosingId, to, kind: "UsesType" });
503
+ }
504
+ }
505
+ for (const annotation of returnAnnotations) {
506
+ for (const ref of typeReferencesIn(annotation)) {
507
+ const to = resolveTypeRef(ref.typeName, checker, declToId, root);
508
+ if (to && to !== enclosingId)
509
+ edges.push({ from: enclosingId, to, kind: "Returns" });
510
+ }
511
+ }
512
+ // A decorator is a metadata/annotation dependency of the decorated symbol —
513
+ // modelled as UsesType (decorated → decorator), uniformly for call-form
514
+ // (`@log()`) and bare (`@sealed`) decorators. So `find_type_users(Component)`
515
+ // answers "what is decorated by @Component?".
516
+ if (ts.canHaveDecorators(node)) {
517
+ for (const decorator of ts.getDecorators(node) ?? []) {
518
+ const ref = ts.isCallExpression(decorator.expression)
519
+ ? decorator.expression.expression
520
+ : decorator.expression;
521
+ const to = resolveValueRef(ref, checker, declToId, root);
522
+ if (to && to !== enclosingId)
523
+ edges.push({ from: enclosingId, to, kind: "UsesType" });
524
+ }
525
+ }
526
+ }
527
+ node.forEachChild((child) => {
528
+ const childId = declToId.get(child);
529
+ this.collectTypeUsages(child, childId ?? enclosingId, declToId, checker, edges, root);
530
+ });
531
+ }
532
+ /**
533
+ * Detect framework routes (Express/NestJS-style call APIs: `app.get("/x", h)`,
534
+ * `router.post(...)`) and emit a Route node per route, plus a References edge to
535
+ * each named handler. Deliberately scoped to avoid false positives: an HTTP-verb
536
+ * method call whose first arg is a "/"-prefixed string literal and which has at
537
+ * least one handler arg (so `map.get("k")` / `headers.get("x")` don't match).
538
+ * Inline arrow/function handlers get a Route node but no edge yet — naming an
539
+ * anonymous handler is the arg-position-handler follow-up (ama-y9q).
540
+ */
541
+ collectRoutes(sf, rel, declToId, checker, nodes, edges, root, mountPrefixes) {
542
+ const visit = (n) => {
543
+ if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
544
+ const method = n.expression.name.text.toLowerCase();
545
+ const [first, ...rest] = n.arguments;
546
+ const handlers = rest.filter((a) => ts.isArrowFunction(a) ||
547
+ ts.isFunctionExpression(a) ||
548
+ ts.isIdentifier(a) ||
549
+ ts.isPropertyAccessExpression(a));
550
+ if (ROUTE_METHODS.has(method) &&
551
+ first !== undefined &&
552
+ ts.isStringLiteralLike(first) &&
553
+ first.text.startsWith("/") &&
554
+ handlers.length > 0) {
555
+ // If this route's receiver is a router mounted under a prefix, prepend it.
556
+ const receiverDecl = valueDeclOf(n.expression.expression, checker);
557
+ const prefix = receiverDecl ? mountPrefixes.get(receiverDecl) : undefined;
558
+ const name = `${method.toUpperCase()} ${joinRoutePath(prefix, first.text)}`;
559
+ const routeId = symbolId({ file: rel, qualifiedName: name });
560
+ nodes.push({
561
+ id: routeId,
562
+ kind: "Route",
563
+ name,
564
+ file: rel,
565
+ qualifiedName: name,
566
+ tier: "deep",
567
+ range: rangeOf(n, sf),
568
+ });
569
+ this.emitRouteHandlers(routeId, name, handlers, rel, sf, declToId, checker, nodes, edges, root);
570
+ }
571
+ }
572
+ // Object-config routes: Hapi `server.route({ method, path, handler })` and
573
+ // Fastify `fastify.route({ method, url, handler })` (also `.route([{…}])`).
574
+ // The method-named path above already covers the `app.get(path, h)` style
575
+ // that Fastify/Koa/Hono share with Express. (ama-rme.10)
576
+ if (ts.isCallExpression(n) &&
577
+ ts.isPropertyAccessExpression(n.expression) &&
578
+ n.expression.name.text === "route") {
579
+ const arg = n.arguments[0];
580
+ const configs = arg ? (ts.isArrayLiteralExpression(arg) ? arg.elements : [arg]) : [];
581
+ for (const config of configs) {
582
+ if (!ts.isObjectLiteralExpression(config))
583
+ continue;
584
+ const methods = routeMethods(objectProp(config, "method"));
585
+ const pathExpr = objectProp(config, "path") ?? objectProp(config, "url");
586
+ const handler = objectProp(config, "handler");
587
+ if (methods.length === 0 || !pathExpr || !ts.isStringLiteralLike(pathExpr) || !handler) {
588
+ continue;
589
+ }
590
+ if (!pathExpr.text.startsWith("/"))
591
+ continue;
592
+ for (const method of methods) {
593
+ const name = `${method.toUpperCase()} ${pathExpr.text}`;
594
+ const routeId = symbolId({ file: rel, qualifiedName: name });
595
+ nodes.push({
596
+ id: routeId,
597
+ kind: "Route",
598
+ name,
599
+ file: rel,
600
+ qualifiedName: name,
601
+ tier: "deep",
602
+ range: rangeOf(config, sf),
603
+ });
604
+ this.emitRouteHandlers(routeId, name, [handler], rel, sf, declToId, checker, nodes, edges, root);
605
+ }
606
+ }
607
+ }
608
+ // tRPC: a router property `name: <chain>.query/mutation/subscription(handler)`.
609
+ // The property key is the procedure name; the call's first arg is the
610
+ // handler. (ama-rme.11)
611
+ if (ts.isPropertyAssignment(n) &&
612
+ ts.isIdentifier(n.name) &&
613
+ ts.isCallExpression(n.initializer) &&
614
+ ts.isPropertyAccessExpression(n.initializer.expression) &&
615
+ PROCEDURE_TYPES.has(n.initializer.expression.name.text)) {
616
+ const handler = n.initializer.arguments[0];
617
+ if (handler && isHandlerExpr(handler)) {
618
+ const name = `${n.initializer.expression.name.text} ${n.name.text}`;
619
+ const routeId = symbolId({ file: rel, qualifiedName: name });
620
+ nodes.push({
621
+ id: routeId,
622
+ kind: "Route",
623
+ name,
624
+ file: rel,
625
+ qualifiedName: name,
626
+ tier: "deep",
627
+ range: rangeOf(n, sf),
628
+ });
629
+ this.emitRouteHandlers(routeId, name, [handler], rel, sf, declToId, checker, nodes, edges, root);
630
+ }
631
+ }
632
+ // GraphQL: a resolver map `{ Query: { field: resolver }, Mutation: {…} }` —
633
+ // each field under a Query/Mutation/Subscription root is a `Type.field`
634
+ // route referencing its resolver. (ama-rme.11)
635
+ if (ts.isObjectLiteralExpression(n)) {
636
+ for (const typeProp of n.properties) {
637
+ if (!ts.isPropertyAssignment(typeProp) ||
638
+ !ts.isIdentifier(typeProp.name) ||
639
+ !GRAPHQL_ROOTS.has(typeProp.name.text) ||
640
+ !ts.isObjectLiteralExpression(typeProp.initializer)) {
641
+ continue;
642
+ }
643
+ for (const field of typeProp.initializer.properties) {
644
+ if (!ts.isPropertyAssignment(field) ||
645
+ !ts.isIdentifier(field.name) ||
646
+ !isHandlerExpr(field.initializer)) {
647
+ continue;
648
+ }
649
+ const name = `${typeProp.name.text}.${field.name.text}`;
650
+ const routeId = symbolId({ file: rel, qualifiedName: name });
651
+ nodes.push({
652
+ id: routeId,
653
+ kind: "Route",
654
+ name,
655
+ file: rel,
656
+ qualifiedName: name,
657
+ tier: "deep",
658
+ range: rangeOf(field, sf),
659
+ });
660
+ this.emitRouteHandlers(routeId, name, [field.initializer], rel, sf, declToId, checker, nodes, edges, root);
661
+ }
662
+ }
663
+ }
664
+ // NestJS: @Controller("prefix") class whose methods carry @Get/@Post/...
665
+ // decorators. The decorated method IS the handler (already a Method node);
666
+ // the route path is the controller prefix joined with the method's path.
667
+ if (ts.isClassDeclaration(n) && ts.canHaveDecorators(n)) {
668
+ const controller = (ts.getDecorators(n) ?? [])
669
+ .map(decoratorInfo)
670
+ .find((d) => d.name === "Controller");
671
+ if (controller) {
672
+ for (const member of n.members) {
673
+ if (!ts.isMethodDeclaration(member) || !ts.canHaveDecorators(member))
674
+ continue;
675
+ const handlerId = declToId.get(member);
676
+ if (handlerId === undefined)
677
+ continue;
678
+ for (const dec of ts.getDecorators(member) ?? []) {
679
+ const info = decoratorInfo(dec);
680
+ if (!ROUTE_METHODS.has(info.name.toLowerCase()))
681
+ continue;
682
+ const name = `${info.name.toUpperCase()} ${joinRoutePath(controller.arg, info.arg)}`;
683
+ const routeId = symbolId({ file: rel, qualifiedName: name });
684
+ nodes.push({
685
+ id: routeId,
686
+ kind: "Route",
687
+ name,
688
+ file: rel,
689
+ qualifiedName: name,
690
+ tier: "deep",
691
+ range: rangeOf(member, sf),
692
+ });
693
+ edges.push({
694
+ from: routeId,
695
+ to: handlerId,
696
+ kind: "References",
697
+ provenance: "heuristic",
698
+ });
699
+ }
700
+ }
701
+ }
702
+ }
703
+ n.forEachChild(visit);
704
+ };
705
+ visit(sf);
706
+ }
707
+ /**
708
+ * Wire a route to its handler argument(s): an inline arrow/function becomes a
709
+ * synthesized handler Function node (named by the route, registered so its body
710
+ * attributes to it), while a named handler reference resolves to its node. Each
711
+ * gets a heuristic References edge from the route. Shared by every route style.
712
+ * (ama-rme.1, ama-rme.10)
713
+ */
714
+ emitRouteHandlers(routeId, name, handlers, rel, sf, declToId, checker, nodes, edges, root) {
715
+ let inlineCount = 0;
716
+ for (const handler of handlers) {
717
+ if (ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) {
718
+ const handlerName = `${name} handler${inlineCount === 0 ? "" : ` ${inlineCount + 1}`}`;
719
+ inlineCount++;
720
+ const handlerId = symbolId({ file: rel, qualifiedName: handlerName });
721
+ nodes.push({
722
+ id: handlerId,
723
+ kind: "Function",
724
+ name: handlerName,
725
+ file: rel,
726
+ qualifiedName: handlerName,
727
+ tier: "deep",
728
+ range: rangeOf(handler, sf),
729
+ });
730
+ declToId.set(handler, handlerId);
731
+ edges.push({ from: routeId, to: handlerId, kind: "References", provenance: "heuristic" });
732
+ continue;
733
+ }
734
+ const to = resolveValueRef(handler, checker, declToId, root);
735
+ if (to && to !== routeId) {
736
+ edges.push({ from: routeId, to, kind: "References", provenance: "heuristic" });
737
+ }
738
+ }
739
+ }
740
+ /**
741
+ * Synthesize a Function node for an inline arrow/function-expression passed as an
742
+ * argument to a *string-named* call whose result is itself consumed —
743
+ * `register("work", wrap("work", () => …))`. The leading string literal names the
744
+ * node (`"work handler"`); registering the arrow in `declToId` before
745
+ * `collectCalls` makes the callback body's calls attribute to it instead of
746
+ * leaking to the enclosing function (so per-handler blast radius is precise).
747
+ *
748
+ * Runs after `collectRoutes`, so a route's inline handler — already registered —
749
+ * is skipped. The "result is consumed" gate (the call is not a bare expression
750
+ * statement) is what separates a handler-producing wrapper like `tap(name, fn)`
751
+ * from a fire-and-forget test block like `it(name, fn)` / `describe(name, fn)`:
752
+ * only the former becomes a node, so the graph isn't flooded with one node per
753
+ * test case. (ama-y9q)
754
+ *
755
+ * The handler need not be a *direct* argument: it may be nested inside a second
756
+ * wrapper whose own first argument is not a string — `tap("search",
757
+ * queryTool(session, () => …))`. `collectHandlerArrows` digs through such
758
+ * wrapper-call arguments (stopping at the first function, so a handler's body is
759
+ * never mistaken for another handler), keying every one by the outer name. (ama-63x)
760
+ */
761
+ collectCallbackHandlers(node, enclosingId, rel, sf, declToId, nodes, edges) {
762
+ node.forEachChild((child) => {
763
+ const first = ts.isCallExpression(child) ? child.arguments[0] : undefined;
764
+ if (ts.isCallExpression(child) &&
765
+ !ts.isExpressionStatement(child.parent) &&
766
+ first !== undefined &&
767
+ ts.isStringLiteralLike(first)) {
768
+ let inlineCount = 0;
769
+ for (const arg of collectHandlerArrows(child.arguments)) {
770
+ if (declToId.has(arg))
771
+ continue; // already a node (e.g. a route handler)
772
+ const handlerName = `${first.text} handler${inlineCount === 0 ? "" : ` ${inlineCount + 1}`}`;
773
+ inlineCount++;
774
+ const handlerId = symbolId({ file: rel, qualifiedName: handlerName });
775
+ nodes.push({
776
+ id: handlerId,
777
+ kind: "Function",
778
+ name: handlerName,
779
+ file: rel,
780
+ qualifiedName: handlerName,
781
+ tier: "deep",
782
+ range: rangeOf(arg, sf),
783
+ });
784
+ declToId.set(arg, handlerId);
785
+ if (enclosingId) {
786
+ edges.push({
787
+ from: enclosingId,
788
+ to: handlerId,
789
+ kind: "References",
790
+ provenance: "heuristic",
791
+ });
792
+ }
793
+ }
794
+ }
795
+ const childId = declToId.get(child);
796
+ // Mirror collectCalls' enclosing rule so a synthesized handler nested inside
797
+ // another becomes the `from` of the inner one's reference edge.
798
+ const nextEnclosing = childId &&
799
+ (ts.isFunctionDeclaration(child) ||
800
+ ts.isMethodDeclaration(child) ||
801
+ ts.isConstructorDeclaration(child) ||
802
+ ts.isVariableDeclaration(child) ||
803
+ ts.isPropertyAssignment(child) ||
804
+ ts.isArrowFunction(child) ||
805
+ ts.isFunctionExpression(child))
806
+ ? childId
807
+ : enclosingId;
808
+ this.collectCallbackHandlers(child, nextEnclosing, rel, sf, declToId, nodes, edges);
809
+ });
810
+ }
811
+ /**
812
+ * File-based routing: a route file at a framework convention path exports HTTP
813
+ * method handlers and the URL comes from the *path* (not a call). Next.js App
814
+ * Router (`app/**​/route.ts`) and SvelteKit (`src/routes/**​/+server.ts`) — each
815
+ * exported `GET`/`POST`/… function becomes a `<METHOD> <path>` Route referencing
816
+ * it. Heuristic: the route is inferred from filesystem convention. (ama-rme.7)
817
+ */
818
+ collectFileRoutes(sf, rel, declToId, checker, nodes, edges, root) {
819
+ // Marker-file conventions (route.ts/+server.ts → directory path, ama-rme.7)
820
+ // and filename conventions (pages//server/ → filename path, ama-w7g).
821
+ const marker = fileRoutePath(rel);
822
+ const named = filenameRoutePath(rel);
823
+ const routePath = marker ?? named?.path;
824
+ if (routePath === undefined)
825
+ return;
826
+ const emit = (methodName, decl) => {
827
+ if (!ROUTE_METHODS.has(methodName.toLowerCase()))
828
+ return;
829
+ const handlerId = declToId.get(decl);
830
+ if (!handlerId)
831
+ return;
832
+ const name = `${methodName} ${routePath}`;
833
+ const routeId = symbolId({ file: rel, qualifiedName: name });
834
+ nodes.push({
835
+ id: routeId,
836
+ kind: "Route",
837
+ name,
838
+ file: rel,
839
+ qualifiedName: name,
840
+ tier: "deep",
841
+ range: rangeOf(decl, sf),
842
+ });
843
+ edges.push({ from: routeId, to: handlerId, kind: "References", provenance: "heuristic" });
844
+ };
845
+ for (const stmt of sf.statements) {
846
+ if (!isExported(stmt))
847
+ continue;
848
+ if (ts.isFunctionDeclaration(stmt) && stmt.name) {
849
+ emit(stmt.name.text, stmt);
850
+ }
851
+ else if (ts.isVariableStatement(stmt)) {
852
+ for (const d of stmt.declarationList.declarations) {
853
+ if (ts.isIdentifier(d.name) &&
854
+ d.initializer &&
855
+ (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))) {
856
+ emit(d.name.text, d);
857
+ }
858
+ }
859
+ }
860
+ }
861
+ // A default-export request handler (Next.js Pages Router, Nuxt) — an any-method
862
+ // route. Page components (a default export outside an `api` dir) are excluded
863
+ // by `allowDefault`. (ama-w7g)
864
+ if (named?.allowDefault) {
865
+ const handler = findDefaultExportHandler(sf);
866
+ if (handler) {
867
+ const handlerNode = "decl" in handler ? handler.decl : handler.expr;
868
+ const name = `ALL ${named.path}`;
869
+ const routeId = symbolId({ file: rel, qualifiedName: name });
870
+ nodes.push({
871
+ id: routeId,
872
+ kind: "Route",
873
+ name,
874
+ file: rel,
875
+ qualifiedName: name,
876
+ tier: "deep",
877
+ range: rangeOf(handlerNode, sf),
878
+ });
879
+ if ("decl" in handler) {
880
+ const handlerId = declToId.get(handler.decl);
881
+ if (handlerId) {
882
+ edges.push({
883
+ from: routeId,
884
+ to: handlerId,
885
+ kind: "References",
886
+ provenance: "heuristic",
887
+ });
888
+ }
889
+ }
890
+ else {
891
+ this.emitRouteHandlers(routeId, name, [handler.expr], rel, sf, declToId, checker, nodes, edges, root);
892
+ }
893
+ }
894
+ }
895
+ }
896
+ /**
897
+ * Synthesize call edges for the EventEmitter pattern: an `emitter.emit("ch")`
898
+ * invokes every handler registered with `.on("ch", h)` (or once/addListener)
899
+ * for the same channel string. Heuristic — matched by channel name, not proven
900
+ * dispatch — so the synthesized edges carry `provenance: "heuristic"`. Runs
901
+ * after collectCallbackHandlers so inline `.on("ch", () => …)` arrows (already
902
+ * synthesized into handler nodes there) are connectable too. (ama-hft.14)
903
+ */
904
+ collectEvents(sf, declToId, checker, edges, root) {
905
+ // Pass 1: channel -> the handler node(s) registered for it.
906
+ const handlers = new Map();
907
+ const collectRegistrations = (node) => {
908
+ node.forEachChild((child) => {
909
+ if (ts.isCallExpression(child) &&
910
+ ts.isPropertyAccessExpression(child.expression) &&
911
+ ON_METHODS.has(child.expression.name.text)) {
912
+ const [channel, handler] = child.arguments;
913
+ if (channel && ts.isStringLiteralLike(channel) && handler) {
914
+ const handlerId = eventHandlerId(handler, declToId, checker, root);
915
+ if (handlerId) {
916
+ const list = handlers.get(channel.text) ?? [];
917
+ list.push(handlerId);
918
+ handlers.set(channel.text, list);
919
+ }
920
+ }
921
+ }
922
+ collectRegistrations(child);
923
+ });
924
+ };
925
+ collectRegistrations(sf);
926
+ if (handlers.size === 0)
927
+ return; // nothing listens — no edges to synthesize
928
+ // Pass 2: each `emit("ch")` calls every handler registered for "ch".
929
+ const collectEmits = (node, enclosingId) => {
930
+ node.forEachChild((child) => {
931
+ if (enclosingId &&
932
+ ts.isCallExpression(child) &&
933
+ ts.isPropertyAccessExpression(child.expression) &&
934
+ child.expression.name.text === "emit") {
935
+ const channel = child.arguments[0];
936
+ if (channel && ts.isStringLiteralLike(channel)) {
937
+ for (const handlerId of handlers.get(channel.text) ?? []) {
938
+ if (handlerId !== enclosingId) {
939
+ edges.push({
940
+ from: enclosingId,
941
+ to: handlerId,
942
+ kind: "Calls",
943
+ provenance: "heuristic",
944
+ at: locationOf(child),
945
+ });
946
+ }
947
+ }
948
+ }
949
+ }
950
+ const childId = declToId.get(child);
951
+ const nextEnclosing = childId &&
952
+ (ts.isFunctionDeclaration(child) ||
953
+ ts.isMethodDeclaration(child) ||
954
+ ts.isConstructorDeclaration(child) ||
955
+ ts.isVariableDeclaration(child) ||
956
+ ts.isPropertyAssignment(child) ||
957
+ ts.isArrowFunction(child) ||
958
+ ts.isFunctionExpression(child))
959
+ ? childId
960
+ : enclosingId;
961
+ collectEmits(child, nextEnclosing);
962
+ });
963
+ };
964
+ collectEmits(sf, undefined);
965
+ }
966
+ /**
967
+ * Find Express mounts — `app.use("/prefix", router, …)` — and map each mounted
968
+ * argument's declaration to the prefix. Runs over every file before route
969
+ * detection so a router defined in one file and mounted in another composes.
970
+ */
971
+ collectMounts(sf, checker, mountPrefixes) {
972
+ const visit = (n) => {
973
+ if (ts.isCallExpression(n) &&
974
+ ts.isPropertyAccessExpression(n.expression) &&
975
+ n.expression.name.text === "use") {
976
+ const [first, ...rest] = n.arguments;
977
+ if (first !== undefined && ts.isStringLiteralLike(first) && first.text.startsWith("/")) {
978
+ for (const arg of rest) {
979
+ if (ts.isIdentifier(arg) || ts.isPropertyAccessExpression(arg)) {
980
+ const decl = valueDeclOf(arg, checker);
981
+ // Harmless if `arg` is middleware, not a router: it just won't have routes.
982
+ if (decl)
983
+ mountPrefixes.set(decl, first.text);
984
+ }
985
+ }
986
+ }
987
+ }
988
+ n.forEachChild(visit);
989
+ };
990
+ visit(sf);
991
+ }
992
+ }
993
+ /** HTTP-verb methods that mark an Express/Nest-style route registration call. */
994
+ const ROUTE_METHODS = new Set(["get", "post", "put", "delete", "patch", "options", "head", "all"]);
995
+ /** A decorator's callee name and its first string-literal argument, if any. */
996
+ function decoratorInfo(dec) {
997
+ const expr = dec.expression;
998
+ const callee = ts.isCallExpression(expr) ? expr.expression : expr;
999
+ const name = ts.isIdentifier(callee)
1000
+ ? callee.text
1001
+ : ts.isPropertyAccessExpression(callee)
1002
+ ? callee.name.text
1003
+ : "";
1004
+ let arg;
1005
+ if (ts.isCallExpression(expr)) {
1006
+ const first = expr.arguments[0];
1007
+ if (first && ts.isStringLiteralLike(first))
1008
+ arg = first.text;
1009
+ }
1010
+ return { name, arg };
1011
+ }
1012
+ /** The value declaration an expression resolves to (alias-followed), or undefined. */
1013
+ function valueDeclOf(expr, checker) {
1014
+ let symbol = checker.getSymbolAtLocation(expr);
1015
+ if (!symbol)
1016
+ return undefined;
1017
+ if (symbol.flags & ts.SymbolFlags.Alias)
1018
+ symbol = checker.getAliasedSymbol(symbol);
1019
+ return symbol.valueDeclaration ?? symbol.declarations?.[0];
1020
+ }
1021
+ /**
1022
+ * The inline handler callbacks reachable from a registration call's arguments:
1023
+ * direct arrow/function args, plus arrows nested inside wrapper calls —
1024
+ * `tap("name", queryTool(session, () => …))`. Descends through call-argument
1025
+ * positions but stops at the first function in each branch: an arrow's body is its
1026
+ * own scope (its `.map`/`.then` callbacks are not handlers), and the wrapper's
1027
+ * non-call args (`session`) carry nothing. (ama-63x)
1028
+ */
1029
+ function collectHandlerArrows(args) {
1030
+ const out = [];
1031
+ const dig = (expr) => {
1032
+ if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
1033
+ out.push(expr); // a handler — do not descend into its body
1034
+ return;
1035
+ }
1036
+ if (ts.isCallExpression(expr)) {
1037
+ for (const a of expr.arguments)
1038
+ dig(a);
1039
+ }
1040
+ };
1041
+ for (const a of args)
1042
+ dig(a);
1043
+ return out;
1044
+ }
1045
+ /** Join a controller prefix and a method sub-path into a leading-slash route path. */
1046
+ function joinRoutePath(prefix, sub) {
1047
+ const parts = [prefix, sub]
1048
+ .filter((p) => p !== undefined && p.length > 0)
1049
+ .map((p) => p.replace(/^\/+|\/+$/g, ""))
1050
+ .filter((p) => p.length > 0);
1051
+ return `/${parts.join("/")}`;
1052
+ }
1053
+ /** Map a declaration node to a (kind, name) pair, or undefined if it isn't one. */
1054
+ function describe(node) {
1055
+ if (ts.isFunctionDeclaration(node) && node.name) {
1056
+ const component = isComponentName(node.name.text) && returnsJsx(node);
1057
+ return { kind: component ? "Component" : "Function", name: node.name.text };
1058
+ }
1059
+ if (ts.isClassDeclaration(node) && node.name) {
1060
+ return { kind: "Class", name: node.name.text };
1061
+ }
1062
+ if (ts.isInterfaceDeclaration(node)) {
1063
+ return { kind: "Interface", name: node.name.text };
1064
+ }
1065
+ if (ts.isEnumDeclaration(node)) {
1066
+ return { kind: "Enum", name: node.name.text };
1067
+ }
1068
+ if (ts.isTypeAliasDeclaration(node)) {
1069
+ return { kind: "TypeAlias", name: node.name.text };
1070
+ }
1071
+ // A `namespace N {}` / `module N {}` or an ambient `declare module "pkg" {}` —
1072
+ // the declared-but-never-emitted Module kind. The name is an Identifier
1073
+ // (namespace) or a StringLiteral (ambient module); both expose `.text`. (ama-hft.13)
1074
+ if (ts.isModuleDeclaration(node)) {
1075
+ return { kind: "Module", name: node.name.text };
1076
+ }
1077
+ if ((ts.isMethodDeclaration(node) || ts.isMethodSignature(node)) && ts.isIdentifier(node.name)) {
1078
+ return { kind: "Method", name: node.name.text };
1079
+ }
1080
+ // A constructor is a Method named "constructor" (qualified `Cls.constructor`),
1081
+ // so its body's wiring — calls, references, param-type usages — attributes to it
1082
+ // instead of being dropped at the class boundary. (ama-vz8)
1083
+ if (ts.isConstructorDeclaration(node)) {
1084
+ return { kind: "Method", name: "constructor" };
1085
+ }
1086
+ if ((ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) &&
1087
+ ts.isIdentifier(node.name)) {
1088
+ return { kind: "Property", name: node.name.text };
1089
+ }
1090
+ if ((ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) &&
1091
+ ts.isIdentifier(node.name)) {
1092
+ // A get/set pair shares one member name -> one Property node (ids dedup).
1093
+ return { kind: "Property", name: node.name.text };
1094
+ }
1095
+ if (ts.isVariableDeclaration(node) &&
1096
+ node.initializer !== undefined &&
1097
+ (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
1098
+ ts.isIdentifier(node.name)) {
1099
+ const component = isComponentName(node.name.text) && returnsJsx(node.initializer);
1100
+ return { kind: component ? "Component" : "Function", name: node.name.text };
1101
+ }
1102
+ // A Vue component: `const X = defineComponent({ … })`. Before the Variable
1103
+ // catch-all (its initializer is a call, not an object literal). (ama-rme.9)
1104
+ if (ts.isVariableDeclaration(node) &&
1105
+ ts.isIdentifier(node.name) &&
1106
+ node.initializer !== undefined &&
1107
+ ts.isCallExpression(node.initializer) &&
1108
+ isDefineComponentCall(node.initializer)) {
1109
+ return { kind: "Component", name: node.name.text };
1110
+ }
1111
+ // A function-valued object-literal property (`{ run: () => … }`) — a method in
1112
+ // all but syntax. Method shorthand (`{ run() {} }`) is already a MethodDeclaration.
1113
+ if (ts.isPropertyAssignment(node) &&
1114
+ (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
1115
+ ts.isIdentifier(node.name)) {
1116
+ return { kind: "Method", name: node.name.text };
1117
+ }
1118
+ // Any other module-level variable binding (`const MAX_RETRIES = 3`, `const SET =
1119
+ // new Set(...)`, `const LABELS = [...] as const`) — a Variable node so it's
1120
+ // searchable, snippet-able, and listed in a file's outline. Function-valued and
1121
+ // object-literal initializers are handled above / by `visit` (their members
1122
+ // become nodes; the object const itself stays a non-node, the ama-zkr rule).
1123
+ // `visit` only reaches top-level / class-member / object-member declarations, so
1124
+ // locals inside function bodies never become Variable nodes.
1125
+ if (ts.isVariableDeclaration(node) &&
1126
+ ts.isIdentifier(node.name) &&
1127
+ !(node.initializer !== undefined && ts.isObjectLiteralExpression(node.initializer))) {
1128
+ return { kind: "Variable", name: node.name.text };
1129
+ }
1130
+ return undefined;
1131
+ }
1132
+ /**
1133
+ * Resolve a call's callee to a graph node id, following import aliases. Accepts
1134
+ * a `new` expression too: its `.expression` is the constructed class, which
1135
+ * resolves the same way (so construction counts as a call site).
1136
+ */
1137
+ /** Collapse repeated Calls/Instantiates edges between the same (from, to) into a
1138
+ * single edge that records *every* call site in `sites`, so find_callers/callees
1139
+ * can report all of them rather than just the first the store would keep. Other
1140
+ * edge kinds pass through untouched. (ama-hft.10) */
1141
+ function accumulateCallSites(edges) {
1142
+ const callEdges = new Map();
1143
+ const result = [];
1144
+ for (const edge of edges) {
1145
+ if (edge.kind !== "Calls" && edge.kind !== "Instantiates") {
1146
+ result.push(edge);
1147
+ continue;
1148
+ }
1149
+ const key = `${edge.from}${edge.to}${edge.kind}`;
1150
+ const existing = callEdges.get(key);
1151
+ if (!existing) {
1152
+ callEdges.set(key, edge);
1153
+ result.push(edge);
1154
+ }
1155
+ else if (edge.at) {
1156
+ if (!existing.at)
1157
+ existing.at = edge.at;
1158
+ else {
1159
+ existing.sites ??= [existing.at];
1160
+ existing.sites.push(edge.at);
1161
+ }
1162
+ }
1163
+ }
1164
+ return result;
1165
+ }
1166
+ /** Built-in higher-order methods that invoke a function argument — Array
1167
+ * iteration and Promise chaining. A named function passed to one of these is
1168
+ * attributed a heuristic Calls edge (the method name is a pattern match, not a
1169
+ * proof it calls the arg, hence heuristic). (ama-hft.15) */
1170
+ const HIGHER_ORDER_METHODS = new Set([
1171
+ "map",
1172
+ "forEach",
1173
+ "filter",
1174
+ "reduce",
1175
+ "reduceRight",
1176
+ "flatMap",
1177
+ "some",
1178
+ "every",
1179
+ "find",
1180
+ "findIndex",
1181
+ "findLast",
1182
+ "findLastIndex",
1183
+ "sort",
1184
+ "then",
1185
+ "catch",
1186
+ "finally",
1187
+ ]);
1188
+ /** EventEmitter registration methods: `.on/.once/.addListener("ch", handler)` and
1189
+ * their prepend variants all bind a handler to a channel. (ama-hft.14) */
1190
+ const ON_METHODS = new Set(["on", "once", "addListener", "prependListener", "prependOnceListener"]);
1191
+ /** The node a `.on("ch", handler)` argument refers to: a named function/method
1192
+ * (resolved via the checker) or an inline arrow already synthesized into a
1193
+ * handler node by collectCallbackHandlers. (ama-hft.14) */
1194
+ function eventHandlerId(arg, declToId, checker, root) {
1195
+ if (ts.isIdentifier(arg) || ts.isPropertyAccessExpression(arg)) {
1196
+ return resolveValueRef(arg, checker, declToId, root);
1197
+ }
1198
+ if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
1199
+ return declToId.get(arg);
1200
+ }
1201
+ return undefined;
1202
+ }
1203
+ /** The initializer of an object-literal property by key — reads route-config
1204
+ * fields (method/path/url/handler) for object-style routing. (ama-rme.10) */
1205
+ function objectProp(obj, key) {
1206
+ for (const prop of obj.properties) {
1207
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === key) {
1208
+ return prop.initializer;
1209
+ }
1210
+ }
1211
+ return undefined;
1212
+ }
1213
+ /** HTTP method(s) from a route config's `method` value — a single string ("GET")
1214
+ * or an array (["GET", "POST"]). (ama-rme.10) */
1215
+ function routeMethods(expr) {
1216
+ if (!expr)
1217
+ return [];
1218
+ if (ts.isStringLiteralLike(expr))
1219
+ return [expr.text];
1220
+ if (ts.isArrayLiteralExpression(expr)) {
1221
+ return expr.elements.filter(ts.isStringLiteralLike).map((e) => e.text);
1222
+ }
1223
+ return [];
1224
+ }
1225
+ /** tRPC procedure builders — a router property `key: proc.query(handler)`. (ama-rme.11) */
1226
+ const PROCEDURE_TYPES = new Set(["query", "mutation", "subscription"]);
1227
+ /** GraphQL root types in a resolver map. (ama-rme.11) */
1228
+ const GRAPHQL_ROOTS = new Set(["Query", "Mutation", "Subscription"]);
1229
+ /** Whether an expression looks like a route/resolver handler — an inline
1230
+ * function or a reference to one. Excludes string/config args so `db.query(sql)`
1231
+ * isn't mistaken for a tRPC procedure. (ama-rme.11) */
1232
+ function isHandlerExpr(expr) {
1233
+ return (ts.isArrowFunction(expr) ||
1234
+ ts.isFunctionExpression(expr) ||
1235
+ ts.isIdentifier(expr) ||
1236
+ ts.isPropertyAccessExpression(expr));
1237
+ }
1238
+ /** Convert a file-route directory segment to a URL segment: `[id]` → `:id`,
1239
+ * `[...slug]` → `*`, `[[opt]]` → `:opt`; a `(group)` is dropped (no URL effect);
1240
+ * else verbatim. (ama-rme.7) */
1241
+ function routeSegment(seg) {
1242
+ if (seg.startsWith("(") && seg.endsWith(")"))
1243
+ return undefined;
1244
+ if (seg.startsWith("[...") && seg.endsWith("]"))
1245
+ return "*";
1246
+ if (seg.startsWith("[[") && seg.endsWith("]]"))
1247
+ return `:${seg.slice(2, -2)}`;
1248
+ if (seg.startsWith("[") && seg.endsWith("]"))
1249
+ return `:${seg.slice(1, -1)}`;
1250
+ return seg;
1251
+ }
1252
+ /** The URL path a file-based route file maps to, or undefined if it isn't one:
1253
+ * Next.js App Router `app/**​/route.ts` and SvelteKit `src/routes/**​/+server.ts`
1254
+ * — the path is the directories between the routes root and the marker file. (ama-rme.7) */
1255
+ function fileRoutePath(rel) {
1256
+ const parts = rel.split("/");
1257
+ const file = parts[parts.length - 1] ?? "";
1258
+ let rootIdx = -1;
1259
+ if (file.startsWith("route."))
1260
+ rootIdx = parts.lastIndexOf("app");
1261
+ else if (file.startsWith("+server."))
1262
+ rootIdx = parts.lastIndexOf("routes");
1263
+ if (rootIdx < 0)
1264
+ return undefined;
1265
+ const segs = parts
1266
+ .slice(rootIdx + 1, parts.length - 1)
1267
+ .map(routeSegment)
1268
+ .filter((s) => s !== undefined);
1269
+ return `/${segs.join("/")}`;
1270
+ }
1271
+ /** Module extensions a filename-routed endpoint can use. (ama-w7g) */
1272
+ const FILE_ROUTE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
1273
+ /** The URL path for a *filename*-routed module (the route includes the filename),
1274
+ * plus whether a default export should count as a handler. Next.js Pages Router
1275
+ * & Astro live under `pages/`/`src/pages/`; Nuxt under `server/api/` (keeps the
1276
+ * `/api` prefix) or `server/routes/` (stripped). `index` maps to its directory.
1277
+ * `allowDefault` is true only in an API context — a `pages/about.tsx` default
1278
+ * export is a page component, not a route. (ama-w7g) */
1279
+ function filenameRoutePath(rel) {
1280
+ const parts = rel.split("/");
1281
+ const file = parts[parts.length - 1] ?? "";
1282
+ const dot = file.lastIndexOf(".");
1283
+ if (dot < 0 || !FILE_ROUTE_EXTS.has(file.slice(dot)))
1284
+ return undefined;
1285
+ const base = file.slice(0, dot);
1286
+ if (base.startsWith("_"))
1287
+ return undefined; // Next.js specials: _app, _document
1288
+ const pagesIdx = parts.lastIndexOf("pages");
1289
+ const serverIdx = parts.lastIndexOf("server");
1290
+ let rootIdx;
1291
+ let allowDefault;
1292
+ if (pagesIdx >= 0) {
1293
+ rootIdx = pagesIdx;
1294
+ allowDefault = parts[pagesIdx + 1] === "api"; // Next API routes live under pages/api
1295
+ }
1296
+ else if (serverIdx >= 0 && parts[serverIdx + 1] === "routes") {
1297
+ rootIdx = serverIdx + 1; // server/routes — `routes` stripped from the URL
1298
+ allowDefault = true;
1299
+ }
1300
+ else if (serverIdx >= 0 && parts[serverIdx + 1] === "api") {
1301
+ rootIdx = serverIdx; // server/api — `api` kept in the URL
1302
+ allowDefault = true;
1303
+ }
1304
+ else {
1305
+ return undefined;
1306
+ }
1307
+ const dirs = parts.slice(rootIdx + 1, parts.length - 1);
1308
+ const leaf = base === "index" ? [] : [base];
1309
+ const segs = [...dirs, ...leaf].map(routeSegment).filter((s) => s !== undefined);
1310
+ return { path: `/${segs.join("/")}`, allowDefault };
1311
+ }
1312
+ /** Whether a declaration carries the `default` modifier (`export default …`). (ama-w7g) */
1313
+ function isDefaultExported(node) {
1314
+ return (ts.canHaveModifiers(node) &&
1315
+ (ts.getModifiers(node) ?? []).some((m) => m.kind === ts.SyntaxKind.DefaultKeyword));
1316
+ }
1317
+ /** Whether a callee is Nuxt's `defineEventHandler`/`eventHandler` wrapper. (ama-w7g) */
1318
+ function isEventHandlerWrapper(expr) {
1319
+ const name = ts.isIdentifier(expr)
1320
+ ? expr.text
1321
+ : ts.isPropertyAccessExpression(expr)
1322
+ ? expr.name.text
1323
+ : "";
1324
+ return name === "defineEventHandler" || name === "eventHandler";
1325
+ }
1326
+ /** A module's default-export request handler: a named `export default function`
1327
+ * (linked via declToId) or an `export default <expr>` (arrow/ref, with Nuxt's
1328
+ * `defineEventHandler(...)` unwrapped). Anonymous default functions have no node
1329
+ * to link, so they're skipped. (ama-w7g) */
1330
+ function findDefaultExportHandler(sf) {
1331
+ for (const stmt of sf.statements) {
1332
+ if (ts.isFunctionDeclaration(stmt) && isDefaultExported(stmt)) {
1333
+ return stmt.name ? { decl: stmt } : undefined;
1334
+ }
1335
+ if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
1336
+ let expr = stmt.expression;
1337
+ if (ts.isCallExpression(expr) &&
1338
+ isEventHandlerWrapper(expr.expression) &&
1339
+ expr.arguments[0]) {
1340
+ expr = expr.arguments[0];
1341
+ }
1342
+ return { expr };
1343
+ }
1344
+ }
1345
+ return undefined;
1346
+ }
1347
+ /** Whether a top-level statement carries an `export` modifier. (ama-rme.7) */
1348
+ function isExported(node) {
1349
+ return (ts.canHaveModifiers(node) &&
1350
+ (ts.getModifiers(node) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword));
1351
+ }
1352
+ /** React requires a component name to be capitalized (so JSX `<Foo/>` isn't a host
1353
+ * element). Used to tell a JSX-returning component from a render helper. (ama-rme.9) */
1354
+ function isComponentName(name) {
1355
+ return /^[A-Z]/.test(name);
1356
+ }
1357
+ /** Whether an expression is a JSX element/fragment (through parentheses). (ama-rme.9) */
1358
+ function isJsxLike(expr) {
1359
+ let e = expr;
1360
+ while (ts.isParenthesizedExpression(e))
1361
+ e = e.expression;
1362
+ return ts.isJsxElement(e) || ts.isJsxFragment(e) || ts.isJsxSelfClosingElement(e);
1363
+ }
1364
+ /** Whether a function returns JSX — an arrow with a JSX concise body, or any
1365
+ * `return <jsx>` in its block (not counting nested functions). (ama-rme.9) */
1366
+ function returnsJsx(fn) {
1367
+ if (ts.isArrowFunction(fn) && fn.body && !ts.isBlock(fn.body))
1368
+ return isJsxLike(fn.body);
1369
+ if (!fn.body)
1370
+ return false;
1371
+ let found = false;
1372
+ const visit = (n) => {
1373
+ if (found)
1374
+ return;
1375
+ if (ts.isReturnStatement(n) && n.expression && isJsxLike(n.expression)) {
1376
+ found = true;
1377
+ return;
1378
+ }
1379
+ // A nested function/class has its own returns — don't attribute them here.
1380
+ if (ts.isFunctionDeclaration(n) ||
1381
+ ts.isFunctionExpression(n) ||
1382
+ ts.isArrowFunction(n) ||
1383
+ ts.isClassDeclaration(n)) {
1384
+ return;
1385
+ }
1386
+ n.forEachChild(visit);
1387
+ };
1388
+ visit(fn.body);
1389
+ return found;
1390
+ }
1391
+ /** Whether a call is `defineComponent(...)` — Vue's component factory. (ama-rme.9) */
1392
+ function isDefineComponentCall(call) {
1393
+ const callee = call.expression;
1394
+ if (ts.isIdentifier(callee))
1395
+ return callee.text === "defineComponent";
1396
+ if (ts.isPropertyAccessExpression(callee))
1397
+ return callee.name.text === "defineComponent";
1398
+ return false;
1399
+ }
1400
+ /** The leftmost identifier of a call's callee — `ts` for `ts.isCallExpression(x)`,
1401
+ * `helper` for `helper()` — used to group unresolved calls by what they target
1402
+ * (a module/object name). `this.X...` groups by `X` (the property/method on
1403
+ * `this`), since the bare `this` root is opaque about where the call is — most of
1404
+ * these are builtin calls on instance fields like `this.items.push()`. Undefined
1405
+ * for call results, super(), etc. (ama-qbn, ama-k9t) */
1406
+ function calleeRoot(call) {
1407
+ let e = call.expression;
1408
+ while (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) {
1409
+ if (e.expression.kind === ts.SyntaxKind.ThisKeyword) {
1410
+ return ts.isPropertyAccessExpression(e) ? e.name.text : undefined;
1411
+ }
1412
+ e = e.expression;
1413
+ }
1414
+ if (ts.isIdentifier(e))
1415
+ return e.text;
1416
+ if (e.kind === ts.SyntaxKind.ThisKeyword)
1417
+ return "this";
1418
+ return undefined;
1419
+ }
1420
+ function resolveCallee(call, checker, declToId, root) {
1421
+ let symbol = checker.getSymbolAtLocation(call.expression);
1422
+ if (!symbol) {
1423
+ const decl = checker.getResolvedSignature(call)?.declaration;
1424
+ return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
1425
+ }
1426
+ if (symbol.flags & ts.SymbolFlags.Alias) {
1427
+ symbol = checker.getAliasedSymbol(symbol);
1428
+ }
1429
+ const decl = symbol.valueDeclaration ?? symbol.declarations?.[0];
1430
+ return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
1431
+ }
1432
+ /** Resolve a value-position reference (e.g. a decorator's `@Foo`) to a node id. */
1433
+ function resolveValueRef(expr, checker, declToId, root) {
1434
+ let symbol = checker.getSymbolAtLocation(expr);
1435
+ if (!symbol)
1436
+ return undefined;
1437
+ if (symbol.flags & ts.SymbolFlags.Alias) {
1438
+ symbol = checker.getAliasedSymbol(symbol);
1439
+ }
1440
+ const decl = symbol.valueDeclaration ?? symbol.declarations?.[0];
1441
+ return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
1442
+ }
1443
+ /** Resolve a named property of a type to its declaration's node id — for destructuring,
1444
+ * where the read is a binding name against the destructured value's type rather than a
1445
+ * `.prop` expression. Same in-project filtering as {@link resolveValueRef}. (ama-eda) */
1446
+ function resolveTypeMember(type, name, declToId, root) {
1447
+ const prop = type.getProperty(name);
1448
+ const decl = prop?.valueDeclaration ?? prop?.declarations?.[0];
1449
+ return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
1450
+ }
1451
+ /** Resolve an identifier read to the node id of the module-level variable it
1452
+ * names, or undefined if it doesn't resolve to a top-level const/let/var. Unlike
1453
+ * the old batch-local variableIds gate, this checks the *decl kind*, so a
1454
+ * cross-file const still resolves in a single-file reindex (where the batch holds
1455
+ * no other file's Variable nodes) — via declToId or, failing that, by location.
1456
+ * (ama-l6k) */
1457
+ function resolveModuleVarRef(expr, checker, declToId, root) {
1458
+ let symbol = checker.getSymbolAtLocation(expr);
1459
+ if (!symbol)
1460
+ return undefined;
1461
+ if (symbol.flags & ts.SymbolFlags.Alias) {
1462
+ symbol = checker.getAliasedSymbol(symbol);
1463
+ }
1464
+ const decl = symbol.valueDeclaration ?? symbol.declarations?.[0];
1465
+ if (!decl || !isModuleVariableDecl(decl))
1466
+ return undefined;
1467
+ return declToId.get(decl) ?? nodeIdForDecl(decl, root);
1468
+ }
1469
+ /** Resolve a heritage type reference (e.g. the `I` in `implements I`) to a node id. */
1470
+ function resolveHeritage(expr, checker, declToId, root) {
1471
+ let symbol = checker.getSymbolAtLocation(expr);
1472
+ if (!symbol)
1473
+ return undefined;
1474
+ if (symbol.flags & ts.SymbolFlags.Alias) {
1475
+ symbol = checker.getAliasedSymbol(symbol);
1476
+ }
1477
+ // Interfaces are type-only, so they have no valueDeclaration — use declarations.
1478
+ const decl = symbol.declarations?.[0];
1479
+ return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
1480
+ }
1481
+ /** Resolve an imported or re-exported name to its original declaration's node id. */
1482
+ function resolveImport(name, checker, declToId, root) {
1483
+ let symbol = checker.getSymbolAtLocation(name);
1484
+ if (!symbol)
1485
+ return undefined;
1486
+ // Import/re-export bindings are aliases; follow the chain to the real declaration.
1487
+ if (symbol.flags & ts.SymbolFlags.Alias) {
1488
+ symbol = checker.getAliasedSymbol(symbol);
1489
+ }
1490
+ const decl = symbol.valueDeclaration ?? symbol.declarations?.[0];
1491
+ return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
1492
+ }
1493
+ /** Resolve a type reference's name (e.g. the `Foo` in `x: Foo`) to a node id. */
1494
+ function resolveTypeRef(name, checker, declToId, root) {
1495
+ let symbol = checker.getSymbolAtLocation(name);
1496
+ if (!symbol)
1497
+ return undefined;
1498
+ if (symbol.flags & ts.SymbolFlags.Alias) {
1499
+ symbol = checker.getAliasedSymbol(symbol);
1500
+ }
1501
+ // Types are usually type-only (no valueDeclaration), so prefer declarations.
1502
+ const decl = symbol.declarations?.[0] ?? symbol.valueDeclaration;
1503
+ return decl ? (declToId.get(decl) ?? nodeIdForDecl(decl, root)) : undefined;
1504
+ }
1505
+ /**
1506
+ * The graph id a declaration *would* receive from a structural walk, computed
1507
+ * from its location alone. This lets resolution target a node in a file the
1508
+ * current pass never walked — the cross-file case during single-file
1509
+ * re-indexing, where `declToId` only holds the one changed file. Returns
1510
+ * undefined for declarations a walk would not emit (nested functions, library
1511
+ * code outside `root`), so the graph never asserts an edge it cannot back.
1512
+ *
1513
+ * It mirrors {@link visit}'s reachability exactly: a node exists only as a
1514
+ * top-level declaration, or as a member of a top-level class/interface.
1515
+ */
1516
+ function nodeIdForDecl(node, root) {
1517
+ // Module references (namespace imports / star re-exports) target the File node.
1518
+ if (ts.isSourceFile(node)) {
1519
+ const rel = repoRel(root, node.fileName);
1520
+ return rel === undefined ? undefined : fileId(rel);
1521
+ }
1522
+ const self = describe(node);
1523
+ if (!self)
1524
+ return undefined;
1525
+ const rel = repoRel(root, node.getSourceFile().fileName);
1526
+ if (rel === undefined)
1527
+ return undefined;
1528
+ const parent = node.parent;
1529
+ if (parent && ts.isSourceFile(parent)) {
1530
+ return symbolId({ file: rel, qualifiedName: self.name });
1531
+ }
1532
+ if (parent &&
1533
+ (ts.isClassDeclaration(parent) || ts.isInterfaceDeclaration(parent)) &&
1534
+ parent.name &&
1535
+ parent.parent &&
1536
+ ts.isSourceFile(parent.parent)) {
1537
+ return symbolId({ file: rel, qualifiedName: `${parent.name.text}.${self.name}` });
1538
+ }
1539
+ // A top-level `const X = …`: VariableDeclaration -> VariableDeclarationList ->
1540
+ // VariableStatement -> SourceFile. Its node id is the module-scoped name, so a
1541
+ // cross-file const resolves in a single-file reindex too. (ama-l6k)
1542
+ if (isModuleVariableDecl(node)) {
1543
+ return symbolId({ file: rel, qualifiedName: self.name });
1544
+ }
1545
+ return undefined;
1546
+ }
1547
+ /** Whether `node` is a top-level `const`/`let`/`var` declaration (module scope):
1548
+ * VariableDeclaration -> VariableDeclarationList -> VariableStatement ->
1549
+ * SourceFile. (ama-l6k) */
1550
+ function isModuleVariableDecl(node) {
1551
+ return (ts.isVariableDeclaration(node) &&
1552
+ ts.isVariableDeclarationList(node.parent) &&
1553
+ ts.isVariableStatement(node.parent.parent) &&
1554
+ ts.isSourceFile(node.parent.parent.parent));
1555
+ }
1556
+ /** Repo-relative path of an absolute file, or undefined if it falls outside the
1557
+ * indexed tree (a different package, or `node_modules`). */
1558
+ function repoRel(root, fileName) {
1559
+ const rel = path.relative(root, fileName);
1560
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
1561
+ return undefined;
1562
+ if (rel.split(path.sep).includes("node_modules"))
1563
+ return undefined;
1564
+ return rel;
1565
+ }
1566
+ /** Every type reference within a type annotation, including those nested in
1567
+ * arrays, unions, and generic type arguments (so `Map<K, Foo>` yields `Foo`). */
1568
+ function typeReferencesIn(node) {
1569
+ const refs = [];
1570
+ const walk = (n) => {
1571
+ if (ts.isTypeReferenceNode(n))
1572
+ refs.push(n);
1573
+ n.forEachChild(walk);
1574
+ };
1575
+ walk(node);
1576
+ return refs;
1577
+ }
1578
+ function rangeOf(node, sf) {
1579
+ const start = sf.getLineAndCharacterOfPosition(node.getStart(sf));
1580
+ const end = sf.getLineAndCharacterOfPosition(node.getEnd());
1581
+ return { startLine: start.line + 1, endLine: end.line + 1 };
1582
+ }
1583
+ /** Whether a constructor parameter is a *parameter property* — it carries an
1584
+ * accessibility (`public`/`private`/`protected`) or `readonly` modifier, which
1585
+ * makes it a real class member, not just an argument. (ama-259) */
1586
+ function isParameterProperty(param) {
1587
+ const mods = ts.canHaveModifiers(param) ? ts.getModifiers(param) : undefined;
1588
+ return (mods?.some((m) => m.kind === ts.SyntaxKind.PublicKeyword ||
1589
+ m.kind === ts.SyntaxKind.PrivateKeyword ||
1590
+ m.kind === ts.SyntaxKind.ProtectedKeyword ||
1591
+ m.kind === ts.SyntaxKind.ReadonlyKeyword) ?? false);
1592
+ }
1593
+ /** A node's 1-based (line, column) start — for tagging an edge with its source
1594
+ * site (a call/new expression's position). (ama-hft.9) */
1595
+ function locationOf(node) {
1596
+ const sf = node.getSourceFile();
1597
+ const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
1598
+ return { line: line + 1, column: character + 1 };
1599
+ }
1600
+ //# sourceMappingURL=analyzer.js.map