@optave/codegraph 3.12.0 → 3.13.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 (144) hide show
  1. package/README.md +71 -35
  2. package/dist/cli/commands/audit.d.ts.map +1 -1
  3. package/dist/cli/commands/audit.js +2 -1
  4. package/dist/cli/commands/audit.js.map +1 -1
  5. package/dist/cli/commands/batch.d.ts.map +1 -1
  6. package/dist/cli/commands/batch.js +1 -0
  7. package/dist/cli/commands/batch.js.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/commands/build.js +6 -1
  10. package/dist/cli/commands/build.js.map +1 -1
  11. package/dist/cli/commands/config.d.ts +3 -0
  12. package/dist/cli/commands/config.d.ts.map +1 -0
  13. package/dist/cli/commands/config.js +272 -0
  14. package/dist/cli/commands/config.js.map +1 -0
  15. package/dist/cli/commands/triage.js +1 -1
  16. package/dist/cli/commands/triage.js.map +1 -1
  17. package/dist/cli/index.d.ts.map +1 -1
  18. package/dist/cli/index.js +10 -0
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/shared/options.d.ts +2 -1
  21. package/dist/cli/shared/options.d.ts.map +1 -1
  22. package/dist/cli/shared/options.js +11 -1
  23. package/dist/cli/shared/options.js.map +1 -1
  24. package/dist/cli/types.d.ts +2 -0
  25. package/dist/cli/types.d.ts.map +1 -1
  26. package/dist/db/migrations.js +1 -1
  27. package/dist/db/migrations.js.map +1 -1
  28. package/dist/domain/graph/builder/call-resolver.d.ts +12 -8
  29. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  30. package/dist/domain/graph/builder/call-resolver.js +93 -38
  31. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  32. package/dist/domain/graph/builder/cha.d.ts +9 -1
  33. package/dist/domain/graph/builder/cha.d.ts.map +1 -1
  34. package/dist/domain/graph/builder/cha.js +17 -2
  35. package/dist/domain/graph/builder/cha.js.map +1 -1
  36. package/dist/domain/graph/builder/helpers.d.ts +8 -0
  37. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  38. package/dist/domain/graph/builder/helpers.js +22 -3
  39. package/dist/domain/graph/builder/helpers.js.map +1 -1
  40. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/incremental.js +1 -1
  42. package/dist/domain/graph/builder/incremental.js.map +1 -1
  43. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/pipeline.js +37 -2
  45. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  46. package/dist/domain/graph/builder/stages/build-edges.d.ts +0 -2
  47. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  48. package/dist/domain/graph/builder/stages/build-edges.js +88 -318
  49. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  50. package/dist/domain/graph/builder/stages/detect-changes.js +1 -1
  51. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/finalize.js +4 -0
  54. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/stages/native-orchestrator.js +341 -82
  57. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  58. package/dist/domain/graph/builder/stages/resolve-imports.js +1 -1
  59. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  60. package/dist/domain/parser.d.ts +4 -5
  61. package/dist/domain/parser.d.ts.map +1 -1
  62. package/dist/domain/parser.js +46 -15
  63. package/dist/domain/parser.js.map +1 -1
  64. package/dist/domain/wasm-worker-entry.js +10 -2
  65. package/dist/domain/wasm-worker-entry.js.map +1 -1
  66. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  67. package/dist/domain/wasm-worker-pool.js +2 -0
  68. package/dist/domain/wasm-worker-pool.js.map +1 -1
  69. package/dist/domain/wasm-worker-protocol.d.ts +1 -0
  70. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  71. package/dist/extractors/cpp.d.ts.map +1 -1
  72. package/dist/extractors/cpp.js +42 -1
  73. package/dist/extractors/cpp.js.map +1 -1
  74. package/dist/extractors/cuda.d.ts.map +1 -1
  75. package/dist/extractors/cuda.js +42 -1
  76. package/dist/extractors/cuda.js.map +1 -1
  77. package/dist/extractors/helpers.d.ts +11 -0
  78. package/dist/extractors/helpers.d.ts.map +1 -1
  79. package/dist/extractors/helpers.js +40 -0
  80. package/dist/extractors/helpers.js.map +1 -1
  81. package/dist/extractors/java.d.ts.map +1 -1
  82. package/dist/extractors/java.js +8 -7
  83. package/dist/extractors/java.js.map +1 -1
  84. package/dist/extractors/javascript.js +137 -6
  85. package/dist/extractors/javascript.js.map +1 -1
  86. package/dist/features/structure-query.d.ts +1 -1
  87. package/dist/features/structure-query.d.ts.map +1 -1
  88. package/dist/features/structure-query.js +6 -6
  89. package/dist/features/structure-query.js.map +1 -1
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +1 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/infrastructure/config.d.ts +77 -4
  95. package/dist/infrastructure/config.d.ts.map +1 -1
  96. package/dist/infrastructure/config.js +395 -21
  97. package/dist/infrastructure/config.js.map +1 -1
  98. package/dist/infrastructure/registry.d.ts +27 -0
  99. package/dist/infrastructure/registry.d.ts.map +1 -1
  100. package/dist/infrastructure/registry.js +59 -1
  101. package/dist/infrastructure/registry.js.map +1 -1
  102. package/dist/presentation/structure.d.ts +1 -1
  103. package/dist/presentation/structure.d.ts.map +1 -1
  104. package/dist/presentation/structure.js +2 -2
  105. package/dist/presentation/structure.js.map +1 -1
  106. package/dist/types.d.ts +37 -0
  107. package/dist/types.d.ts.map +1 -1
  108. package/grammars/tree-sitter-gleam.wasm +0 -0
  109. package/package.json +7 -8
  110. package/src/cli/commands/audit.ts +2 -1
  111. package/src/cli/commands/batch.ts +1 -0
  112. package/src/cli/commands/build.ts +6 -1
  113. package/src/cli/commands/config.ts +353 -0
  114. package/src/cli/commands/triage.ts +1 -1
  115. package/src/cli/index.ts +10 -0
  116. package/src/cli/shared/options.ts +11 -1
  117. package/src/cli/types.ts +2 -0
  118. package/src/db/migrations.ts +1 -1
  119. package/src/domain/graph/builder/call-resolver.ts +99 -41
  120. package/src/domain/graph/builder/cha.ts +18 -1
  121. package/src/domain/graph/builder/helpers.ts +24 -4
  122. package/src/domain/graph/builder/incremental.ts +1 -0
  123. package/src/domain/graph/builder/pipeline.ts +49 -2
  124. package/src/domain/graph/builder/stages/build-edges.ts +130 -399
  125. package/src/domain/graph/builder/stages/detect-changes.ts +1 -1
  126. package/src/domain/graph/builder/stages/finalize.ts +4 -0
  127. package/src/domain/graph/builder/stages/native-orchestrator.ts +396 -92
  128. package/src/domain/graph/builder/stages/resolve-imports.ts +1 -1
  129. package/src/domain/parser.ts +45 -14
  130. package/src/domain/wasm-worker-entry.ts +10 -2
  131. package/src/domain/wasm-worker-pool.ts +1 -0
  132. package/src/domain/wasm-worker-protocol.ts +1 -0
  133. package/src/extractors/cpp.ts +44 -1
  134. package/src/extractors/cuda.ts +44 -1
  135. package/src/extractors/helpers.ts +43 -0
  136. package/src/extractors/java.ts +8 -7
  137. package/src/extractors/javascript.ts +127 -6
  138. package/src/features/structure-query.ts +7 -7
  139. package/src/index.ts +5 -1
  140. package/src/infrastructure/config.ts +481 -22
  141. package/src/infrastructure/registry.ts +82 -1
  142. package/src/presentation/structure.ts +3 -3
  143. package/src/types.ts +41 -0
  144. package/grammars/tree-sitter-erlang.wasm +0 -0
