@optave/codegraph 3.11.2 → 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 (167) 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 +4 -2
  10. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  11. package/dist/domain/graph/builder/call-resolver.js +170 -8
  12. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  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 +73 -1
  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 +926 -26
  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/parser.d.ts +10 -1
  53. package/dist/domain/parser.d.ts.map +1 -1
  54. package/dist/domain/parser.js +39 -7
  55. package/dist/domain/parser.js.map +1 -1
  56. package/dist/domain/wasm-worker-entry.js +25 -0
  57. package/dist/domain/wasm-worker-entry.js.map +1 -1
  58. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  59. package/dist/domain/wasm-worker-pool.js +32 -0
  60. package/dist/domain/wasm-worker-pool.js.map +1 -1
  61. package/dist/domain/wasm-worker-protocol.d.ts +14 -1
  62. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  63. package/dist/extractors/c.js +3 -3
  64. package/dist/extractors/c.js.map +1 -1
  65. package/dist/extractors/clojure.js +1 -1
  66. package/dist/extractors/clojure.js.map +1 -1
  67. package/dist/extractors/cpp.js +3 -3
  68. package/dist/extractors/cpp.js.map +1 -1
  69. package/dist/extractors/csharp.d.ts.map +1 -1
  70. package/dist/extractors/csharp.js +37 -8
  71. package/dist/extractors/csharp.js.map +1 -1
  72. package/dist/extractors/cuda.js +3 -3
  73. package/dist/extractors/cuda.js.map +1 -1
  74. package/dist/extractors/elixir.js +6 -6
  75. package/dist/extractors/elixir.js.map +1 -1
  76. package/dist/extractors/fsharp.js +1 -1
  77. package/dist/extractors/fsharp.js.map +1 -1
  78. package/dist/extractors/go.js +5 -5
  79. package/dist/extractors/go.js.map +1 -1
  80. package/dist/extractors/haskell.js +1 -1
  81. package/dist/extractors/haskell.js.map +1 -1
  82. package/dist/extractors/java.js +2 -2
  83. package/dist/extractors/java.js.map +1 -1
  84. package/dist/extractors/javascript.d.ts +2 -0
  85. package/dist/extractors/javascript.d.ts.map +1 -1
  86. package/dist/extractors/javascript.js +1674 -64
  87. package/dist/extractors/javascript.js.map +1 -1
  88. package/dist/extractors/kotlin.js +5 -5
  89. package/dist/extractors/kotlin.js.map +1 -1
  90. package/dist/extractors/lua.js +1 -1
  91. package/dist/extractors/lua.js.map +1 -1
  92. package/dist/extractors/objc.js +3 -3
  93. package/dist/extractors/objc.js.map +1 -1
  94. package/dist/extractors/ocaml.js +1 -1
  95. package/dist/extractors/ocaml.js.map +1 -1
  96. package/dist/extractors/php.js +2 -2
  97. package/dist/extractors/php.js.map +1 -1
  98. package/dist/extractors/python.js +7 -7
  99. package/dist/extractors/python.js.map +1 -1
  100. package/dist/extractors/ruby.js +2 -2
  101. package/dist/extractors/ruby.js.map +1 -1
  102. package/dist/extractors/scala.js +1 -1
  103. package/dist/extractors/scala.js.map +1 -1
  104. package/dist/extractors/solidity.js +1 -1
  105. package/dist/extractors/solidity.js.map +1 -1
  106. package/dist/extractors/swift.js +4 -4
  107. package/dist/extractors/swift.js.map +1 -1
  108. package/dist/extractors/zig.js +4 -4
  109. package/dist/extractors/zig.js.map +1 -1
  110. package/dist/infrastructure/config.d.ts +10 -0
  111. package/dist/infrastructure/config.d.ts.map +1 -1
  112. package/dist/infrastructure/config.js +15 -0
  113. package/dist/infrastructure/config.js.map +1 -1
  114. package/dist/infrastructure/native.d.ts +11 -0
  115. package/dist/infrastructure/native.d.ts.map +1 -1
  116. package/dist/infrastructure/native.js +78 -5
  117. package/dist/infrastructure/native.js.map +1 -1
  118. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  119. package/dist/presentation/queries-cli/overview.js +5 -0
  120. package/dist/presentation/queries-cli/overview.js.map +1 -1
  121. package/dist/types.d.ts +184 -0
  122. package/dist/types.d.ts.map +1 -1
  123. package/package.json +7 -7
  124. package/src/db/migrations.ts +7 -0
  125. package/src/domain/analysis/module-map.ts +29 -1
  126. package/src/domain/graph/builder/call-resolver.ts +177 -7
  127. package/src/domain/graph/builder/cha.ts +175 -0
  128. package/src/domain/graph/builder/context.ts +3 -0
  129. package/src/domain/graph/builder/helpers.ts +175 -5
  130. package/src/domain/graph/builder/incremental.ts +79 -1
  131. package/src/domain/graph/builder/stages/build-edges.ts +1128 -24
  132. package/src/domain/graph/builder/stages/detect-changes.ts +3 -1
  133. package/src/domain/graph/builder/stages/native-orchestrator.ts +583 -20
  134. package/src/domain/graph/builder/stages/resolve-imports.ts +14 -0
  135. package/src/domain/graph/journal.ts +1 -1
  136. package/src/domain/graph/resolver/points-to.ts +254 -0
  137. package/src/domain/graph/resolver/ts-resolver.ts +536 -0
  138. package/src/domain/parser.ts +43 -5
  139. package/src/domain/wasm-worker-entry.ts +25 -0
  140. package/src/domain/wasm-worker-pool.ts +21 -0
  141. package/src/domain/wasm-worker-protocol.ts +14 -0
  142. package/src/extractors/c.ts +3 -3
  143. package/src/extractors/clojure.ts +1 -1
  144. package/src/extractors/cpp.ts +3 -3
  145. package/src/extractors/csharp.ts +33 -9
  146. package/src/extractors/cuda.ts +3 -3
  147. package/src/extractors/elixir.ts +6 -6
  148. package/src/extractors/fsharp.ts +1 -1
  149. package/src/extractors/go.ts +5 -5
  150. package/src/extractors/haskell.ts +1 -1
  151. package/src/extractors/java.ts +2 -2
  152. package/src/extractors/javascript.ts +1802 -66
  153. package/src/extractors/kotlin.ts +5 -5
  154. package/src/extractors/lua.ts +1 -1
  155. package/src/extractors/objc.ts +3 -3
  156. package/src/extractors/ocaml.ts +1 -1
  157. package/src/extractors/php.ts +2 -2
  158. package/src/extractors/python.ts +7 -7
  159. package/src/extractors/ruby.ts +2 -2
  160. package/src/extractors/scala.ts +1 -1
  161. package/src/extractors/solidity.ts +1 -1
  162. package/src/extractors/swift.ts +4 -4
  163. package/src/extractors/zig.ts +4 -4
  164. package/src/infrastructure/config.ts +15 -0
  165. package/src/infrastructure/native.ts +87 -5
  166. package/src/presentation/queries-cli/overview.ts +15 -1
  167. package/src/types.ts +194 -0
