@optave/codegraph 3.11.1 → 3.12.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 (176) hide show
  1. package/README.md +8 -8
  2. package/dist/db/migrations.d.ts.map +1 -1
  3. package/dist/db/migrations.js +7 -0
  4. package/dist/db/migrations.js.map +1 -1
  5. package/dist/domain/analysis/module-map.d.ts +2 -0
  6. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  7. package/dist/domain/analysis/module-map.js +24 -2
  8. package/dist/domain/analysis/module-map.js.map +1 -1
  9. package/dist/domain/graph/builder/call-resolver.d.ts +73 -0
  10. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
  11. package/dist/domain/graph/builder/call-resolver.js +292 -0
  12. package/dist/domain/graph/builder/call-resolver.js.map +1 -0
  13. package/dist/domain/graph/builder/cha.d.ts +61 -0
  14. package/dist/domain/graph/builder/cha.d.ts.map +1 -0
  15. package/dist/domain/graph/builder/cha.js +143 -0
  16. package/dist/domain/graph/builder/cha.js.map +1 -0
  17. package/dist/domain/graph/builder/context.d.ts +3 -0
  18. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  19. package/dist/domain/graph/builder/context.js +2 -0
  20. package/dist/domain/graph/builder/context.js.map +1 -1
  21. package/dist/domain/graph/builder/helpers.d.ts +17 -1
  22. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  23. package/dist/domain/graph/builder/helpers.js +159 -5
  24. package/dist/domain/graph/builder/helpers.js.map +1 -1
  25. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  26. package/dist/domain/graph/builder/incremental.js +147 -54
  27. package/dist/domain/graph/builder/incremental.js.map +1 -1
  28. package/dist/domain/graph/builder/stages/build-edges.d.ts +2 -0
  29. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  30. package/dist/domain/graph/builder/stages/build-edges.js +932 -110
  31. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  32. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/stages/detect-changes.js +2 -1
  34. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  35. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  36. package/dist/domain/graph/builder/stages/native-orchestrator.js +501 -14
  37. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  38. package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
  39. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  40. package/dist/domain/graph/builder/stages/resolve-imports.js +9 -0
  41. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  42. package/dist/domain/graph/journal.js +1 -1
  43. package/dist/domain/graph/journal.js.map +1 -1
  44. package/dist/domain/graph/resolver/points-to.d.ts +53 -0
  45. package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
  46. package/dist/domain/graph/resolver/points-to.js +213 -0
  47. package/dist/domain/graph/resolver/points-to.js.map +1 -0
  48. package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
  49. package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
  50. package/dist/domain/graph/resolver/ts-resolver.js +476 -0
  51. package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
  52. package/dist/domain/graph/watcher.d.ts.map +1 -1
  53. package/dist/domain/graph/watcher.js +5 -2
  54. package/dist/domain/graph/watcher.js.map +1 -1
  55. package/dist/domain/parser.d.ts +10 -1
  56. package/dist/domain/parser.d.ts.map +1 -1
  57. package/dist/domain/parser.js +39 -7
  58. package/dist/domain/parser.js.map +1 -1
  59. package/dist/domain/wasm-worker-entry.js +25 -0
  60. package/dist/domain/wasm-worker-entry.js.map +1 -1
  61. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  62. package/dist/domain/wasm-worker-pool.js +32 -0
  63. package/dist/domain/wasm-worker-pool.js.map +1 -1
  64. package/dist/domain/wasm-worker-protocol.d.ts +14 -1
  65. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  66. package/dist/extractors/c.js +3 -3
  67. package/dist/extractors/c.js.map +1 -1
  68. package/dist/extractors/clojure.js +1 -1
  69. package/dist/extractors/clojure.js.map +1 -1
  70. package/dist/extractors/cpp.js +3 -3
  71. package/dist/extractors/cpp.js.map +1 -1
  72. package/dist/extractors/csharp.d.ts.map +1 -1
  73. package/dist/extractors/csharp.js +37 -8
  74. package/dist/extractors/csharp.js.map +1 -1
  75. package/dist/extractors/cuda.js +3 -3
  76. package/dist/extractors/cuda.js.map +1 -1
  77. package/dist/extractors/elixir.js +6 -6
  78. package/dist/extractors/elixir.js.map +1 -1
  79. package/dist/extractors/fsharp.js +1 -1
  80. package/dist/extractors/fsharp.js.map +1 -1
  81. package/dist/extractors/go.js +5 -5
  82. package/dist/extractors/go.js.map +1 -1
  83. package/dist/extractors/haskell.js +1 -1
  84. package/dist/extractors/haskell.js.map +1 -1
  85. package/dist/extractors/java.js +2 -2
  86. package/dist/extractors/java.js.map +1 -1
  87. package/dist/extractors/javascript.d.ts +2 -0
  88. package/dist/extractors/javascript.d.ts.map +1 -1
  89. package/dist/extractors/javascript.js +1674 -64
  90. package/dist/extractors/javascript.js.map +1 -1
  91. package/dist/extractors/kotlin.js +5 -5
  92. package/dist/extractors/kotlin.js.map +1 -1
  93. package/dist/extractors/lua.js +1 -1
  94. package/dist/extractors/lua.js.map +1 -1
  95. package/dist/extractors/objc.js +3 -3
  96. package/dist/extractors/objc.js.map +1 -1
  97. package/dist/extractors/ocaml.js +1 -1
  98. package/dist/extractors/ocaml.js.map +1 -1
  99. package/dist/extractors/php.js +2 -2
  100. package/dist/extractors/php.js.map +1 -1
  101. package/dist/extractors/python.js +7 -7
  102. package/dist/extractors/python.js.map +1 -1
  103. package/dist/extractors/ruby.js +2 -2
  104. package/dist/extractors/ruby.js.map +1 -1
  105. package/dist/extractors/scala.js +1 -1
  106. package/dist/extractors/scala.js.map +1 -1
  107. package/dist/extractors/solidity.js +1 -1
  108. package/dist/extractors/solidity.js.map +1 -1
  109. package/dist/extractors/swift.js +4 -4
  110. package/dist/extractors/swift.js.map +1 -1
  111. package/dist/extractors/zig.js +4 -4
  112. package/dist/extractors/zig.js.map +1 -1
  113. package/dist/features/structure.d.ts.map +1 -1
  114. package/dist/features/structure.js +121 -16
  115. package/dist/features/structure.js.map +1 -1
  116. package/dist/infrastructure/config.d.ts +10 -0
  117. package/dist/infrastructure/config.d.ts.map +1 -1
  118. package/dist/infrastructure/config.js +15 -0
  119. package/dist/infrastructure/config.js.map +1 -1
  120. package/dist/infrastructure/native.d.ts +11 -0
  121. package/dist/infrastructure/native.d.ts.map +1 -1
  122. package/dist/infrastructure/native.js +78 -5
  123. package/dist/infrastructure/native.js.map +1 -1
  124. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  125. package/dist/presentation/queries-cli/overview.js +5 -0
  126. package/dist/presentation/queries-cli/overview.js.map +1 -1
  127. package/dist/types.d.ts +184 -0
  128. package/dist/types.d.ts.map +1 -1
  129. package/grammars/tree-sitter-erlang.wasm +0 -0
  130. package/package.json +9 -9
  131. package/src/db/migrations.ts +7 -0
  132. package/src/domain/analysis/module-map.ts +29 -1
  133. package/src/domain/graph/builder/call-resolver.ts +351 -0
  134. package/src/domain/graph/builder/cha.ts +175 -0
  135. package/src/domain/graph/builder/context.ts +3 -0
  136. package/src/domain/graph/builder/helpers.ts +175 -5
  137. package/src/domain/graph/builder/incremental.ts +186 -66
  138. package/src/domain/graph/builder/stages/build-edges.ts +1146 -146
  139. package/src/domain/graph/builder/stages/detect-changes.ts +3 -1
  140. package/src/domain/graph/builder/stages/native-orchestrator.ts +583 -20
  141. package/src/domain/graph/builder/stages/resolve-imports.ts +14 -0
  142. package/src/domain/graph/journal.ts +1 -1
  143. package/src/domain/graph/resolver/points-to.ts +254 -0
  144. package/src/domain/graph/resolver/ts-resolver.ts +536 -0
  145. package/src/domain/graph/watcher.ts +4 -2
  146. package/src/domain/parser.ts +43 -5
  147. package/src/domain/wasm-worker-entry.ts +25 -0
  148. package/src/domain/wasm-worker-pool.ts +21 -0
  149. package/src/domain/wasm-worker-protocol.ts +14 -0
  150. package/src/extractors/c.ts +3 -3
  151. package/src/extractors/clojure.ts +1 -1
  152. package/src/extractors/cpp.ts +3 -3
  153. package/src/extractors/csharp.ts +33 -9
  154. package/src/extractors/cuda.ts +3 -3
  155. package/src/extractors/elixir.ts +6 -6
  156. package/src/extractors/fsharp.ts +1 -1
  157. package/src/extractors/go.ts +5 -5
  158. package/src/extractors/haskell.ts +1 -1
  159. package/src/extractors/java.ts +2 -2
  160. package/src/extractors/javascript.ts +1802 -66
  161. package/src/extractors/kotlin.ts +5 -5
  162. package/src/extractors/lua.ts +1 -1
  163. package/src/extractors/objc.ts +3 -3
  164. package/src/extractors/ocaml.ts +1 -1
  165. package/src/extractors/php.ts +2 -2
  166. package/src/extractors/python.ts +7 -7
  167. package/src/extractors/ruby.ts +2 -2
  168. package/src/extractors/scala.ts +1 -1
  169. package/src/extractors/solidity.ts +1 -1
  170. package/src/extractors/swift.ts +4 -4
  171. package/src/extractors/zig.ts +4 -4
  172. package/src/features/structure.ts +143 -23
  173. package/src/infrastructure/config.ts +15 -0
  174. package/src/infrastructure/native.ts +87 -5
  175. package/src/presentation/queries-cli/overview.ts +15 -1
  176. package/src/types.ts +194 -0