@@ -12,15 +12,23 @@ import { PROPAGATION_HOP_PENALTY } from '../../../../extractors/javascript.js';
12
12
  import { debug } from '../../../../infrastructure/logger.js';
13
13
  import { loadNative } from '../../../../infrastructure/native.js';
14
14
  import type {
15
+ ArrayCallbackBinding,
16
+ ArrayElemBinding,
15
17
  BetterSqlite3Database,
16
18
  Call,
17
19
  ClassRelation,
18
20
  Definition,
19
21
  ExtractorOutput,
20
22
  FnRefBinding,
23
+ ForOfBinding,
21
24
  Import,
22
25
  NativeAddon,
23
26
  NodeRow,
27
+ ObjectPropBinding,
28
+ ObjectRestParamBinding,
29
+ ParamBinding,
30
+ SpreadArgBinding,
31
+ ThisCallBinding,
24
32
  TypeMapEntry,
25
33
  } from '../../../../types.js';
26
34
  import { computeConfidence } from '../../resolve.js';
@@ -37,7 +45,13 @@ import {
37
45
  import type { ChaContext } from '../cha.js';
38
46
  import { buildChaContext, resolveChaTargets, resolveThisDispatch } from '../cha.js';
39
47
  import type { PipelineContext } from '../context.js';
40
- import { BUILTIN_RECEIVERS, batchInsertEdges, runChaPostPass } from '../helpers.js';
48
+ import {
49
+ BUILTIN_RECEIVERS,
50
+ batchInsertEdges,
51
+ CHA_DISPATCH_PENALTY,
52
+ CHA_TYPED_DISPATCH_CONFIDENCE,
53
+ runChaPostPass,
54
+ } from '../helpers.js';
41
55
  import { getResolved, isBarrelFile, resolveBarrelExportCached } from './resolve-imports.js';
42
56
 
43
57
  // ── Local types ──────────────────────────────────────────────────────────
@@ -61,13 +75,27 @@ interface QueryNodeRow {
61
75
  interface NativeFileEntry {
62
76
  file: string;
63
77
  fileNodeId: number;
64
- definitions: Array<{ name: string; kind: string; line: number; endLine: number | null }>;
78
+ definitions: Array<{
79
+ name: string;
80
+ kind: string;
81
+ line: number;
82
+ endLine: number | null;
83
+ params?: string[];
84
+ }>;
65
85
  calls: Call[];
66
86
  importedNames: Array<{ name: string; file: string }>;
67
87
  classes: ClassRelation[];
68
88
  typeMap: Array<{ name: string; typeName: string; confidence: number }>;
69
89
  /** Phase 8.3: function-reference bindings for pts analysis. */
70
90
  fnRefBindings?: Array<{ lhs: string; rhs: string; rhsReceiver?: string }>;
91
+ paramBindings?: ParamBinding[];
92
+ thisCallBindings?: ThisCallBinding[];
93
+ arrayElemBindings?: ArrayElemBinding[];
94
+ spreadArgBindings?: SpreadArgBinding[];
95
+ forOfBindings?: ForOfBinding[];
96
+ arrayCallbackBindings?: ArrayCallbackBinding[];
97
+ objectRestParamBindings?: ObjectRestParamBinding[];
98
+ objectPropBindings?: ObjectPropBinding[];
71
99
  }
72
100
 
73
101
  /** Shape returned by native buildCallEdges. */
@@ -79,9 +107,6 @@ interface NativeEdge {
79
107
  dynamic: number;
80
108
  }
81
109
 
82
- /** Phase 8.5: confidence penalty applied to CHA-dispatch edges. */
83
- export const CHA_DISPATCH_PENALTY = 0.1;
84
-
85
110
  // ── Node lookup setup ───────────────────────────────────────────────────
86
111
 
87
112
  function makeGetNodeIdStmt(db: BetterSqlite3Database): NodeIdStmt {
@@ -509,17 +534,35 @@ function buildCallEdgesNative(
509
534
  nativeFiles.push({
510
535
  file: relPath,
511
536
  fileNodeId: fileNodeRow.id,
512
- definitions: symbols.definitions.map((d) => ({
513
- name: d.name,
514
- kind: d.kind,
515
- line: d.line,
516
- endLine: d.endLine ?? null,
517
- })),
537
+ definitions: symbols.definitions.map((d) => {
538
+ const params = d.children?.filter((c) => c.kind === 'parameter').map((c) => c.name);
539
+ return {
540
+ name: d.name,
541
+ kind: d.kind,
542
+ line: d.line,
543
+ endLine: d.endLine ?? null,
544
+ params: params?.length ? params : undefined,
545
+ };
546
+ }),
518
547
  calls: symbols.calls,
519
548
  importedNames,
520
549
  classes: symbols.classes,
521
550
  typeMap,
522
551
  fnRefBindings: symbols.fnRefBindings?.length ? symbols.fnRefBindings : undefined,
552
+ paramBindings: symbols.paramBindings?.length ? symbols.paramBindings : undefined,
553
+ thisCallBindings: symbols.thisCallBindings?.length ? symbols.thisCallBindings : undefined,
554
+ arrayElemBindings: symbols.arrayElemBindings?.length ? symbols.arrayElemBindings : undefined,
555
+ spreadArgBindings: symbols.spreadArgBindings?.length ? symbols.spreadArgBindings : undefined,
556
+ forOfBindings: symbols.forOfBindings?.length ? symbols.forOfBindings : undefined,
557
+ arrayCallbackBindings: symbols.arrayCallbackBindings?.length
558
+ ? symbols.arrayCallbackBindings
559
+ : undefined,
560
+ objectRestParamBindings: symbols.objectRestParamBindings?.length
561
+ ? symbols.objectRestParamBindings
562
+ : undefined,
563
+ objectPropBindings: symbols.objectPropBindings?.length
564
+ ? symbols.objectPropBindings
565
+ : undefined,
523
566
  });
524
567
  }
525
568
 
@@ -538,363 +581,6 @@ function buildCallEdgesNative(
538
581
  }
539
582
  }
540
583
 
541
- /**
542
- * Phase 8.3c pts post-pass for the native call-edge path.
543
- *
544
- * The native Rust engine builds call edges without knowledge of paramBindings,
545
- * so `fn()` calls inside higher-order functions are not resolved to their
546
- * concrete targets. This JS post-pass runs after the native edge pass and adds
547
- * only the parameter-flow pts edges that the native engine missed.
548
- *
549
- * To avoid duplicating edges already emitted by the native engine, the current
550
- * allEdgeRows snapshot is used to seed a seenByPair set before processing each
551
- * file.
552
- */
553
- function buildParamFlowPtsPostPass(
554
- ctx: PipelineContext,
555
- getNodeIdStmt: NodeIdStmt,
556
- allEdgeRows: EdgeRowTuple[],
557
- sharedLookup?: CallNodeLookup,
558
- ): void {
559
- // Only process files that actually have paramBindings (avoid useless work).
560
- const filesWithParams = [...ctx.fileSymbols].filter(
561
- ([, symbols]) => symbols.paramBindings && symbols.paramBindings.length > 0,
562
- );
563
- if (filesWithParams.length === 0) return;
564
-
565
- // Seed seenByPair from the existing rows so we don't duplicate native edges.
566
- // This is O(|allEdgeRows|) once per post-pass, which is acceptable.
567
- const seenByPair = new Set<string>();
568
- for (const [srcId, tgtId] of allEdgeRows) {
569
- seenByPair.add(`${srcId}|${tgtId}`);
570
- }
571
-
572
- const { barrelOnlyFiles, rootDir } = ctx;
573
- const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
574
-
575
- for (const [relPath, symbols] of filesWithParams) {
576
- if (barrelOnlyFiles.has(relPath)) continue;
577
- const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
578
- if (!fileNodeRow) continue;
579
-
580
- const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
581
- const typeMap: Map<string, TypeMapEntry | string> = symbols.typeMap || new Map();
582
- const ptsMap = buildPointsToMapForFile(symbols, importedNames);
583
- if (!ptsMap) continue;
584
-
585
- for (const call of symbols.calls) {
586
- if (call.receiver || call.dynamic) continue; // pts post-pass handles only param-flow (non-dynamic)
587
-
588
- const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
589
- const scopedKey = caller.callerName != null ? `${caller.callerName}::${call.name}` : null;
590
- if (!scopedKey || !ptsMap.has(scopedKey)) continue;
591
-
592
- // Only resolve calls that had no direct targets (same guard as buildFileCallEdges).
593
- const { targets } = resolveCallTargets(
594
- lookup,
595
- call,
596
- relPath,
597
- importedNames,
598
- typeMap as Map<string, unknown>,
599
- );
600
- if (targets.length > 0) continue;
601
-
602
- for (const alias of resolveViaPointsTo(scopedKey, ptsMap)) {
603
- const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(
604
- lookup,
605
- { name: alias },
606
- relPath,
607
- importedNames,
608
- typeMap as Map<string, unknown>,
609
- );
610
- for (const t of aliasTargets) {
611
- const edgeKey = `${caller.id}|${t.id}`;
612
- if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
613
- const conf =
614
- computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
615
- if (conf > 0) {
616
- seenByPair.add(edgeKey);
617
- allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
618
- }
619
- }
620
- }
621
- }
622
- }
623
- }
624
- }
625
-
626
- /**
627
- * bind/alias pts post-pass for the native call-edge path.
628
- *
629
- * The native Rust engine has no knowledge of JS-layer fnRefBindings (e.g.
630
- * `const f = fn.bind(ctx)`), so calls to bind-created aliases are not resolved
631
- * to their original function on the native path. This JS post-pass runs after
632
- * the native edge pass and adds only the fnRefBindings-seeded pts edges that the
633
- * native engine missed.
634
- *
635
- * Uses the same seenByPair dedup guard as buildParamFlowPtsPostPass to avoid
636
- * duplicating edges already emitted by the native engine.
637
- */
638
- function buildFnRefBindingsPtsPostPass(
639
- ctx: PipelineContext,
640
- getNodeIdStmt: NodeIdStmt,
641
- allEdgeRows: EdgeRowTuple[],
642
- sharedLookup?: CallNodeLookup,
643
- ): void {
644
- // Only process files that actually have fnRefBindings.
645
- const filesWithBindings = [...ctx.fileSymbols].filter(
646
- ([, symbols]) => symbols.fnRefBindings && symbols.fnRefBindings.length > 0,
647
- );
648
- if (filesWithBindings.length === 0) return;
649
-
650
- // Seed seenByPair from the existing rows so we don't duplicate native edges.
651
- const seenByPair = new Set<string>();
652
- for (const [srcId, tgtId] of allEdgeRows) {
653
- seenByPair.add(`${srcId}|${tgtId}`);
654
- }
655
-
656
- const { barrelOnlyFiles, rootDir } = ctx;
657
- const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
658
-
659
- for (const [relPath, symbols] of filesWithBindings) {
660
- if (barrelOnlyFiles.has(relPath)) continue;
661
- const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
662
- if (!fileNodeRow) continue;
663
-
664
- const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
665
- const typeMap: Map<string, TypeMapEntry | string> = symbols.typeMap || new Map();
666
- const ptsMap = buildPointsToMapForFile(symbols, importedNames);
667
- if (!ptsMap) continue;
668
-
669
- // Only resolve calls whose name is an lhs in fnRefBindings — the same
670
- // narrowed guard used in buildFileCallEdges case (c).
671
- const fnRefBindingLhs = new Set(symbols.fnRefBindings!.map((b) => b.lhs));
672
-
673
- for (const call of symbols.calls) {
674
- if (call.receiver || call.dynamic) continue; // bind aliases are flat-keyed, never dynamic
675
- if (!fnRefBindingLhs.has(call.name)) continue;
676
- if (!ptsMap.has(call.name)) continue;
677
-
678
- const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
679
-
680
- // Only resolve calls that had no direct targets (same guard as buildFileCallEdges).
681
- const { targets } = resolveCallTargets(
682
- lookup,
683
- call,
684
- relPath,
685
- importedNames,
686
- typeMap as Map<string, unknown>,
687
- );
688
- if (targets.length > 0) continue;
689
-
690
- for (const alias of resolveViaPointsTo(call.name, ptsMap)) {
691
- const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(
692
- lookup,
693
- { name: alias },
694
- relPath,
695
- importedNames,
696
- typeMap as Map<string, unknown>,
697
- );
698
- for (const t of aliasTargets) {
699
- const edgeKey = `${caller.id}|${t.id}`;
700
- if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
701
- const conf =
702
- computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
703
- if (conf > 0) {
704
- seenByPair.add(edgeKey);
705
- allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
706
- }
707
- }
708
- }
709
- }
710
- }
711
- }
712
- }
713
-
714
- /**
715
- * this-rebinding post-pass for the native call-edge path.
716
- *
717
- * When `fn.call(namedCtx, ...)` or `fn.apply(namedCtx, ...)` is extracted by the
718
- * WASM layer, `thisCallBindings` records `{ callee: 'fn', thisArg: 'namedCtx' }`.
719
- * The native Rust engine has no knowledge of these bindings, so `this()` calls
720
- * inside `fn` remain unresolved. This JS post-pass adds the missing edges by
721
- * resolving `this()` calls inside each `fn` that has a thisCallBinding.
722
- */
723
- function buildThisCallBindingsPtsPostPass(
724
- ctx: PipelineContext,
725
- getNodeIdStmt: NodeIdStmt,
726
- allEdgeRows: EdgeRowTuple[],
727
- sharedLookup?: CallNodeLookup,
728
- ): void {
729
- const filesWithBindings = [...ctx.fileSymbols].filter(
730
- ([, symbols]) => symbols.thisCallBindings && symbols.thisCallBindings.length > 0,
731
- );
732
- if (filesWithBindings.length === 0) return;
733
-
734
- const seenByPair = new Set<string>();
735
- for (const [srcId, tgtId] of allEdgeRows) {
736
- seenByPair.add(`${srcId}|${tgtId}`);
737
- }
738
-
739
- const { barrelOnlyFiles, rootDir } = ctx;
740
- const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
741
-
742
- for (const [relPath, symbols] of filesWithBindings) {
743
- if (barrelOnlyFiles.has(relPath)) continue;
744
- const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
745
- if (!fileNodeRow) continue;
746
-
747
- const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
748
- const typeMap: Map<string, TypeMapEntry | string> = symbols.typeMap || new Map();
749
- const ptsMap = buildPointsToMapForFile(symbols, importedNames);
750
- if (!ptsMap) continue;
751
-
752
- // Only process calls named 'this' (callee-not-receiver usage)
753
- for (const call of symbols.calls) {
754
- if (call.name !== 'this' || call.receiver) continue;
755
-
756
- const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
757
- if (caller.callerName == null) continue;
758
-
759
- const scopedKey = `${caller.callerName}::this`;
760
- if (!ptsMap.has(scopedKey)) continue;
761
-
762
- for (const alias of resolveViaPointsTo(scopedKey, ptsMap)) {
763
- const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(
764
- lookup,
765
- { name: alias },
766
- relPath,
767
- importedNames,
768
- typeMap as Map<string, unknown>,
769
- );
770
- for (const t of aliasTargets) {
771
- const edgeKey = `${caller.id}|${t.id}`;
772
- if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
773
- const conf =
774
- computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
775
- if (conf > 0) {
776
- seenByPair.add(edgeKey);
777
- allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
778
- }
779
- }
780
- }
781
- }
782
- }
783
- }
784
- }
785
-
786
- /**
787
- * Phase 8.3f post-pass for the native call-edge path.
788
- *
789
- * The native Rust engine builds call edges without knowledge of
790
- * objectRestParamBindings, so `rest.method()` calls inside functions with
791
- * object-destructuring rest parameters are not resolved via the typeMap chain.
792
- * The Rust engine already resolves same-file and directly-imported callees
793
- * (via steps 1–2 of its resolution logic), so this post-pass only adds edges
794
- * that require the typeMap-chain path:
795
- * typeMap[restName] → argName → typeMap[argName.method] → target
796
- *
797
- * Mirrors the seeding in buildCallEdgesJS (Phase 8.3f) to ensure both engine
798
- * paths produce identical results for receiver-typed rest-param calls.
799
- */
800
- function buildObjectRestParamPostPass(
801
- ctx: PipelineContext,
802
- getNodeIdStmt: NodeIdStmt,
803
- allEdgeRows: EdgeRowTuple[],
804
- sharedLookup?: CallNodeLookup,
805
- ): void {
806
- const filesWithRestBindings = [...ctx.fileSymbols].filter(
807
- ([, symbols]) =>
808
- symbols.objectRestParamBindings &&
809
- symbols.objectRestParamBindings.length > 0 &&
810
- symbols.paramBindings &&
811
- symbols.paramBindings.length > 0,
812
- );
813
- if (filesWithRestBindings.length === 0) return;
814
-
815
- const seenByPair = new Set<string>();
816
- for (const [srcId, tgtId] of allEdgeRows) {
817
- seenByPair.add(`${srcId}|${tgtId}`);
818
- }
819
-
820
- const { barrelOnlyFiles, rootDir } = ctx;
821
- const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
822
-
823
- for (const [relPath, symbols] of filesWithRestBindings) {
824
- if (barrelOnlyFiles.has(relPath)) continue;
825
- const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
826
- if (!fileNodeRow) continue;
827
-
828
- const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
829
- const typeMap: Map<string, TypeMapEntry | string> = new Map(
830
- symbols.typeMap instanceof Map ? symbols.typeMap : [],
831
- );
832
-
833
- // Seed typeMap[callee::restName] = { type: argName } for each matching pair.
834
- // Mirrors the seeding in buildCallEdgesJS Phase 8.3f. Keys are scoped by
835
- // callee so two functions with the same rest-param name (e.g. `...rest`) in
836
- // the same file don't collide (#1358).
837
- // When only one callee uses a given rest name, also seed the unscoped key
838
- // as a null-callerName fallback so edges aren't silently dropped if
839
- // findCaller can't identify the enclosing function (#1358).
840
- const restNameCallees = new Map<string, Set<string>>();
841
- for (const orpb of symbols.objectRestParamBindings!) {
842
- if (!restNameCallees.has(orpb.restName)) restNameCallees.set(orpb.restName, new Set());
843
- restNameCallees.get(orpb.restName)!.add(orpb.callee);
844
- }
845
- const restNames = new Set<string>();
846
- for (const orpb of symbols.objectRestParamBindings!) {
847
- for (const pb of symbols.paramBindings!) {
848
- if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) {
849
- const scopedKey = `${orpb.callee}::${orpb.restName}`;
850
- if (!typeMap.has(scopedKey)) {
851
- typeMap.set(scopedKey, { type: pb.argName, confidence: 0.65 });
852
- if (restNameCallees.get(orpb.restName)!.size === 1 && !typeMap.has(orpb.restName)) {
853
- typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
854
- }
855
- }
856
- // restNames tracks every rest-parameter name found, regardless of whether the
857
- // scoped key was already in typeMap. This ensures the post-pass (below) processes
858
- // all calls whose receiver matches a known rest binding — not just those whose
859
- // typeMap entry was seeded in this iteration.
860
- restNames.add(orpb.restName);
861
- }
862
- }
863
- }
864
- if (restNames.size === 0) continue;
865
-
866
- for (const call of symbols.calls) {
867
- // Only process calls whose receiver is a known rest-binding name.
868
- if (!call.receiver || !restNames.has(call.receiver)) continue;
869
-
870
- const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
871
-
872
- // Resolve with the enriched typeMap. callerName is passed so
873
- // resolveByMethodOrGlobal can look up the scoped key callee::restName (#1358).
874
- // seenByPair deduplicates edges the native engine already emitted.
875
- const { targets, importedFrom } = resolveCallTargets(
876
- lookup,
877
- call,
878
- relPath,
879
- importedNames,
880
- typeMap as Map<string, unknown>,
881
- caller.callerName,
882
- );
883
- for (const t of targets) {
884
- const edgeKey = `${caller.id}|${t.id}`;
885
- if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
886
- const conf =
887
- computeConfidence(relPath, t.file, importedFrom ?? null) - PROPAGATION_HOP_PENALTY;
888
- if (conf > 0) {
889
- seenByPair.add(edgeKey);
890
- allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
891
- }
892
- }
893
- }
894
- }
895
- }
896
- }
897
-
898
584
  /**
899
585
  * Object.defineProperty accessor post-pass for the native call-edge path.
900
586
  *
@@ -988,11 +674,11 @@ function buildDefinePropertyPostPass(
988
674
  *
989
675
  * The native Rust engine has no knowledge of the CHA context, so `this.method()`
990
676
  * calls and interface method dispatches are not expanded to their concrete
991
- * implementations. This JS post-pass runs after the native edges (and the pts
992
- * post-pass) and adds only the CHA-resolved edges that the native engine missed.
677
+ * implementations. This JS post-pass runs after the native edges and adds only
678
+ * the CHA-resolved edges that the native engine missed.
993
679
  *
994
- * Like buildParamFlowPtsPostPass, it seeds seenByPair from the current allEdgeRows
995
- * snapshot to avoid duplicating edges the native engine already produced.
680
+ * Seeds seenByPair from the current allEdgeRows snapshot to avoid duplicating
681
+ * edges the native engine already produced.
996
682
  */