@@ -21,8 +21,11 @@ import { normalizePath } from '../../../../shared/constants.js';
21
21
  import { toErrorMessage } from '../../../../shared/errors.js';
22
22
  import { CODEGRAPH_VERSION } from '../../../../shared/version.js';
23
23
  import { classifyNativeDrops, formatDropExtensionSummary, getInstalledWasmExtensions, NATIVE_SUPPORTED_EXTENSIONS, parseFilesWasmForBackfill, } from '../../../parser.js';
24
- import { batchInsertNodes, collectFiles as collectFilesUtil, fileHash, fileStat, readFileSafe, } from '../helpers.js';
24
+ import { computeConfidence } from '../../resolve.js';
25
+ import { resolveThisDispatch } from '../cha.js';
26
+ import { batchInsertEdges, batchInsertNodes, collectFiles as collectFilesUtil, fileHash, fileStat, readFileSafe, } from '../helpers.js';
25
27
  import { NativeDbProxy } from '../native-db-proxy.js';
28
+ import { CHA_DISPATCH_PENALTY } from './build-edges.js';
26
29
  import { closeNativeDb } from './native-db-lifecycle.js';
27
30
  // ── Native orchestrator helpers ───────────────────────────────────────
28
31
  /** Determine whether the native orchestrator should be skipped. Returns a reason string, or null if it should run. */
@@ -242,8 +245,403 @@ async function runPostNativeAnalysis(ctx, allFileSymbols, changedFiles) {
242
245
  }
243
246
  return timing;
244
247
  }