@@ -0,0 +1,536 @@
1
+ /**
2
+ * TypeScript-native type resolver (Phase 8.1).
3
+ *
4
+ * Runs as a build-time enrichment pass after tree-sitter parsing. Uses the
5
+ * TypeScript compiler API to resolve the actual runtime type of every variable
6
+ * and parameter in .ts/.tsx files, replacing heuristic typeMap entries (0.7–0.9
7
+ * confidence) with compiler-verified ones (1.0).
8
+ *
9
+ * Tree-sitter parses fast; this pass resolves accurately. Together they give
10
+ * codegraph both speed and precision on its primary use case.
11
+ *
12
+ * The `typescript` package is a peer/optional dependency — it is present on any
13
+ * machine that compiles TypeScript but is not bundled with codegraph itself. This
14
+ * module lazy-imports it at runtime; if the import fails the pass is silently
15
+ * skipped so JS-only projects and environments without `typescript` installed are
16
+ * unaffected.
17
+ */
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { debug } from '../../../infrastructure/logger.js';
21
+ import type { CallAssignment, ExtractorOutput, TypeMapEntry } from '../../../types.js';
22
+
23
+ // typescript is not a hard dependency — lazy-load it so JS-only projects
24
+ // and environments without typescript installed work without error.
25
+ type TsModule = typeof import('typescript');
26
+ let _ts: TsModule | null | undefined; // undefined = not yet tried; null = unavailable
27
+
28
+ async function loadTs(): Promise<TsModule | null> {
29
+ if (_ts !== undefined) return _ts;
30
+ try {
31
+ // TypeScript 6+ ships dual CJS/ESM exports; `.default` is the CJS interop
32
+ // namespace and is present and non-null in both TS 5.x and TS 6.x.
33
+ _ts = (await import('typescript')).default as TsModule;
34
+ } catch {
35
+ _ts = null;
36
+ debug('ts-resolver: typescript package not available — skipping TSC type enrichment');
37
+ }
38
+ return _ts;
39
+ }
40
+
41
+ const TS_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);
42
+
43
+ function isTsFile(relPath: string): boolean {
44
+ // Exclude .d.ts declaration files — path.extname('.d.ts') returns '.ts',
45
+ // so we must check the full suffix explicitly.
46
+ return TS_EXTENSIONS.has(path.extname(relPath)) && !relPath.endsWith('.d.ts');
47
+ }
48
+
49
+ // Primitive and built-in type names that don't help call resolution.
50
+ const SKIP_TYPE_NAMES = new Set([
51
+ 'string',
52
+ 'number',
53
+ 'boolean',
54
+ 'any',
55
+ 'unknown',
56
+ 'never',
57
+ 'void',
58
+ 'null',
59
+ 'undefined',
60
+ 'object',
61
+ 'symbol',
62
+ 'bigint',
63
+ 'String',
64
+ 'Number',
65
+ 'Boolean',
66
+ 'Object',
67
+ 'Array',
68
+ 'Promise',
69
+ 'Map',
70
+ 'Set',
71
+ 'WeakMap',
72
+ 'WeakSet',
73
+ 'Error',
74
+ 'Function',
75
+ 'RegExp',
76
+ 'Date',
77
+ ]);
78
+
79
+ /**
80
+ * Enrich the typeMap for every .ts/.tsx file using the TypeScript compiler API.
81
+ *
82
+ * Called from buildEdges before call-edge construction. Only overwrites entries
83
+ * with lower confidence than 1.0 (constructor calls are already exact).
84
+ */
85
+ export async function enrichTypeMapWithTsc(
86
+ rootDir: string,
87
+ fileSymbols: Map<string, ExtractorOutput>,
88
+ ): Promise<void> {
89
+ const tsRelPaths = [...fileSymbols.keys()].filter(isTsFile);
90
+ if (tsRelPaths.length === 0) return;
91
+
92
+ const ts = await loadTs();
93
+ if (!ts) return;
94
+
95
+ const tsconfigPath = findTsconfig(rootDir);
96
+ if (!tsconfigPath) {
97
+ debug('ts-resolver: no tsconfig.json found — skipping TypeScript type enrichment');
98
+ return;
99
+ }
100
+
101
+ const t0 = Date.now();
102
+ const program = createProgram(ts, tsconfigPath);
103
+ if (!program) return;
104
+
105
+ const checker = program.getTypeChecker();
106
+ let enrichedFiles = 0;
107
+ let enrichedEntries = 0;
108
+ let backfilledFiles = 0;
109
+
110
+ for (const relPath of tsRelPaths) {
111
+ const symbols = fileSymbols.get(relPath)!;
112
+ const absPath = path.resolve(rootDir, relPath);
113
+ const sourceFile = program.getSourceFile(absPath);
114
+ if (!sourceFile) continue;
115
+
116
+ const before = symbols.typeMap.size;
117
+ const countBefore = countLowConfidence(symbols.typeMap);
118
+ enrichSourceFile(ts, sourceFile, checker, symbols.typeMap);
119
+ const countAfter = countLowConfidence(symbols.typeMap);
120
+ const gained = countBefore - countAfter + (symbols.typeMap.size - before);
121
+ if (gained > 0) {
122
+ enrichedEntries += gained;
123
+ enrichedFiles++;
124
+ }
125
+
126
+ // Phase 8.2 parity: backfill returnTypeMap and callAssignments for engines
127
+ // (native Rust) that don't populate them during extraction. The JS extractor
128
+ // sets these fields; native leaves them undefined.
129
+ // Guards are intentionally independent so a future extractor that sets one
130
+ // but not the other is handled correctly without silently skipping either.
131
+ let didBackfill = false;
132
+ if (symbols.returnTypeMap === undefined) {
133
+ symbols.returnTypeMap = new Map();
134
+ enrichReturnTypeMap(ts, sourceFile, checker, symbols.returnTypeMap);
135
+ if (symbols.returnTypeMap.size > 0) didBackfill = true;
136
+ }
137
+ if (symbols.callAssignments === undefined) {
138
+ symbols.callAssignments = [];
139
+ enrichCallAssignments(ts, sourceFile, symbols.typeMap, symbols.callAssignments);
140
+ if (symbols.callAssignments.length > 0) didBackfill = true;
141
+ }
142
+ if (didBackfill) backfilledFiles++;
143
+ }
144
+
145
+ debug(
146
+ `ts-resolver: enriched ${enrichedEntries} typeMap entries across ${enrichedFiles} files` +
147
+ (backfilledFiles > 0
148
+ ? `, backfilled returnTypeMap/callAssignments in ${backfilledFiles} files`
149
+ : '') +
150
+ ` in ${Date.now() - t0}ms`,
151
+ );
152
+ }
153
+
154
+ function countLowConfidence(typeMap: Map<string, TypeMapEntry>): number {
155
+ let count = 0;
156
+ for (const entry of typeMap.values()) {
157
+ if (entry.confidence < 1.0) count++;
158
+ }
159
+ return count;
160
+ }
161
+
162
+ /**
163
+ * Walk up from rootDir looking for tsconfig.json (up to 4 levels).
164
+ * Handles monorepo setups where rootDir is a package subdirectory but
165
+ * the tsconfig lives at the repository root.
166
+ */
167
+ function findTsconfig(rootDir: string): string | null {
168
+ let dir = rootDir;
169
+ for (let i = 0; i < 4; i++) {
170
+ const candidate = path.join(dir, 'tsconfig.json');
171
+ if (fs.existsSync(candidate)) return candidate;
172
+ const parent = path.dirname(dir);
173
+ if (parent === dir) break; // reached filesystem root
174
+ dir = parent;
175
+ }
176
+ return null;
177
+ }
178
+
179
+ function createProgram(ts: TsModule, tsconfigPath: string): import('typescript').Program | null {
180
+ try {
181
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
182
+ if (configFile.error) {
183
+ debug(
184
+ `ts-resolver: tsconfig error — ${ts.flattenDiagnosticMessageText(configFile.error.messageText, '\n')}`,
185
+ );
186
+ return null;
187
+ }
188
+
189
+ const parsed = ts.parseJsonConfigFileContent(
190
+ configFile.config,
191
+ ts.sys,
192
+ path.dirname(tsconfigPath),
193
+ );
194
+
195
+ if (parsed.errors.length > 0) {
196
+ for (const err of parsed.errors) {
197
+ debug(
198
+ `ts-resolver: tsconfig parse warning — ${ts.flattenDiagnosticMessageText(err.messageText, '\n')}`,
199
+ );
200
+ }
201
+ }
202
+
203
+ if (parsed.fileNames.length === 0) {
204
+ // Empty fileNames usually means a solution-style tsconfig that only has
205
+ // `references:[]` and no `files`/`include`. In this case ts.createProgram
206
+ // would receive [tsconfigPath] as source — a JSON file — and every
207
+ // subsequent getSourceFile() call for real .ts files returns undefined,
208
+ // producing zero enrichment silently. Warn instead of wasting time.
209
+ debug(
210
+ 'ts-resolver: tsconfig resolved no source files (solution-style tsconfig?) — skipping enrichment',
211
+ );
212
+ return null;
213
+ }
214
+
215
+ return ts.createProgram({
216
+ rootNames: parsed.fileNames,
217
+ options: {
218
+ ...parsed.options,
219
+ noEmit: true,
220
+ skipLibCheck: true,
221
+ },
222
+ });
223
+ } catch (err) {
224
+ debug(`ts-resolver: failed to create TS program — ${err}`);
225
+ return null;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Walk a single SourceFile and update typeMap entries for:
231
+ * - Variable declarations: const/let/var names with inferred or annotated types
232
+ * - Function/method parameters with type annotations
233
+ *
234
+ * Keys are scoped as `<line>:<col>:<name>` to avoid collisions across functions
235
+ * that share parameter names (e.g., two functions both taking `service`). The
236
+ * call-edge resolver looks up by bare name, so we only write bare-name entries
237
+ * when there is no ambiguity (i.e., the name appears exactly once in this file).
238
+ *
239
+ * Entries already at confidence 1.0 (e.g., `new Foo()` from tree-sitter) are
240
+ * left unchanged. New entries from the compiler are added at confidence 1.0.
241
+ */
242
+ function enrichSourceFile(
243
+ ts: TsModule,
244
+ sourceFile: import('typescript').SourceFile,
245
+ checker: import('typescript').TypeChecker,
246
+ typeMap: Map<string, TypeMapEntry>,
247
+ ): void {
248
+ // First pass: collect resolved types keyed by bare identifier name.
249
+ // Track both the short name (for typeMap writes) and the fully-qualified name
250
+ // (module-path-prefixed) for ambiguity detection. Two classes may share the
251
+ // same short name (e.g., `OrderService` from two different modules), and
252
+ // symbol.getName() returns the declared name — not the local alias — so
253
+ // deduplication on short names alone would incorrectly collapse them.
254
+ const nameToEntries = new Map<string, { shortName: string; qualifiedName: string }[]>();
255
+ // Track class property declaration names so we can also seed "this.X" entries.
256
+ const propertyDeclNames = new Set<string>();
257
+
258
+ function visit(node: import('typescript').Node): void {
259
+ let identName: string | null = null;
260
+ let nameNode: import('typescript').Identifier | null = null;
261
+
262
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
263
+ identName = node.name.text;
264
+ nameNode = node.name;
265
+ } else if (ts.isParameter(node) && ts.isIdentifier(node.name)) {
266
+ identName = node.name.text;
267
+ nameNode = node.name;
268
+ } else if (ts.isPropertyDeclaration(node) && ts.isIdentifier(node.name)) {
269
+ // TypeScript class field: `private repo: Repository<User>`
270
+ // Seeds typeMap so `this.repo.method()` can be resolved via receiver type.
271
+ identName = node.name.text;
272
+ nameNode = node.name;
273
+ propertyDeclNames.add(node.name.text);
274
+ }
275
+
276
+ if (identName && nameNode) {
277
+ const resolved = resolveTypeName(ts, nameNode, checker);
278
+ if (resolved) {
279
+ const existing = nameToEntries.get(identName);
280
+ if (existing) {
281
+ existing.push(resolved);
282
+ } else {
283
+ nameToEntries.set(identName, [resolved]);
284
+ }
285
+ }
286
+ }
287
+
288
+ ts.forEachChild(node, visit);
289
+ }
290
+ ts.forEachChild(sourceFile, visit);
291
+
292
+ // Second pass: only write unambiguous entries (single unique qualified type for a name)
293
+ for (const [name, entries] of nameToEntries) {
294
+ const uniqueQualified = [...new Set(entries.map((e) => e.qualifiedName))];
295
+ if (uniqueQualified.length !== 1) continue; // ambiguous across modules — skip
296
+ // entries is non-empty because we only set() on first occurrence and push() after —
297
+ // TypeScript's noUncheckedIndexedAccess can flag [0] access, so assert the type.
298
+ const first = entries[0];
299
+ if (!first) continue;
300
+ const shortName = first.shortName;
301
+ const existing = typeMap.get(name);
302
+ if (!existing || existing.confidence < 1.0) {
303
+ typeMap.set(name, { type: shortName, confidence: 1.0 });
304
+ }
305
+ // For class property declarations, also seed "this.fieldName" so that
306
+ // `this.repo.findById()` call sites resolve to the interface/class type.
307
+ if (propertyDeclNames.has(name)) {
308
+ const thisKey = `this.${name}`;
309
+ const existingThis = typeMap.get(thisKey);
310
+ if (!existingThis || existingThis.confidence < 1.0) {
311
+ typeMap.set(thisKey, { type: shortName, confidence: 1.0 });
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Walk a SourceFile and populate returnTypeMap with compiler-verified return types.
319
+ * Handles function declarations, method declarations, and arrow/function-expression
320
+ * variable initialisers at module scope. Methods are stored as `ClassName.methodName`.
321
+ *
322
+ * Only captures declarations at module scope or directly inside a class body —
323
+ * local functions nested inside method bodies are excluded to avoid spurious
324
+ * cross-file type matches (same guard as enrichSourceFile's "unambiguous names only"
325
+ * heuristic). Recursion stops at function/method body boundaries.
326
+ *
327
+ * Async functions returning Promise<T> are unwrapped: the inner type argument T is
328
+ * used so that async methods receive a returnTypeMap entry just like sync ones.
329
+ */
330
+ function enrichReturnTypeMap(
331
+ ts: TsModule,
332
+ sourceFile: import('typescript').SourceFile,
333
+ checker: import('typescript').TypeChecker,
334
+ returnTypeMap: Map<string, TypeMapEntry>,
335
+ ): void {
336
+ let currentClass: string | null = null;
337
+
338
+ /**
339
+ * Resolve the concrete return type name for a signature, unwrapping
340
+ * Promise<T> so async functions contribute their inner type.
341
+ */
342
+ function resolveReturnTypeName(sig: import('typescript').Signature | undefined): string | null {
343
+ if (!sig) return null;
344
+ try {
345
+ let retType = checker.getReturnTypeOfSignature(sig);
346
+
347
+ // Unwrap Promise<T> → T so async functions get a useful returnTypeMap entry.
348
+ const outerSym = retType.getSymbol() ?? retType.aliasSymbol;
349
+ if (outerSym?.getName() === 'Promise') {
350
+ const args = checker.getTypeArguments(retType as import('typescript').TypeReference);
351
+ if (args.length > 0) retType = args[0]!;
352
+ }
353
+
354
+ const sym = retType.getSymbol() ?? retType.aliasSymbol;
355
+ if (!sym) return null;
356
+ const name = sym.getName();
357
+ if (!name || name === '__type' || name === '__object' || SKIP_TYPE_NAMES.has(name))
358
+ return null;
359
+ return name;
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
364
+
365
+ function writeEntry(fnName: string, sigNode: import('typescript').SignatureDeclaration): void {
366
+ const typeName = resolveReturnTypeName(checker.getSignatureFromDeclaration(sigNode));
367
+ if (typeName) {
368
+ const existing = returnTypeMap.get(fnName);
369
+ if (!existing || existing.confidence < 1.0)
370
+ returnTypeMap.set(fnName, { type: typeName, confidence: 1.0 });
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Visit nodes at the current lexical scope (module level or class body).
376
+ * Does NOT recurse into function/method bodies to avoid capturing local
377
+ * helper functions under bare names.
378
+ */
379
+ function visit(node: import('typescript').Node): void {
380
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
381
+ // Enter class scope: visit direct children (method/property declarations).
382
+ const saved = currentClass;
383
+ currentClass =
384
+ (node as import('typescript').ClassDeclaration | import('typescript').ClassExpression).name
385
+ ?.text ?? null;
386
+ ts.forEachChild(node, visit);
387
+ currentClass = saved;
388
+ return; // class body fully handled — stop here
389
+ }
390
+
391
+ if (ts.isFunctionDeclaration(node) && node.name) {
392
+ // Module-level function declaration: record and stop (no body descent).
393
+ writeEntry(node.name.text, node);
394
+ return;
395
+ }
396
+
397
+ if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name)) {
398
+ // Class method: record as ClassName.methodName and stop.
399
+ const fnName = currentClass ? `${currentClass}.${node.name.text}` : node.name.text;
400
+ writeEntry(fnName, node);
401
+ return;
402
+ }
403
+
404
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
405
+ // Arrow/function-expression assigned to a variable at the current scope.
406
+ // Because we never recurse into function bodies, any VariableDeclaration
407
+ // we see here is guaranteed to be at module scope or inside a class body
408
+ // (not inside a method body), making the bare name safe for cross-file use.
409
+ const init = node.initializer;
410
+ if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
411
+ writeEntry(node.name.text, init);
412
+ }
413
+ return; // variable declaration fully handled — stop here
414
+ }
415
+
416
+ // For all other node kinds (VariableStatement, VariableDeclarationList,
417
+ // ExportDeclaration, etc.) recurse to reach nested function/class/var nodes.
418
+ ts.forEachChild(node, visit);
419
+ }
420
+
421
+ ts.forEachChild(sourceFile, visit);
422
+ }
423
+
424
+ /**
425
+ * Walk a SourceFile and push call assignments (`const x = fn()`) whose variable
426
+ * is not yet in typeMap into callAssignments for cross-file propagation.
427
+ * Phase 8.1 already resolved the common case into typeMap; this captures the rest.
428
+ *
429
+ * Uses the same two-pass "unambiguous names only" strategy as `enrichSourceFile`:
430
+ * collect all candidates first, then only push entries where a given `varName`
431
+ * maps to exactly one distinct `calleeName`. This prevents multiple methods in the
432
+ * same file that each bind a different imported function to a common local name
433
+ * (e.g., `const result = getA()` in one method, `const result = getB()` in
434
+ * another) from both landing in `callAssignments`, which would cause
435
+ * `propagateReturnTypesAcrossFiles` to silently resolve one arbitrarily.
436
+ */
437
+ function enrichCallAssignments(
438
+ ts: TsModule,
439
+ sourceFile: import('typescript').SourceFile,
440
+ typeMap: Map<string, TypeMapEntry>,
441
+ callAssignments: CallAssignment[],
442
+ ): void {
443
+ // First pass: collect all candidates keyed by varName.
444
+ const candidates = new Map<string, CallAssignment[]>();
445
+
446
+ function visit(node: import('typescript').Node): void {
447
+ if (
448
+ ts.isVariableDeclaration(node) &&
449
+ ts.isIdentifier(node.name) &&
450
+ node.initializer &&
451
+ ts.isCallExpression(node.initializer)
452
+ ) {
453
+ const varName = node.name.text;
454
+ if (!typeMap.has(varName)) {
455
+ const call = node.initializer;
456
+ let calleeName: string | null = null;
457
+ let receiverTypeName: string | undefined;
458
+
459
+ if (ts.isIdentifier(call.expression)) {
460
+ calleeName = call.expression.text;
461
+ } else if (ts.isPropertyAccessExpression(call.expression)) {
462
+ calleeName = call.expression.name.text;
463
+ const obj = call.expression.expression;
464
+ if (ts.isIdentifier(obj)) {
465
+ const entry = typeMap.get(obj.text);
466
+ if (entry && typeof entry === 'object') receiverTypeName = entry.type;
467
+ }
468
+ }
469
+
470
+ if (calleeName) {
471
+ const ca: CallAssignment = { varName, calleeName, receiverTypeName };
472
+ const existing = candidates.get(varName);
473
+ if (existing) {
474
+ existing.push(ca);
475
+ } else {
476
+ candidates.set(varName, [ca]);
477
+ }
478
+ }
479
+ }
480
+ }
481
+
482
+ ts.forEachChild(node, visit);
483
+ }
484
+
485
+ ts.forEachChild(sourceFile, visit);
486
+
487
+ // Second pass: only push entries where varName maps to exactly one distinct
488
+ // calleeName. Ambiguous varNames (same name, different callees across scopes)
489
+ // are excluded to avoid silently resolving the wrong type cross-file.
490
+ for (const entries of candidates.values()) {
491
+ const uniqueCallees = new Set(entries.map((e) => e.calleeName));
492
+ if (uniqueCallees.size === 1) {
493
+ callAssignments.push(entries[0] as CallAssignment);
494
+ }
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Ask the type checker for the type of a name node and return both the short
500
+ * declared name and the fully-qualified module-prefixed name. Returns null when
501
+ * the type is a primitive, anonymous, or otherwise not useful for resolution.
502
+ *
503
+ * The fully-qualified name (e.g., `"./legacy/service".OrderService`) is used for
504
+ * ambiguity detection — it distinguishes two classes that share the same short
505
+ * declaration name but come from different modules. The short name is what the
506
+ * call-edge resolver looks up in the typeMap.
507
+ */
508
+ function resolveTypeName(
509
+ ts: TsModule,
510
+ nameNode: import('typescript').Identifier,
511
+ checker: import('typescript').TypeChecker,
512
+ ): { shortName: string; qualifiedName: string } | null {
513
+ try {
514
+ const type = checker.getTypeAtLocation(nameNode);
515
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
516
+ if (!symbol) return null;
517
+ const shortName = symbol.getName();
518
+ if (
519
+ !shortName ||
520
+ shortName === '__type' ||
521
+ shortName === '__object' ||
522
+ SKIP_TYPE_NAMES.has(shortName) ||
523
+ // Skip generic type-parameter symbols (T, E, K, etc.) — they do not
524
+ // correspond to any real class and would overwrite useful lower-confidence
525
+ // heuristic entries, causing call edges to be silently dropped.
526
+ (symbol.flags & (ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeAlias)) !== 0
527
+ )
528
+ return null;
529
+ // getFullyQualifiedName returns e.g. `"./path/to/module".ClassName` for
530
+ // imported symbols — unique across modules even when short names collide.
531
+ const qualifiedName = checker.getFullyQualifiedName(symbol);
532
+ return { shortName, qualifiedName };
533
+ } catch {
534
+ return null;
535
+ }
536
+ }
@@ -35,10 +35,12 @@ function prepareWatcherStatements(db: ReturnType<typeof openDb>): IncrementalStm
35
35
  'SELECT COUNT(*) as c FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
36
36
  ),
37
37
  findNodeInFile: db.prepare(
38
- "SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant') AND file = ?",
38
+ "SELECT id, kind, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant') AND file = ?",
39
39
  ),
40
40
  findNodeByName: db.prepare(
41
- "SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')",
41
+ // `kind` is included so resolveByMethodOrGlobal can filter to 'method' for
42
+ // type-aware receiver resolution (mirrors the full-build resolver).
43
+ "SELECT id, file, kind FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')",
42
44
  ),
43
45
  listSymbols: db.prepare("SELECT name, kind, line FROM nodes WHERE file = ? AND kind != 'file'"),
44
46
  };
@@ -152,8 +152,10 @@ interface WasmExtractResult {
152
152
  // Shared patterns for all JS/TS/TSX (class_declaration excluded — name type differs)
153
153
  const COMMON_QUERY_PATTERNS: string[] = [
154
154
  '(function_declaration name: (identifier) @fn_name) @fn_node',
155
+ '(generator_function_declaration name: (identifier) @fn_name) @fn_node',
155
156
  '(variable_declarator name: (identifier) @varfn_name value: (arrow_function) @varfn_value)',
156
157
  '(variable_declarator name: (identifier) @varfn_name value: (function_expression) @varfn_value)',
158
+ '(variable_declarator name: (identifier) @varfn_name value: (generator_function) @varfn_value)',
157
159
  '(method_definition name: (property_identifier) @meth_name) @meth_node',
158
160
  '(method_definition name: (private_property_identifier) @meth_name) @meth_node',
159
161
  '(import_statement source: (string) @imp_source) @imp_node',
@@ -166,12 +168,20 @@ const COMMON_QUERY_PATTERNS: string[] = [
166
168
  '(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
167
169
  ];
168
170
 
169
- // JS: class name is (identifier)
170
- const JS_CLASS_PATTERN: string = '(class_declaration name: (identifier) @cls_name) @cls_node';
171
+ // JS: class name is (identifier) — declarations and expressions
172
+ const JS_CLASS_PATTERNS: string[] = [
173
+ '(class_declaration name: (identifier) @cls_name) @cls_node',
174
+ // class expressions: `return class Foo extends Bar { ... }` or `const X = class Foo { ... }`
175
+ '(class name: (identifier) @cls_name) @cls_node',
176
+ ];
171
177
 
172
178
  // TS/TSX: class name is (type_identifier), plus interface and type alias
179
+ // abstract_class_declaration is a separate node type in tree-sitter-typescript
173
180
  const TS_EXTRA_PATTERNS: string[] = [
174
181
  '(class_declaration name: (type_identifier) @cls_name) @cls_node',
182
+ '(abstract_class_declaration name: (type_identifier) @cls_name) @cls_node',
183
+ // class expressions: `return class Foo extends Bar { ... }`
184
+ '(class name: (type_identifier) @cls_name) @cls_node',
175
185
  '(interface_declaration name: (type_identifier) @iface_name) @iface_node',
176
186
  '(type_alias_declaration name: (type_identifier) @type_name) @type_node',
177
187
  ];
@@ -202,7 +212,7 @@ async function doLoadLanguage(entry: LanguageRegistryEntry): Promise<void> {
202
212
  const isTS = entry.id === 'typescript' || entry.id === 'tsx';
203
213
  const patterns = isTS
204
214
  ? [...COMMON_QUERY_PATTERNS, ...TS_EXTRA_PATTERNS]
205
- : [...COMMON_QUERY_PATTERNS, JS_CLASS_PATTERN];
215
+ : [...COMMON_QUERY_PATTERNS, ...JS_CLASS_PATTERNS];
206
216
  _queryCache.set(entry.id, new Query(lang, patterns.join('\n')));
207
217
  }
208
218
  } catch (e: unknown) {
@@ -684,6 +694,24 @@ function patchTypeMap(r: any): void {
684
694
  }
685
695
  }
686
696
 
697
+ /** Normalize native returnTypeMap array to a Map instance, keeping highest-confidence entry per key. */
698
+ function patchReturnTypeMap(r: any): void {
699
+ if (!r.returnTypeMap || r.returnTypeMap instanceof Map) return;
700
+ const map = new Map<string, TypeMapEntry>();
701
+ for (const e of r.returnTypeMap as Array<{
702
+ name: string;
703
+ typeName: string;
704
+ confidence?: number;
705
+ }>) {
706
+ const conf = e.confidence ?? 1.0;
707
+ const existing = map.get(e.name);
708
+ if (!existing || conf > existing.confidence) {
709
+ map.set(e.name, { type: e.typeName, confidence: conf });
710
+ }
711
+ }
712
+ r.returnTypeMap = map.size > 0 ? map : new Map();
713
+ }
714
+
687
715
  /** Wrap bindingType into binding object for dataflow argFlows and mutations. */
688
716
  function patchDataflow(dataflow: any): void {
689
717
  if (dataflow.argFlows) {
@@ -706,6 +734,7 @@ function patchNativeResult(r: any): ExtractorOutput {
706
734
  if (r.definitions) patchDefinitions(r.definitions);
707
735
  if (r.imports) patchImports(r.imports);
708
736
  patchTypeMap(r);
737
+ patchReturnTypeMap(r);
709
738
  if (r.dataflow) patchDataflow(r.dataflow);
710
739
 
711
740
  return r;
@@ -1127,6 +1156,7 @@ async function backfillTypeMapBatch(
1127
1156
  async function parseFilesWasm(
1128
1157
  filePaths: string[],
1129
1158
  rootDir: string,
1159
+ analysis: WorkerAnalysisOpts = FULL_ANALYSIS,
1130
1160
  ): Promise<Map<string, ExtractorOutput>> {
1131
1161
  const result = new Map<string, ExtractorOutput>();
1132
1162
  const pool = getWasmWorkerPool();
@@ -1139,7 +1169,7 @@ async function parseFilesWasm(
1139
1169
  warn(`Skipping ${path.relative(rootDir, filePath)}: ${(err as Error).message}`);
1140
1170
  continue;
1141
1171
  }
1142
- const output = await pool.parse(filePath, code, FULL_ANALYSIS);
1172
+ const output = await pool.parse(filePath, code, analysis);
1143
1173
  if (output) {
1144
1174
  const relPath = path.relative(rootDir, filePath).split(path.sep).join('/');
1145
1175
  result.set(relPath, output);
@@ -1202,15 +1232,23 @@ async function parseFilesWasmInline(
1202
1232
  * batches keep the worker-pool isolation against tree-sitter WASM crashes
1203
1233
  * (#965). Threshold matches typical engine-parity drop sizes (a few fixture
1204
1234
  * files in one or two languages).
1235
+ *
1236
+ * `opts.symbolsOnly` skips the AST/complexity/CFG/dataflow visitors in the
1237
+ * worker (and their result serialization across the thread boundary) for
1238
+ * callers that only consume definitions/calls/typeMap — the native
1239
+ * orchestrator's prototype-methods and this-dispatch post-passes. Callers
1240
+ * that ingest the files into the DB (dropped-language backfill) must keep
1241
+ * the default full analysis.
1205
1242
  */
1206
1243
  export async function parseFilesWasmForBackfill(
1207
1244
  filePaths: string[],
1208
1245
  rootDir: string,
1246
+ opts: { symbolsOnly?: boolean } = {},
1209
1247
  ): Promise<Map<string, ExtractorOutput>> {
1210
1248
  if (filePaths.length <= INLINE_BACKFILL_THRESHOLD) {
1211
1249
  return parseFilesWasmInline(filePaths, rootDir);
1212
1250
  }
1213
- return parseFilesWasm(filePaths, rootDir);
1251
+ return parseFilesWasm(filePaths, rootDir, opts.symbolsOnly ? EXTRACT_ONLY : FULL_ANALYSIS);
1214
1252
  }
1215
1253
 
1216
1254
  /**