997
683
  function buildChaPostPass(
998
684
  ctx: PipelineContext,
@@ -1026,6 +712,7 @@ function buildChaPostPass(
1026
712
 
1027
713
  const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
1028
714
  let chaTargets: ReadonlyArray<{ id: number; file: string }> = [];
715
+ let isTypedReceiverDispatch = false;
1029
716
 
1030
717
  if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
1031
718
  chaTargets = resolveThisDispatch(
@@ -1034,6 +721,7 @@ function buildChaPostPass(
1034
721
  call.receiver,
1035
722
  chaCtx,
1036
723
  lookup,
724
+ relPath,
1037
725
  );
1038
726
  } else {
1039
727
  const typeEntry = typeMap.get(call.receiver);
@@ -1044,16 +732,26 @@ function buildChaPostPass(
1044
732
  : null;
1045
733
  if (typeName) {
1046
734
  chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
735
+ isTypedReceiverDispatch = true;
1047
736
  }
1048
737
  }
1049
738
 
1050
739
  for (const t of chaTargets) {
1051
740
  const edgeKey = `${caller.id}|${t.id}`;
1052
741
  if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
1053
- const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
742
+ // Typed-receiver (interface/CHA) dispatch: use CHA_TYPED_DISPATCH_CONFIDENCE
743
+ // — file proximity is not meaningful for virtual dispatch confidence.
744
+ // this/super dispatch keeps computeConfidence-based proximity scoring to
745
+ // match runPostNativeThisDispatch (native-orchestrator.ts).
746
+ const conf = isTypedReceiverDispatch
747
+ ? CHA_TYPED_DISPATCH_CONFIDENCE
748
+ : computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
1054
749
  if (conf > 0) {
1055
750
  seenByPair.add(edgeKey);
1056
- allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'cha']);
751
+ // Tag super-dispatch edges distinctly so runChaPostPass can exclude them
752
+ // from further CHA expansion (super calls are not virtual dispatch).
753
+ const technique = call.receiver === 'super' ? 'super-dispatch' : 'cha';
754
+ allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, technique]);
1057
755
  }