248
+ /**
249
+ * Phase 8.5: CHA expansion post-pass for the native orchestrator path.
250
+ *
251
+ * The Rust build pipeline resolves typed receiver calls (e.g. `worker.doWork()`
252
+ * where `worker: IWorker`) to the interface method declaration only. This
253
+ * post-pass reads the class hierarchy (via `implements`/`extends` edges) and
254
+ * instantiated types (via `calls` edges to class nodes) from the DB and expands
255
+ * each call to an interface/abstract method to ALL RTA-filtered concrete
256
+ * implementations.
257
+ *
258
+ * Note: `this`/`super` dispatch is handled separately by `runPostNativeThisDispatch`,
259
+ * which WASM-re-parses JS/TS files to obtain raw call site receiver info.
260
+ *
261
+ * Returns the count of newly inserted CHA edges plus the set of files containing
262
+ * the new edges' endpoints, so the caller can scope role re-classification to the
263
+ * nodes whose fan-in/out actually changed. A zero count means no edges were added
264
+ * and role re-classification is unnecessary.
265
+ */
266
+ function runPostNativeCha(db) {
267
+ const affectedFiles = new Set();
268
+ const empty = { newEdgeCount: 0, affectedFiles };
269
+ // Fast guard: no hierarchy edges → no CHA work
270
+ const hasHierarchy = db
271
+ .prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
272
+ .get();
273
+ if (!hasHierarchy)
274
+ return empty;
275
+ // Build implementors map: parent/interface name → [child/implementing class names]
276
+ const hierarchyRows = db
277
+ .prepare(`
278
+ SELECT src.name AS child_name, tgt.name AS parent_name
279
+ FROM edges e
280
+ JOIN nodes src ON e.source_id = src.id
281
+ JOIN nodes tgt ON e.target_id = tgt.id
282
+ WHERE e.kind IN ('extends', 'implements')
283
+ `)
284
+ .all();
285
+ const implementors = new Map();
286
+ for (const row of hierarchyRows) {
287
+ let list = implementors.get(row.parent_name);
288
+ if (!list) {
289
+ list = [];
290
+ implementors.set(row.parent_name, list);
291
+ }
292
+ if (!list.includes(row.child_name))
293
+ list.push(row.child_name);
294
+ }
295
+ if (implementors.size === 0)
296
+ return empty;
297
+ // RTA: collect class names that are actually instantiated via `new X()`.
298
+ // Primary query targets `class`-kind nodes (the canonical schema).
299
+ // Fallback also matches `constructor`/`function`-kind nodes because some native
300
+ // engine versions record constructor calls against those kinds instead of `class`.
301
+ let rtaRows = db
302
+ .prepare(`
303
+ SELECT DISTINCT tgt.name
304
+ FROM edges e
305
+ JOIN nodes tgt ON e.target_id = tgt.id
306
+ WHERE e.kind = 'calls' AND tgt.kind = 'class'
307
+ `)
308
+ .all();
309
+ if (rtaRows.length === 0) {
310
+ // Fallback: try constructor/function-kind nodes for older native engine schemas
311
+ rtaRows = db
312
+ .prepare(`
313
+ SELECT DISTINCT tgt.name
314
+ FROM edges e
315
+ JOIN nodes tgt ON e.target_id = tgt.id
316
+ WHERE e.kind = 'calls' AND tgt.kind IN ('constructor', 'function')
317
+ AND INSTR(tgt.name, '.') = 0
318
+ `)
319
+ .all();
320
+ }
321
+ const instantiated = new Set(rtaRows.map((r) => r.name));
322
+ // noRtaEvidence: true when no constructor-call evidence exists in the DB (e.g. graph
323
+ // built by an older native engine that doesn't emit constructor call edges at all).
324
+ // In that case we skip RTA filtering so interface dispatch still produces edges —
325
+ // all instantiated implementors are admitted rather than silently dropping everything.
326
+ const noRtaEvidence = instantiated.size === 0;
327
+ if (noRtaEvidence) {
328
+ debug('runPostNativeCha: no constructor-call evidence found — proceeding without RTA filter');
329
+ }
330
+ // Find existing call edges targeting qualified methods (e.g., 'IWorker.doWork').
331
+ // Include the caller node's file so confidence can be computed file-pair-aware,
332
+ // matching the WASM path's computeConfidence(callerFile, targetFile, null) - CHA_DISPATCH_PENALTY formula.
333
+ const callToMethods = db
334
+ .prepare(`
335
+ SELECT e.source_id, tgt.name AS method_name, src.file AS caller_file
336
+ FROM edges e
337
+ JOIN nodes tgt ON e.target_id = tgt.id
338
+ JOIN nodes src ON e.source_id = src.id
339
+ WHERE e.kind = 'calls' AND tgt.kind = 'method'
340
+ AND INSTR(tgt.name, '.') > 0
341
+ `)
342
+ .all();
343
+ // Seed seen-pairs only from the source_ids we'll be expanding — avoids loading every
344
+ // call edge in the DB (which would be O(all edges)) for large codebases.
345
+ const seen = new Set();
346
+ if (callToMethods.length > 0) {
347
+ const sourceIds = [...new Set(callToMethods.map((r) => r.source_id))];
348
+ const CHUNK_SIZE = 500;
349
+ for (let i = 0; i < sourceIds.length; i += CHUNK_SIZE) {
350
+ const chunk = sourceIds.slice(i, i + CHUNK_SIZE);
351
+ const placeholders = chunk.map(() => '?').join(',');
352
+ const existingPairs = db
353
+ .prepare(`SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND source_id IN (${placeholders})`)
354
+ .all(...chunk);
355
+ for (const e of existingPairs)
356
+ seen.add(`${e.source_id}|${e.target_id}`);
357
+ }
358
+ }
359
+ // No LIMIT: multiple files can define the same qualified name in a monorepo.
360
+ const findMethodStmt = db.prepare(`SELECT id, file AS method_file FROM nodes WHERE name = ? AND kind = 'method'`);
361
+ const newEdges = [];
362
+ let newEdgeCount = 0;
363
+ for (const { source_id, method_name, caller_file } of callToMethods) {
364
+ const dotIdx = method_name.indexOf('.');
365
+ if (dotIdx === -1)
366
+ continue;
367
+ const typeName = method_name.slice(0, dotIdx);
368
+ const methodSuffix = method_name.slice(dotIdx + 1);
369
+ // BFS over the implementors map — handles multi-level hierarchies where
370
+ // abstract/non-instantiated classes sit between the call-site type and
371
+ // the concrete leaf implementations (issue #1311).
372
+ const bfsQueue = [typeName];
373
+ const bfsVisited = new Set([typeName]);
374
+ while (bfsQueue.length > 0) {
375
+ const current = bfsQueue.shift();
376
+ const children = implementors.get(current);
377
+ if (!children?.length)
378
+ continue;
379
+ for (const cls of children) {
380
+ if (bfsVisited.has(cls))
381
+ continue;
382
+ bfsVisited.add(cls);
383
+ if (noRtaEvidence || instantiated.has(cls)) {
384
+ const qualifiedName = `${cls}.${methodSuffix}`;
385
+ const methodNodes = findMethodStmt.all(qualifiedName);
386
+ for (const methodNode of methodNodes) {
387
+ const key = `${source_id}|${methodNode.id}`;
388
+ if (seen.has(key))
389
+ continue;
390
+ seen.add(key);
391
+ // Compute confidence file-pair-aware (mirrors WASM path: computeConfidence - CHA_DISPATCH_PENALTY)
392
+ // Skip zero-confidence edges to match buildFileCallEdges / buildChaPostPass behaviour.
393
+ const conf = computeConfidence(caller_file ?? '', methodNode.method_file ?? '', null) -
394
+ CHA_DISPATCH_PENALTY;
395
+ if (conf <= 0)
396
+ continue;
397
+ newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha']);
398
+ newEdgeCount++;
399
+ if (caller_file)
400
+ affectedFiles.add(caller_file);
401
+ if (methodNode.method_file)
402
+ affectedFiles.add(methodNode.method_file);
403
+ }
404
+ }
405
+ // Always traverse children — non-instantiated classes may have instantiated subclasses.
406
+ bfsQueue.push(cls);
407
+ }
408
+ }
409
+ }
410
+ if (newEdges.length > 0) {
411
+ db.transaction(() => batchInsertEdges(db, newEdges))();
412
+ }
413
+ return { newEdgeCount, affectedFiles };
414
+ }
415
+ // Extensions where `this`/`super` dispatch can occur (JS/TS family)
416
+ const THIS_DISPATCH_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
417
+ /**
418
+ * Phase 8.5: this/super dispatch post-pass for the native orchestrator path.
419
+ *
420
+ * The Rust build pipeline resolves typed receiver calls but does NOT persist raw
421
+ * unresolved call site receiver info (e.g. `this`, `super`) to the DB. This
422
+ * hybrid post-pass re-parses JS/TS/TSX files via WASM to collect call sites with
423
+ * `this`/`super` receivers, then resolves them through the class hierarchy stored
424
+ * in DB `extends` edges — mirroring what `buildChaPostPass` does on the WASM path.
425
+ *
426
+ * Only runs when `extends` edges exist in the DB; if there is no inheritance
427
+ * hierarchy there is nothing to resolve via `this`/`super` dispatch.
428
+ */
429
+ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild) {
430
+ const t0 = Date.now();
431
+ const targetIds = new Set();
432
+ // Files containing endpoints of newly inserted edges — lets the caller scope
433
+ // role re-classification to the nodes whose fan-in/out actually changed.
434
+ const affectedFiles = new Set();
435
+ // Fast guard: need at least one extends edge for this/super to have meaning
436
+ const hasExtends = db.prepare(`SELECT 1 FROM edges WHERE kind = 'extends' LIMIT 1`).get();
437
+ if (!hasExtends)
438
+ return { elapsedMs: 0, targetIds, affectedFiles };
439
+ // Build parents map: child class → direct parent class (from `extends` edges)
440
+ const parentRows = db
441
+ .prepare(`
442
+ SELECT src.name AS child_name, tgt.name AS parent_name
443
+ FROM edges e
444
+ JOIN nodes src ON e.source_id = src.id
445
+ JOIN nodes tgt ON e.target_id = tgt.id
446
+ WHERE e.kind = 'extends'
447
+ `)
448
+ .all();
449
+ const parents = new Map();
450
+ for (const row of parentRows) {
451
+ if (!parents.has(row.child_name))
452
+ parents.set(row.child_name, row.parent_name);
453
+ }
454
+ if (parents.size === 0)
455
+ return { elapsedMs: 0, targetIds, affectedFiles };
456
+ const chaCtx = {
457
+ implementors: new Map(), // not needed for this/super resolution
458
+ parents,
459
+ instantiatedTypes: new Set(), // not needed for this/super resolution
460
+ };
461
+ // Determine which files to re-parse.
462
+ //
463
+ // On a full build we do NOT re-parse every JS/TS file — that would WASM-parse
464
+ // the entire project on top of the native pass, causing a massive regression
465
+ // (measured: +358% ms/file on codegraph itself). Instead we restrict to files
466
+ // that are part of the class inheritance hierarchy: both subclass files (which
467
+ // contain `super.X()` calls dispatching to a parent) and parent-class files
468
+ // (whose method bodies contain `this.X()` calls that CHA must resolve). Any
469
+ // file not in the hierarchy has no `extends` relationship, so `this`/`super`
470
+ // calls in it either resolve locally (same-class dispatch, already handled by
471
+ // the direct-call edge) or have no class context — and will be skipped by
472
+ // `resolveThisDispatch` anyway.
473
+ let relFiles;
474
+ if (isFullBuild || !changedFiles) {
475
+ const rows = db
476
+ .prepare(`
477
+ SELECT DISTINCT file FROM (
478
+ SELECT src.file AS file
479
+ FROM edges e
480
+ JOIN nodes src ON e.source_id = src.id
481
+ WHERE e.kind = 'extends' AND src.file IS NOT NULL
482
+ UNION
483
+ SELECT tgt.file AS file
484
+ FROM edges e
485
+ JOIN nodes tgt ON e.target_id = tgt.id
486
+ WHERE e.kind = 'extends' AND tgt.file IS NOT NULL
487
+ )
488
+ `)
489
+ .all();
490
+ relFiles = rows
491
+ .map((r) => r.file)
492
+ .filter((f) => THIS_DISPATCH_EXTS.has(path.extname(f).toLowerCase()));
493
+ }
494
+ else {
495
+ // NOTE: Only files explicitly listed in changedFiles are re-parsed.
496
+ // If a parent-class method is replaced (new node ID) but the child file is
497
+ // unchanged, the stale super.method() edge is not refreshed here. A full
498
+ // rebuild (isFullBuild=true) is required to recover in that scenario.
499
+ relFiles = changedFiles.filter((f) => THIS_DISPATCH_EXTS.has(path.extname(f).toLowerCase()));
500
+ }
501
+ if (relFiles.length === 0)
502
+ return { elapsedMs: 0, targetIds, affectedFiles };
503
+ // DB-backed CallNodeLookup — resolveThisDispatch only calls byName()
504
+ const findByNameStmt = db.prepare(`SELECT id, file, kind FROM nodes WHERE name = ?`);
505
+ const lookup = {
506
+ byName: (name) => findByNameStmt.all(name),
507
+ byNameAndFile: (name, file) => findByNameStmt.all(name).filter((n) => n.file === file),
508
+ isBarrel: () => false,
509
+ resolveBarrel: () => null,
510
+ nodeId: () => undefined,
511
+ };
512
+ // Seed seen-pairs from existing call edges on source nodes in our file set
513
+ const seen = new Set();
514
+ const CHUNK = 500;
515
+ for (let i = 0; i < relFiles.length; i += CHUNK) {
516
+ const chunk = relFiles.slice(i, i + CHUNK);
517
+ const ph = chunk.map(() => '?').join(',');
518
+ const rows = db
519
+ .prepare(`SELECT e.source_id, e.target_id
520
+ FROM edges e
521
+ JOIN nodes n ON e.source_id = n.id
522
+ WHERE e.kind = 'calls' AND n.file IN (${ph})`)
523
+ .all(...chunk);
524
+ for (const r of rows)
525
+ seen.add(`${r.source_id}|${r.target_id}`);
526
+ }
527
+ // Find the innermost containing method/function for a call at `line` in `file`.
528
+ // COALESCE maps NULL end_line to a large sentinel so unbounded nodes sort last
529
+ // (SQLite ASC orders NULLs first, so a raw `end_line - line` would pick them first).
530
+ const findCallerByLineStmt = db.prepare(`
531
+ SELECT id, name FROM nodes
532
+ WHERE file = ? AND kind IN ('method', 'function')
533
+ AND line <= ? AND (end_line IS NULL OR end_line >= ?)
534
+ ORDER BY COALESCE(end_line - line, 999999999) ASC
535
+ LIMIT 1
536
+ `);
537
+ // Re-parse the files to obtain raw call sites with receiver info. Only
538
+ // `calls` (with receivers) are consumed here.
539
+ //
540
+ // The native engine is preferred: this pass only runs after a native
541
+ // orchestrator build, so the addon is already loaded and re-parses the
542
+ // hierarchy file set in single-digit milliseconds with the same
543
+ // receiver-annotated call sites as the WASM extractor. Booting the WASM
544
+ // runtime here instead cost ~40–110ms per full build (in-process
545
+ // web-tree-sitter + grammar init dominated) — part of the v3.12.0
546
+ // publish-gate regression. Files the native engine cannot parse (extension
547
+ // outside NATIVE_SUPPORTED_EXTENSIONS, e.g. .mts/.cts) and native parse
548
+ // failures fall back to the WASM backfill path so the sweep stays complete.
549
+ const absFiles = relFiles.map((f) => path.join(rootDir, f));
550
+ const nativeAbs = absFiles.filter((f) => NATIVE_SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()));
551
+ const callsByRel = new Map();
552
+ // Track native-supported files that returned null (per-file parse error) so
553
+ // they can be included in the WASM fallback set below, ensuring no file's
554
+ // this/super call sites are silently discarded.
555
+ const nativeNullFiles = new Set();
556
+ let nativeParsed = false;
557
+ if (nativeAbs.length > 0) {
558
+ const native = loadNative();
559
+ if (native) {
560
+ try {
561
+ const results = native.parseFiles(nativeAbs, rootDir, false, false);
562
+ for (let i = 0; i < results.length; i++) {
563
+ const r = results[i];
564
+ if (!r) {
565
+ // Per-file parse failure — fall back to WASM for this file.
566
+ const abs = nativeAbs[i];
567
+ if (abs)
568
+ nativeNullFiles.add(abs);
569
+ continue;
570
+ }
571
+ callsByRel.set(normalizePath(path.relative(rootDir, r.file)), r.calls ?? []);
572
+ }
573
+ nativeParsed = true;
574
+ }
575
+ catch (e) {
576
+ debug(`this-dispatch native re-parse failed, falling back to WASM: ${toErrorMessage(e)}`);
577
+ }
578
+ }
579
+ }
580
+ // WASM handles: (a) non-native extensions (e.g. .mts/.cts), (b) the entire
581
+ // file list when the native batch threw, and (c) individual files where the
582
+ // native addon returned null (per-file parse error).
583
+ const wasmAbs = nativeParsed
584
+ ? [
585
+ ...absFiles.filter((f) => !NATIVE_SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase())),
586
+ ...nativeNullFiles,
587
+ ]
588
+ : absFiles;
589
+ const wasmResults = wasmAbs.length > 0
590
+ ? await parseFilesWasmForBackfill(wasmAbs, rootDir, { symbolsOnly: true })
591
+ : new Map();
592
+ for (const [relPath, symbols] of wasmResults) {
593
+ callsByRel.set(relPath, symbols.calls ?? []);
594
+ }
595
+ const newEdges = [];
596
+ for (const [relPath, calls] of callsByRel) {
597
+ for (const call of calls) {
598
+ // Only 'this' and 'super' are class-instance receivers in JS/TS.
599
+ // 'self' refers to WindowOrWorkerGlobalScope — not a class instance — so
600
+ // filtering it here prevents spurious dispatch edges from Worker call sites.
601
+ if (call.receiver !== 'this' && call.receiver !== 'super')
602
+ continue;
603
+ const callerRow = findCallerByLineStmt.get(relPath, call.line, call.line);
604
+ if (!callerRow)
605
+ continue;
606
+ const targets = resolveThisDispatch(call.name, callerRow.name, call.receiver, chaCtx, lookup);
607
+ for (const t of targets) {
608
+ const key = `${callerRow.id}|${t.id}`;
609
+ if (seen.has(key))
610
+ continue;
611
+ seen.add(key);
612
+ const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
613
+ if (conf <= 0)
614
+ continue;
615
+ newEdges.push([callerRow.id, t.id, 'calls', conf, 0, 'cha']);
616
+ targetIds.add(t.id);
617
+ affectedFiles.add(relPath);
618
+ if (t.file)
619
+ affectedFiles.add(t.file);
620
+ }
621
+ }
622
+ }
623
+ if (newEdges.length > 0) {
624
+ db.transaction(() => batchInsertEdges(db, newEdges))();
625
+ debug(`this/super dispatch post-pass: inserted ${newEdges.length} edge(s)`);
626
+ }
627
+ // Free WASM parse trees — mirrors the cleanup in backfillNativeDroppedFiles
628
+ for (const [, symbols] of wasmResults) {
629
+ const tree = symbols._tree;
630
+ if (tree && typeof tree.delete === 'function') {
631
+ try {
632
+ tree.delete();
633
+ }
634
+ catch {
635
+ /* ignore cleanup errors */
636
+ }
637
+ }
638
+ symbols._tree = undefined;
639
+ symbols._langId = undefined;
640
+ }
641
+ return { elapsedMs: Date.now() - t0, targetIds, affectedFiles };
642
+ }
245
643
  /** Format timing result from native orchestrator phases + JS post-processing. */