1058
756
  }
1059
757
  }
@@ -1317,7 +1015,6 @@ function buildFileCallEdges(
1317
1015
  // bind/alias entries, not for every locally-defined function or import that
1318
1016
  // buildPointsToMap seeds with a self-pointing entry.
1319
1017
  const fnRefBindingLhs = new Set(symbols.fnRefBindings?.map((b) => b.lhs) ?? []);
1320
-
1321
1018
  for (const call of symbols.calls) {
1322
1019
  if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
1323
1020
 
@@ -1424,6 +1121,19 @@ function buildFileCallEdges(
1424
1121
  }
1425
1122
  }
1426
1123
 
1124
+ // Sort targets by confidence descending before emitting edges.
1125
+ // For multi-target calls with duplicate (source_id, target_id) pairs the
1126
+ // stored confidence depends on which duplicate is processed last — sorting
1127
+ // here guarantees the highest-confidence target wins on dedup, matching the
1128
+ // native engine's sort_targets_by_confidence call in build_edges.rs.
1129
+ if (targets.length > 1) {
1130
+ targets = [...targets].sort(
1131
+ (a, b) =>
1132
+ computeConfidence(relPath, b.file, importedFrom ?? null) -
1133
+ computeConfidence(relPath, a.file, importedFrom ?? null),
1134
+ );
1135
+ }
1136
+
1427
1137
  for (const t of targets) {
1428
1138
  const edgeKey = `${caller.id}|${t.id}`;
1429
1139
  if (t.id !== caller.id) {
@@ -1505,7 +1215,15 @@ function buildFileCallEdges(
1505
1215
  importedNames,
1506
1216
  typeMap as Map<string, unknown>,
1507
1217
  );
1508
- for (const t of aliasTargets) {
1218
+ const sortedAliasTargets =
1219
+ aliasTargets.length > 1
1220
+ ? [...aliasTargets].sort(
1221
+ (a, b) =>
1222
+ computeConfidence(relPath, b.file, aliasFrom ?? null) -
1223
+ computeConfidence(relPath, a.file, aliasFrom ?? null),
1224
+ )
1225
+ : aliasTargets;
1226
+ for (const t of sortedAliasTargets) {
1509
1227
  const edgeKey = `${caller.id}|${t.id}`;
1510
1228
  if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1511
1229
  const conf =
@@ -1541,7 +1259,15 @@ function buildFileCallEdges(
1541
1259
  importedNames,
1542
1260
  typeMap as Map<string, unknown>,
1543
1261
  );
1544
- for (const t of aliasTargets) {
1262
+ const sortedAliasTargets =
1263
+ aliasTargets.length > 1
1264
+ ? [...aliasTargets].sort(
1265
+ (a, b) =>
1266
+ computeConfidence(relPath, b.file, aliasFrom ?? null) -
1267
+ computeConfidence(relPath, a.file, aliasFrom ?? null),
1268
+ )
1269
+ : aliasTargets;
1270
+ for (const t of sortedAliasTargets) {
1545
1271
  const edgeKey = `${caller.id}|${t.id}`;
1546
1272
  if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1547
1273
  const conf =
@@ -1570,6 +1296,7 @@ function buildFileCallEdges(
1570
1296
  relPath,
1571
1297
  typeMap as Map<string, unknown>,
1572
1298
  seenCallEdges,
1299
+ importedNames,
1573
1300
  );
1574
1301
  if (recv) {
1575
1302
  allEdgeRows.push([recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0, null]);
@@ -1582,6 +1309,7 @@ function buildFileCallEdges(
1582
1309
  // For typed receiver calls: expand to all instantiated concrete implementations.
1583
1310
  if (chaCtx && call.receiver) {
1584
1311
  let chaTargets: ReadonlyArray<{ id: number; file: string }> = [];
1312
+ let isTypedReceiverDispatch = false;
1585
1313
  if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
1586
1314
  chaTargets = resolveThisDispatch(
1587
1315
  call.name,
@@ -1589,6 +1317,7 @@ function buildFileCallEdges(
1589
1317
  call.receiver,
1590
1318
  chaCtx,
1591
1319
  lookup,
1320
+ relPath,
1592
1321
  );
1593
1322
  } else if (!BUILTIN_RECEIVERS.has(call.receiver)) {
1594
1323
  const typeEntry = typeMap.get(call.receiver);
@@ -1599,12 +1328,19 @@ function buildFileCallEdges(
1599
1328
  : null;
1600
1329
  if (typeName) {
1601
1330
  chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
1331
+ isTypedReceiverDispatch = true;
1602
1332
  }
1603
1333
  }
1604
1334
  for (const t of chaTargets) {
1605
1335
  const edgeKey = `${caller.id}|${t.id}`;
1606
1336
  if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1607
- const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
1337
+ // Typed-receiver (interface/CHA) dispatch: use CHA_TYPED_DISPATCH_CONFIDENCE
1338
+ // — file proximity is not meaningful for virtual dispatch confidence.
1339
+ // this/super dispatch keeps computeConfidence-based proximity scoring to
1340
+ // match runPostNativeThisDispatch (native-orchestrator.ts).
1341
+ const conf = isTypedReceiverDispatch
1342
+ ? CHA_TYPED_DISPATCH_CONFIDENCE
1343
+ : computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
1608
1344
  if (conf > 0) {
1609
1345
  seenCallEdges.add(edgeKey);
1610
1346
  allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'cha']);
@@ -1775,7 +1511,7 @@ function reconnectReverseDepEdges(ctx: PipelineContext): void {
1775
1511
  * their import targets. Falls back to loading ALL nodes for full builds or
1776
1512
  * larger incremental changes.
1777
1513
  */
1778
- const NODE_KIND_FILTER_SQL = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant')`;
1514
+ const NODE_KIND_FILTER_SQL = `kind IN ('function','method','class','interface','struct','type','module','enum','trait','record','constant','variable')`;
1779
1515
 
1780
1516
  function loadNodes(ctx: PipelineContext): { rows: QueryNodeRow[]; scoped: boolean } {
1781
1517
  const { db, fileSymbols, isFullBuild, batchResolved } = ctx;
@@ -1858,7 +1594,16 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
1858
1594
  // Enrich typeMap for .ts/.tsx files using the TypeScript compiler API.
1859
1595
  // Runs before call-edge construction so the accurate types are available
1860
1596
  // for method-call resolution. Gated on config so users can opt out.
1861
- if (ctx.config.build.typescriptResolver) {
1597
+ //
1598
+ // Skip for small incremental builds: TypeScript program creation requires
1599
+ // loading the entire tsconfig file list (~700ms startup on the codegraph
1600
+ // corpus), which dominates the 1-file rebuild time. Native engine bypasses
1601
+ // this entirely via the Rust orchestrator; WASM/JS engines need this gate
1602
+ // to match native's effective behaviour on tiny incremental changes.
1603
+ // Mirrors the smallFilesThreshold gates for nativeDb and native call-edges.
1604
+ const isSmallIncremental =
1605
+ !ctx.isFullBuild && ctx.fileSymbols.size <= ctx.config.build.smallFilesThreshold;
1606
+ if (ctx.config.build.typescriptResolver && !isSmallIncremental) {
1862
1607
  await enrichTypeMapWithTsc(ctx.rootDir, ctx.fileSymbols);
1863
1608
  }
1864
1609
 
@@ -1921,26 +1666,12 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
1921
1666
  (ctx.isFullBuild || ctx.fileSymbols.size > ctx.config.build.smallFilesThreshold);
1922
1667
  if (useNativeCallEdges) {
1923
1668
  buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodesBefore, native!);
1924
- // Build the shared lookup once — both pts post-passes use it, avoiding
1925
- // redundant construction of the same context closure.
1669
+ // The native engine receives all pts bindings (paramBindings,
1670
+ // fnRefBindings, thisCallBindings, objectRestParamBindings, …) through
1671
+ // NativeFileEntry and runs the same points-to solver as the JS path, so
1672
+ // no pts post-passes are needed here. Only capabilities that remain
1673
+ // JS-only run as post-passes below.
1926
1674
  const sharedLookup = makeContextLookup(ctx, getNodeIdStmt);
1927
- // Phase 8.3c post-pass: augment native call edges with parameter-flow pts
1928
- // edges. The native Rust engine has no knowledge of paramBindings, so any
1929
- // `fn()` call inside a higher-order function would be missed. This JS pass
1930
- // runs on top of the native edges and adds only the pts-resolved edges that
1931
- // the native engine could not produce.
1932
- buildParamFlowPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1933
- // bind/alias post-pass: augment native call edges with fnRefBindings-seeded
1934
- // pts edges. The native Rust engine has no knowledge of JS fnRefBindings
1935
- // (e.g. `const f = fn.bind(ctx)`), so calls to bind-created aliases are
1936
- // not resolved to their original function on the native path.
1937
- buildFnRefBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1938
- // this-rebinding post-pass: resolve `this()` calls inside functions that
1939
- // were invoked via `.call(namedCtx, ...)` / `.apply(namedCtx, ...)`.
1940
- buildThisCallBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1941
- // Phase 8.3f post-pass: augment native call edges with object rest-param
1942
- // receiver resolution — typeMap[restName] → argName → typeMap[argName.method].
1943
- buildObjectRestParamPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
1944
1675
  // Object.defineProperty accessor post-pass: resolve this-dispatch inside
1945
1676
  // getter/setter functions registered via Object.defineProperty.
1946
1677
  buildDefinePropertyPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);