246
- function formatNativeTimingResult(p, structurePatchMs, analysisTiming) {
644
+ function formatNativeTimingResult(p, structurePatchMs, analysisTiming, thisDispatchMs) {
247
645
  return {
248
646
  phases: {
249
647
  setupMs: +(p.setupMs ?? 0).toFixed(1),
@@ -255,6 +653,7 @@ function formatNativeTimingResult(p, structurePatchMs, analysisTiming) {
255
653
  edgesMs: +(p.edgesMs ?? 0).toFixed(1),
256
654
  structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
257
655
  rolesMs: +(p.rolesMs ?? 0).toFixed(1),
656
+ thisDispatchMs: +thisDispatchMs.toFixed(1),
258
657
  astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
259
658
  complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
260
659
  cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
@@ -572,6 +971,41 @@ async function backfillNativeDroppedFiles(ctx, gap) {
572
971
  symbols._langId = undefined;
573
972
  }
574
973
  }
974
+ /**
975
+ * Backfill the `technique` column on `calls` edges written by the native Rust
976
+ * orchestrator, which does not write the column itself.
977
+ *
978
+ * For full builds, all `calls` edges in the DB are new so a global UPDATE is
979
+ * correct. For incremental builds, only changed-file source nodes are updated
980
+ * to avoid overwriting previously-set technique values on unchanged edges.
981
+ */
982
+ function backfillEdgeTechniquesAfterNativeOrchestrator(db, isFullBuild, changedFiles) {
983
+ // Quiet incremental: no files changed → no new edges inserted, nothing to tag.
984
+ // Running the global UPDATE here would mis-tag pre-migration NULL-technique edges
985
+ // from unchanged files as 'ts-native'.
986
+ if (!isFullBuild && changedFiles && changedFiles.length === 0) {
987
+ return;
988
+ }
989
+ if (isFullBuild || !changedFiles) {
990
+ db.prepare("UPDATE edges SET technique = 'ts-native' WHERE kind = 'calls' AND technique IS NULL").run();
991
+ return;
992
+ }
993
+ // Incremental: scope to source nodes whose file is one of the changed files.
994
+ // Chunk to stay within SQLite's SQLITE_LIMIT_VARIABLE_NUMBER (999 on older builds).
995
+ const CHUNK_SIZE = 500;
996
+ const tx = db.transaction(() => {
997
+ for (let i = 0; i < changedFiles.length; i += CHUNK_SIZE) {
998
+ const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
999
+ const placeholders = chunk.map(() => '?').join(',');
1000
+ db.prepare(`UPDATE edges SET technique = 'ts-native'
1001
+ WHERE kind = 'calls' AND technique IS NULL
1002
+ AND source_id IN (
1003
+ SELECT id FROM nodes WHERE file IN (${placeholders})
1004
+ )`).run(...chunk);
1005
+ }
1006
+ });
1007
+ tx();
1008
+ }
575
1009
  /**
576
1010
  * Try the native build orchestrator.
577
1011
  *
@@ -696,10 +1130,13 @@ export async function tryNativeOrchestrator(ctx) {
696
1130
  ctx.opts.complexity !== false ||
697
1131
  ctx.opts.cfg !== false ||
698
1132
  ctx.opts.dataflow !== false);
1133
+ // ── DB handoff ────────────────────────────────────────────────────────────
1134
+ // Ensure a proper better-sqlite3 connection is open before any post-pass that
1135
+ // writes edges (dropped-language backfill, CHA) and before structure/analysis.
1136
+ // When analysis fallback is needed the handoff already happened above; when
1137
+ // neither structure nor analysis is needed the proxy conversion is deferred to
1138
+ // here so CHA and technique-backfill can still write rows.
699
1139
  if (needsStructure || needsAnalysisFallback) {
700
- // When analysis fallback is needed, handoff to better-sqlite3 — the
701
- // analysis engine uses the suspend/resume WAL pattern that requires a
702
- // real better-sqlite3 connection, not the NativeDbProxy.
703
1140
  if (needsAnalysisFallback && ctx.nativeFirstProxy) {
704
1141
  closeNativeDb(ctx, 'pre-analysis-fallback');
705
1142
  ctx.db = openDb(ctx.dbPath);
@@ -707,16 +1144,10 @@ export async function tryNativeOrchestrator(ctx) {
707
1144
  }
708
1145
  else if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
709
1146
  // DB reopen failed — return partial result
710
- return formatNativeTimingResult(p, 0, analysisTiming);
711
- }
712
- const fileSymbols = reconstructFileSymbolsFromDb(ctx);
713
- if (needsStructure) {
714
- structurePatchMs = await runPostNativeStructure(ctx, fileSymbols, !!result.isFullBuild, result.changedFiles);
715
- }
716
- if (needsAnalysisFallback) {
717
- analysisTiming = await runPostNativeAnalysis(ctx, fileSymbols, result.changedFiles);
1147
+ return formatNativeTimingResult(p, 0, analysisTiming, 0);
718
1148
  }
719
1149
  }
1150
+ // ── Edge-writing post-passes (run before structure so roles see full graph) ──
720
1151
  // Engine parity: the native orchestrator silently drops files whose
721
1152
  // Rust extractor/grammar is missing or fails (e.g. HCL, Scala, Swift on
722
1153
  // stale native binaries). WASM handles those — backfill via WASM so both
@@ -741,7 +1172,63 @@ export async function tryNativeOrchestrator(ctx) {
741
1172
  gap.staleRel.length > 0) {
742
1173
  await backfillNativeDroppedFiles(ctx, gap);
743
1174
  }
1175
+ // Phase 8.5: expand CHA call edges (interface dispatch → concrete implementations).
1176
+ // Returns the affected files so role re-classification below can be scoped to
1177
+ // the nodes whose fan-in/out actually changed.
1178
+ //
1179
+ // Function-as-object-property methods (`fn.method = function() {}`) are extracted
1180
+ // natively by the Rust engine (#1432) and resolved in-build by its edge builder, so
1181
+ // no WASM re-parse post-pass is needed for them. `Foo.prototype.bar = fn` likewise.
1182
+ const { newEdgeCount: chaEdgeCount, affectedFiles: chaAffectedFiles } = runPostNativeCha(ctx.db);
1183
+ // Phase 8.5: this/super dispatch — hybrid WASM re-parse to resolve call sites
1184
+ // whose raw receiver info the Rust pipeline does not persist to DB.
1185
+ const { elapsedMs: thisDispatchMs, targetIds: thisDispatchTargetIds, affectedFiles: thisDispatchAffectedFiles, } = await runPostNativeThisDispatch(ctx.db, ctx.rootDir, result.changedFiles, !!result.isFullBuild);
1186
+ // Role re-classification after JS edge-writing post-passes.
1187
+ // The Rust orchestrator classifies roles before these post-passes (CHA,
1188
+ // this-dispatch) add edges, so roles for the edge endpoints are stale.
1189
+ // Scoped to the files containing those endpoints: a new edge only changes
1190
+ // fan-in/out for its own source and target nodes, so re-classifying their
1191
+ // files restores correctness without re-running the classifier over the
1192
+ // whole graph (which cost ~130ms per build on codegraph itself and was a
1193
+ // major part of the v3.12.0 native full-build benchmark regression).
1194
+ if (chaEdgeCount > 0 || thisDispatchTargetIds.size > 0) {
1195
+ const affectedFiles = [...new Set([...chaAffectedFiles, ...thisDispatchAffectedFiles])];
1196
+ // When edges were inserted but all their endpoint nodes have null `file`
1197
+ // columns (rare but possible), affectedFiles stays empty even though
1198
+ // fan-in/out changed. Fall back to full-graph re-classification in that
1199
+ // case — scoped classification with an empty set would be a no-op, leaving
1200
+ // roles stale for those nodes.
1201
+ const scopedFiles = affectedFiles.length > 0 ? affectedFiles : null;
1202
+ try {
1203
+ const { classifyNodeRoles } = (await import('../../../../features/structure.js'));
1204
+ classifyNodeRoles(ctx.db, scopedFiles);
1205
+ debug(scopedFiles
1206
+ ? `Post-pass role re-classification complete (${scopedFiles.length} file(s))`
1207
+ : 'Post-pass role re-classification complete (full graph — null-file endpoints)');
1208
+ }
1209
+ catch (err) {
1210
+ debug(`Post-pass role re-classification failed: ${toErrorMessage(err)}`);
1211
+ }
1212
+ }
1213
+ // Backfill the `technique` column on `calls` edges written by the Rust
1214
+ // orchestrator, which does not write the column. Runs after all edge-writing
1215
+ // phases (including the WASM dropped-language backfill, CHA post-pass, and
1216
+ // this/super dispatch) so every new edge in this build cycle gets a label.
1217
+ backfillEdgeTechniquesAfterNativeOrchestrator(ctx.db, !!result.isFullBuild, result.changedFiles);
1218
+ // ── Structure and analysis fallback (run after edge-writing so roles see full graph) ──
1219
+ // Reconstruct fileSymbols once for both structure and analysis to avoid two
1220
+ // expensive DB scans. The DB handoff above already ensured ctx.db is a proper
1221
+ // better-sqlite3 connection when either flag is set.
1222
+ if (needsStructure || needsAnalysisFallback) {
1223
+ const fileSymbols = reconstructFileSymbolsFromDb(ctx);
1224
+ if (needsStructure) {
1225
+ structurePatchMs = await runPostNativeStructure(ctx, fileSymbols, !!result.isFullBuild, result.changedFiles);
1226
+ }
1227
+ if (needsAnalysisFallback) {
1228
+ analysisTiming = await runPostNativeAnalysis(ctx, fileSymbols, result.changedFiles);
1229
+ }
1230
+ }
744
1231
  closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
745
- return formatNativeTimingResult(p, structurePatchMs, analysisTiming);
1232
+ return formatNativeTimingResult(p, structurePatchMs, analysisTiming, thisDispatchMs);
746
1233
  }
747
1234
  //# sourceMappingURL=native-orchestrator.js.map