@optave/codegraph 3.11.2 → 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 (236) hide show
  1. package/README.md +73 -37
  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.d.ts.map +1 -1
  27. package/dist/db/migrations.js +8 -1
  28. package/dist/db/migrations.js.map +1 -1
  29. package/dist/domain/analysis/module-map.d.ts +2 -0
  30. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  31. package/dist/domain/analysis/module-map.js +24 -2
  32. package/dist/domain/analysis/module-map.js.map +1 -1
  33. package/dist/domain/graph/builder/call-resolver.d.ts +16 -10
  34. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
  35. package/dist/domain/graph/builder/call-resolver.js +251 -34
  36. package/dist/domain/graph/builder/call-resolver.js.map +1 -1
  37. package/dist/domain/graph/builder/cha.d.ts +69 -0
  38. package/dist/domain/graph/builder/cha.d.ts.map +1 -0
  39. package/dist/domain/graph/builder/cha.js +158 -0
  40. package/dist/domain/graph/builder/cha.js.map +1 -0
  41. package/dist/domain/graph/builder/context.d.ts +3 -0
  42. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  43. package/dist/domain/graph/builder/context.js +2 -0
  44. package/dist/domain/graph/builder/context.js.map +1 -1
  45. package/dist/domain/graph/builder/helpers.d.ts +25 -1
  46. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/helpers.js +178 -5
  48. package/dist/domain/graph/builder/helpers.js.map +1 -1
  49. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/incremental.js +74 -2
  51. package/dist/domain/graph/builder/incremental.js.map +1 -1
  52. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/pipeline.js +37 -2
  54. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/stages/build-edges.js +704 -34
  57. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  58. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  59. package/dist/domain/graph/builder/stages/detect-changes.js +3 -2
  60. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  61. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  62. package/dist/domain/graph/builder/stages/finalize.js +4 -0
  63. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  64. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  65. package/dist/domain/graph/builder/stages/native-orchestrator.js +783 -37
  66. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  67. package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
  68. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  69. package/dist/domain/graph/builder/stages/resolve-imports.js +10 -1
  70. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  71. package/dist/domain/graph/journal.js +1 -1
  72. package/dist/domain/graph/journal.js.map +1 -1
  73. package/dist/domain/graph/resolver/points-to.d.ts +53 -0
  74. package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
  75. package/dist/domain/graph/resolver/points-to.js +213 -0
  76. package/dist/domain/graph/resolver/points-to.js.map +1 -0
  77. package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
  78. package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
  79. package/dist/domain/graph/resolver/ts-resolver.js +476 -0
  80. package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
  81. package/dist/domain/parser.d.ts +12 -4
  82. package/dist/domain/parser.d.ts.map +1 -1
  83. package/dist/domain/parser.js +83 -20
  84. package/dist/domain/parser.js.map +1 -1
  85. package/dist/domain/wasm-worker-entry.js +35 -2
  86. package/dist/domain/wasm-worker-entry.js.map +1 -1
  87. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  88. package/dist/domain/wasm-worker-pool.js +34 -0
  89. package/dist/domain/wasm-worker-pool.js.map +1 -1
  90. package/dist/domain/wasm-worker-protocol.d.ts +15 -1
  91. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  92. package/dist/extractors/c.js +3 -3
  93. package/dist/extractors/c.js.map +1 -1
  94. package/dist/extractors/clojure.js +1 -1
  95. package/dist/extractors/clojure.js.map +1 -1
  96. package/dist/extractors/cpp.d.ts.map +1 -1
  97. package/dist/extractors/cpp.js +45 -4
  98. package/dist/extractors/cpp.js.map +1 -1
  99. package/dist/extractors/csharp.d.ts.map +1 -1
  100. package/dist/extractors/csharp.js +37 -8
  101. package/dist/extractors/csharp.js.map +1 -1
  102. package/dist/extractors/cuda.d.ts.map +1 -1
  103. package/dist/extractors/cuda.js +45 -4
  104. package/dist/extractors/cuda.js.map +1 -1
  105. package/dist/extractors/elixir.js +6 -6
  106. package/dist/extractors/elixir.js.map +1 -1
  107. package/dist/extractors/fsharp.js +1 -1
  108. package/dist/extractors/fsharp.js.map +1 -1
  109. package/dist/extractors/go.js +5 -5
  110. package/dist/extractors/go.js.map +1 -1
  111. package/dist/extractors/haskell.js +1 -1
  112. package/dist/extractors/haskell.js.map +1 -1
  113. package/dist/extractors/helpers.d.ts +11 -0
  114. package/dist/extractors/helpers.d.ts.map +1 -1
  115. package/dist/extractors/helpers.js +40 -0
  116. package/dist/extractors/helpers.js.map +1 -1
  117. package/dist/extractors/java.d.ts.map +1 -1
  118. package/dist/extractors/java.js +10 -9
  119. package/dist/extractors/java.js.map +1 -1
  120. package/dist/extractors/javascript.d.ts +2 -0
  121. package/dist/extractors/javascript.d.ts.map +1 -1
  122. package/dist/extractors/javascript.js +1812 -71
  123. package/dist/extractors/javascript.js.map +1 -1
  124. package/dist/extractors/kotlin.js +5 -5
  125. package/dist/extractors/kotlin.js.map +1 -1
  126. package/dist/extractors/lua.js +1 -1
  127. package/dist/extractors/lua.js.map +1 -1
  128. package/dist/extractors/objc.js +3 -3
  129. package/dist/extractors/objc.js.map +1 -1
  130. package/dist/extractors/ocaml.js +1 -1
  131. package/dist/extractors/ocaml.js.map +1 -1
  132. package/dist/extractors/php.js +2 -2
  133. package/dist/extractors/php.js.map +1 -1
  134. package/dist/extractors/python.js +7 -7
  135. package/dist/extractors/python.js.map +1 -1
  136. package/dist/extractors/ruby.js +2 -2
  137. package/dist/extractors/ruby.js.map +1 -1
  138. package/dist/extractors/scala.js +1 -1
  139. package/dist/extractors/scala.js.map +1 -1
  140. package/dist/extractors/solidity.js +1 -1
  141. package/dist/extractors/solidity.js.map +1 -1
  142. package/dist/extractors/swift.js +4 -4
  143. package/dist/extractors/swift.js.map +1 -1
  144. package/dist/extractors/zig.js +4 -4
  145. package/dist/extractors/zig.js.map +1 -1
  146. package/dist/features/structure-query.d.ts +1 -1
  147. package/dist/features/structure-query.d.ts.map +1 -1
  148. package/dist/features/structure-query.js +6 -6
  149. package/dist/features/structure-query.js.map +1 -1
  150. package/dist/index.d.ts +1 -1
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.js +1 -1
  153. package/dist/index.js.map +1 -1
  154. package/dist/infrastructure/config.d.ts +85 -2
  155. package/dist/infrastructure/config.d.ts.map +1 -1
  156. package/dist/infrastructure/config.js +408 -19
  157. package/dist/infrastructure/config.js.map +1 -1
  158. package/dist/infrastructure/native.d.ts +11 -0
  159. package/dist/infrastructure/native.d.ts.map +1 -1
  160. package/dist/infrastructure/native.js +78 -5
  161. package/dist/infrastructure/native.js.map +1 -1
  162. package/dist/infrastructure/registry.d.ts +27 -0
  163. package/dist/infrastructure/registry.d.ts.map +1 -1
  164. package/dist/infrastructure/registry.js +59 -1
  165. package/dist/infrastructure/registry.js.map +1 -1
  166. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  167. package/dist/presentation/queries-cli/overview.js +5 -0
  168. package/dist/presentation/queries-cli/overview.js.map +1 -1
  169. package/dist/presentation/structure.d.ts +1 -1
  170. package/dist/presentation/structure.d.ts.map +1 -1
  171. package/dist/presentation/structure.js +2 -2
  172. package/dist/presentation/structure.js.map +1 -1
  173. package/dist/types.d.ts +221 -0
  174. package/dist/types.d.ts.map +1 -1
  175. package/grammars/tree-sitter-gleam.wasm +0 -0
  176. package/package.json +7 -8
  177. package/src/cli/commands/audit.ts +2 -1
  178. package/src/cli/commands/batch.ts +1 -0
  179. package/src/cli/commands/build.ts +6 -1
  180. package/src/cli/commands/config.ts +353 -0
  181. package/src/cli/commands/triage.ts +1 -1
  182. package/src/cli/index.ts +10 -0
  183. package/src/cli/shared/options.ts +11 -1
  184. package/src/cli/types.ts +2 -0
  185. package/src/db/migrations.ts +8 -1
  186. package/src/domain/analysis/module-map.ts +29 -1
  187. package/src/domain/graph/builder/call-resolver.ts +263 -35
  188. package/src/domain/graph/builder/cha.ts +192 -0
  189. package/src/domain/graph/builder/context.ts +3 -0
  190. package/src/domain/graph/builder/helpers.ts +195 -5
  191. package/src/domain/graph/builder/incremental.ts +80 -1
  192. package/src/domain/graph/builder/pipeline.ts +49 -2
  193. package/src/domain/graph/builder/stages/build-edges.ts +867 -32
  194. package/src/domain/graph/builder/stages/detect-changes.ts +4 -2
  195. package/src/domain/graph/builder/stages/finalize.ts +4 -0
  196. package/src/domain/graph/builder/stages/native-orchestrator.ts +910 -43
  197. package/src/domain/graph/builder/stages/resolve-imports.ts +15 -1
  198. package/src/domain/graph/journal.ts +1 -1
  199. package/src/domain/graph/resolver/points-to.ts +254 -0
  200. package/src/domain/graph/resolver/ts-resolver.ts +536 -0
  201. package/src/domain/parser.ts +86 -17
  202. package/src/domain/wasm-worker-entry.ts +35 -2
  203. package/src/domain/wasm-worker-pool.ts +22 -0
  204. package/src/domain/wasm-worker-protocol.ts +15 -0
  205. package/src/extractors/c.ts +3 -3
  206. package/src/extractors/clojure.ts +1 -1
  207. package/src/extractors/cpp.ts +47 -4
  208. package/src/extractors/csharp.ts +33 -9
  209. package/src/extractors/cuda.ts +47 -4
  210. package/src/extractors/elixir.ts +6 -6
  211. package/src/extractors/fsharp.ts +1 -1
  212. package/src/extractors/go.ts +5 -5
  213. package/src/extractors/haskell.ts +1 -1
  214. package/src/extractors/helpers.ts +43 -0
  215. package/src/extractors/java.ts +10 -9
  216. package/src/extractors/javascript.ts +1929 -72
  217. package/src/extractors/kotlin.ts +5 -5
  218. package/src/extractors/lua.ts +1 -1
  219. package/src/extractors/objc.ts +3 -3
  220. package/src/extractors/ocaml.ts +1 -1
  221. package/src/extractors/php.ts +2 -2
  222. package/src/extractors/python.ts +7 -7
  223. package/src/extractors/ruby.ts +2 -2
  224. package/src/extractors/scala.ts +1 -1
  225. package/src/extractors/solidity.ts +1 -1
  226. package/src/extractors/swift.ts +4 -4
  227. package/src/extractors/zig.ts +4 -4
  228. package/src/features/structure-query.ts +7 -7
  229. package/src/index.ts +5 -1
  230. package/src/infrastructure/config.ts +494 -20
  231. package/src/infrastructure/native.ts +87 -5
  232. package/src/infrastructure/registry.ts +82 -1
  233. package/src/presentation/queries-cli/overview.ts +15 -1
  234. package/src/presentation/structure.ts +3 -3
  235. package/src/types.ts +235 -0
  236. package/grammars/tree-sitter-erlang.wasm +0 -0
@@ -7,7 +7,7 @@ import { createHash } from 'node:crypto';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { purgeFilesData } from '../../../db/index.js';
10
- import { warn } from '../../../infrastructure/logger.js';
10
+ import { debug, warn } from '../../../infrastructure/logger.js';
11
11
  import { EXTENSIONS, IGNORE_DIRS, normalizePath } from '../../../shared/constants.js';
12
12
  import { compileGlobs, matchesAny } from '../../../shared/globs.js';
13
13
  import type {
@@ -48,6 +48,13 @@ export const BUILTIN_RECEIVERS: Set<string> = new Set([
48
48
  'require',
49
49
  ]);
50
50
 
51
+ /** Phase 8.6: confidence penalty applied to CHA-dispatch edges. */
52
+ export const CHA_DISPATCH_PENALTY = 0.1;
53
+ /** Phase 8.6: fixed confidence for typed-receiver (interface/CHA) dispatch edges.
54
+ * File proximity is not meaningful for virtual dispatch — all three engine paths
55
+ * (WASM inline, WASM post-pass, native post-pass) must agree on this value. */
56
+ export const CHA_TYPED_DISPATCH_CONFIDENCE = 0.8;
57
+
51
58
  /** Check if a directory entry should be skipped (ignored dirs, dotfiles). */
52
59
  function shouldSkipEntry(entry: fs.Dirent, extraIgnore: Set<string> | null): boolean {
53
60
  if (entry.name.startsWith('.') && entry.name !== '.') {
@@ -313,9 +320,9 @@ function getEdgeStmt(db: BetterSqlite3Database, chunkSize: number): SqliteStatem
313
320
  }
314
321
  let stmt = cache.get(chunkSize);
315
322
  if (!stmt) {
316
- const ph = '(?,?,?,?,?)';
323
+ const ph = '(?,?,?,?,?,?)';
317
324
  stmt = db.prepare(
318
- 'INSERT INTO edges (source_id,target_id,kind,confidence,dynamic) VALUES ' +
325
+ 'INSERT INTO edges (source_id,target_id,kind,confidence,dynamic,technique) VALUES ' +
319
326
  Array.from({ length: chunkSize }, () => ph).join(','),
320
327
  );
321
328
  cache.set(chunkSize, stmt);
@@ -344,7 +351,7 @@ export function batchInsertNodes(db: BetterSqlite3Database, rows: unknown[][]):
344
351
 
345
352
  /**
346
353
  * Batch-insert edge rows via multi-value INSERT statements.
347
- * Each row: [source_id, target_id, kind, confidence, dynamic]
354
+ * Each row: [source_id, target_id, kind, confidence, dynamic, technique]
348
355
  */
349
356
  export function batchInsertEdges(db: BetterSqlite3Database, rows: unknown[][]): void {
350
357
  if (!rows.length) return;
@@ -355,8 +362,191 @@ export function batchInsertEdges(db: BetterSqlite3Database, rows: unknown[][]):
355
362
  const vals: unknown[] = [];
356
363
  for (let j = i; j < end; j++) {
357
364
  const r = rows[j] as unknown[];
358
- vals.push(r[0], r[1], r[2], r[3], r[4]);
365
+ vals.push(r[0], r[1], r[2], r[3], r[4], r[5] ?? null);
359
366
  }
360
367
  stmt.run(...vals);
361
368
  }
362
369
  }
370
+
371
+ /** Confidence assigned to CHA-expanded interface/abstract dispatch edges. */
372
+ export const CHA_DISPATCH_CONFIDENCE = 0.8;
373
+
374
+ /**
375
+ * CHA (Class Hierarchy Analysis) post-pass.
376
+ *
377
+ * Expands virtual-dispatch call edges for class hierarchies and interface
378
+ * implementations already present in the DB:
379
+ *
380
+ * 1. Build implementors map: parent/interface → [child/implementing class] from
381
+ * `extends` and `implements` edges.
382
+ * 2. Collect RTA evidence: class nodes that appear as `calls` targets (new X()).
383
+ * 3. Find all `calls` edges to qualified method nodes (name contains '.').
384
+ * 4. For each such call, expand to concrete overrides via the implementors map,
385
+ * filtered by RTA when evidence exists.
386
+ *
387
+ * Used by both the native orchestrator post-pass and the WASM build-edges pass.
388
+ */
389
+ export function runChaPostPass(db: BetterSqlite3Database): number {
390
+ const hasHierarchy = db
391
+ .prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
392
+ .get();
393
+ if (!hasHierarchy) return 0;
394
+
395
+ const hierarchyRows = db
396
+ .prepare(
397
+ `SELECT src.name AS child_name, tgt.name AS parent_name
398
+ FROM edges e
399
+ JOIN nodes src ON e.source_id = src.id
400
+ JOIN nodes tgt ON e.target_id = tgt.id
401
+ WHERE e.kind IN ('extends', 'implements')`,
402
+ )
403
+ .all() as Array<{ child_name: string; parent_name: string }>;
404
+
405
+ const implementorSets = new Map<string, Set<string>>();
406
+ for (const row of hierarchyRows) {
407
+ let set = implementorSets.get(row.parent_name);
408
+ if (!set) {
409
+ set = new Set<string>();
410
+ implementorSets.set(row.parent_name, set);
411
+ }
412
+ set.add(row.child_name);
413
+ }
414
+ if (implementorSets.size === 0) return 0;
415
+ // Convert to arrays for iteration compatibility with the rest of the function
416
+ const implementors = new Map([...implementorSets.entries()].map(([k, v]) => [k, [...v]]));
417
+
418
+ // RTA: collect class names instantiated via constructor calls (`new X()`).
419
+ let rtaRows = db
420
+ .prepare(
421
+ `SELECT DISTINCT tgt.name
422
+ FROM edges e
423
+ JOIN nodes tgt ON e.target_id = tgt.id
424
+ WHERE e.kind = 'calls' AND tgt.kind = 'class'`,
425
+ )
426
+ .all() as Array<{ name: string }>;
427
+ if (rtaRows.length === 0) {
428
+ // Fallback: some languages (e.g. TypeScript via WASM) record constructor calls as
429
+ // 'function' or 'constructor' kind rather than 'class'. Restrict to names that are
430
+ // actually known class names to avoid treating unrelated function calls like `logger()`
431
+ // as class-instantiation evidence.
432
+ // Include both parent/interface names AND implementor (child) names so that
433
+ // `new UserRepository()` (a child class) is correctly detected as RTA evidence.
434
+ const knownClassNames = [
435
+ ...new Set([
436
+ ...implementorSets.keys(),
437
+ ...[...implementorSets.values()].flatMap((s) => [...s]),
438
+ ]),
439
+ ];
440
+ if (knownClassNames.length > 0) {
441
+ // Chunk to stay within SQLite SQLITE_MAX_VARIABLE_NUMBER (999 in many builds).
442
+ const CHUNK = 999;
443
+ for (let i = 0; i < knownClassNames.length; i += CHUNK) {
444
+ const chunk = knownClassNames.slice(i, i + CHUNK);
445
+ const placeholders = chunk.map(() => '?').join(',');
446
+ const chunkRows = db
447
+ .prepare(
448
+ `SELECT DISTINCT tgt.name
449
+ FROM edges e
450
+ JOIN nodes tgt ON e.target_id = tgt.id
451
+ WHERE e.kind = 'calls' AND tgt.kind IN ('constructor', 'function')
452
+ AND tgt.name IN (${placeholders})`,
453
+ )
454
+ .all(...chunk) as Array<{ name: string }>;
455
+ rtaRows = rtaRows.concat(chunkRows);
456
+ }
457
+ }
458
+ }
459
+ const instantiated = new Set(rtaRows.map((r) => r.name));
460
+ const noRtaEvidence = instantiated.size === 0;
461
+ if (noRtaEvidence) {
462
+ debug('runChaPostPass: no constructor-call evidence — proceeding without RTA filter');
463
+ }
464
+
465
+ const callToMethods = db
466
+ .prepare(
467
+ `SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name
468
+ FROM edges e
469
+ JOIN nodes tgt ON e.target_id = tgt.id
470
+ JOIN nodes src ON e.source_id = src.id
471
+ WHERE e.kind = 'calls' AND tgt.kind = 'method'
472
+ AND INSTR(tgt.name, '.') > 0
473
+ AND (e.technique IS NULL OR e.technique != 'cha-expanded')`,
474
+ )
475
+ .all() as Array<{ source_id: number; caller_name: string; method_name: string }>;
476
+
477
+ const seen = new Set<string>();
478
+ // Scope deduplication to only the source_ids we are about to expand, avoiding
479
+ // a full-table scan. CHA only inserts edges FROM callers that already call a
480
+ // qualified method (the source_ids in callToMethods), so we only need to
481
+ // check existing edges for those specific callers.
482
+ const callerIds = [...new Set(callToMethods.map((r) => r.source_id))];
483
+ if (callerIds.length > 0) {
484
+ // Chunk to stay within SQLite SQLITE_MAX_VARIABLE_NUMBER (999 in many builds).
485
+ const CHUNK = 999;
486
+ for (let i = 0; i < callerIds.length; i += CHUNK) {
487
+ const chunk = callerIds.slice(i, i + CHUNK);
488
+ const placeholders = chunk.map(() => '?').join(',');
489
+ const existingPairs = db
490
+ .prepare(
491
+ `SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND source_id IN (${placeholders})`,
492
+ )
493
+ .all(...chunk) as Array<{ source_id: number; target_id: number }>;
494
+ for (const e of existingPairs) seen.add(`${e.source_id}|${e.target_id}`);
495
+ }
496
+ }
497
+
498
+ // No LIMIT: multiple files can define the same qualified name in a monorepo.
499
+ const findMethodStmt = db.prepare(`SELECT id FROM nodes WHERE name = ? AND kind = 'method'`);
500
+ const newEdges: Array<[number, number, string, number, number, string]> = [];
501
+
502
+ for (const { source_id, method_name } of callToMethods) {
503
+ const dotIdx = method_name.indexOf('.');
504
+ if (dotIdx === -1) continue;
505
+ const typeName = method_name.slice(0, dotIdx);
506
+ const methodSuffix = method_name.slice(dotIdx + 1);
507
+
508
+ // BFS over the implementors map — handles multi-level hierarchies where
509
+ // abstract/non-instantiated classes sit between the call-site type and
510
+ // the concrete leaf implementations (matches runPostNativeCha, issue #1311).
511
+ const bfsQueue: string[] = [typeName];
512
+ const bfsVisited = new Set<string>([typeName]);
513
+ while (bfsQueue.length > 0) {
514
+ const current = bfsQueue.shift()!;
515
+ const children = implementors.get(current);
516
+ if (!children?.length) continue;
517
+
518
+ for (const cls of children) {
519
+ if (bfsVisited.has(cls)) continue;
520
+ bfsVisited.add(cls);
521
+
522
+ if (noRtaEvidence || instantiated.has(cls)) {
523
+ const qualifiedName = `${cls}.${methodSuffix}`;
524
+ const methodNodes = findMethodStmt.all(qualifiedName) as Array<{ id: number }>;
525
+ for (const methodNode of methodNodes) {
526
+ if (methodNode.id === source_id) continue; // skip self-loops
527
+ const key = `${source_id}|${methodNode.id}`;
528
+ if (seen.has(key)) continue;
529
+ seen.add(key);
530
+ newEdges.push([
531
+ source_id,
532
+ methodNode.id,
533
+ 'calls',
534
+ CHA_TYPED_DISPATCH_CONFIDENCE,
535
+ 0,
536
+ 'cha-expanded',
537
+ ]);
538
+ }
539
+ }
540
+
541
+ // Always traverse children — non-instantiated classes may have instantiated subclasses.
542
+ bfsQueue.push(cls);
543
+ }
544
+ }
545
+ }
546
+
547
+ if (newEdges.length > 0) {
548
+ db.transaction(() => batchInsertEdges(db, newEdges))();
549
+ debug(`runChaPostPass: inserted ${newEdges.length} CHA dispatch edge(s)`);
550
+ }
551
+ return newEdges.length;
552
+ }
@@ -502,6 +502,34 @@ function buildCallEdges(
502
502
  ]),
503
503
  )
504
504
  : new Map();
505
+
506
+ // Phase 8.3f: seed typeMap[callee::restName] = { type: argName } from
507
+ // objectRestParamBindings × paramBindings, mirroring buildObjectRestParamPostPass.
508
+ // Scoped keys prevent same-name rest-param collisions when two functions in
509
+ // the same file both use `...rest` (#1358). The unscoped key is also seeded
510
+ // when only one callee uses a given rest name, preserving resolution when
511
+ // callerName is null (findCaller couldn't identify the enclosing function).
512
+ if (symbols.objectRestParamBindings?.length && symbols.paramBindings?.length) {
513
+ const restNameCallees = new Map<string, Set<string>>();
514
+ for (const orpb of symbols.objectRestParamBindings) {
515
+ if (!restNameCallees.has(orpb.restName)) restNameCallees.set(orpb.restName, new Set());
516
+ restNameCallees.get(orpb.restName)!.add(orpb.callee);
517
+ }
518
+ for (const orpb of symbols.objectRestParamBindings) {
519
+ for (const pb of symbols.paramBindings) {
520
+ if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) {
521
+ const scopedKey = `${orpb.callee}::${orpb.restName}`;
522
+ if (!typeMap.has(scopedKey)) {
523
+ typeMap.set(scopedKey, { type: pb.argName, confidence: 0.65 });
524
+ if (restNameCallees.get(orpb.restName)!.size === 1 && !typeMap.has(orpb.restName)) {
525
+ typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
526
+ }
527
+ }
528
+ }
529
+ }
530
+ }
531
+ }
532
+
505
533
  const seenCallEdges = new Set<string>();
506
534
  const lookup = makeIncrementalLookup(db, stmts);
507
535
  let edgesAdded = 0;
@@ -510,13 +538,63 @@ function buildCallEdges(
510
538
  if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
511
539
 
512
540
  const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
513
- const { targets, importedFrom } = resolveCallTargets(
541
+ const { targets: initialTargets, importedFrom } = resolveCallTargets(
514
542
  lookup,
515
543
  call,
516
544
  relPath,
517
545
  importedNames,
518
546
  typeMap,
547
+ caller.callerName,
519
548
  );
549
+ let targets = initialTargets;
550
+
551
+ if (targets.length === 0 && call.receiver === 'this' && caller.callerName != null) {
552
+ const dotIdx = caller.callerName.indexOf('.');
553
+ if (dotIdx > 0) {
554
+ const className = caller.callerName.slice(0, dotIdx);
555
+ const qualifiedName = `${className}.${call.name}`;
556
+ const qualified = lookup
557
+ .byNameAndFile(qualifiedName, relPath)
558
+ .filter((n) => n.kind === 'method');
559
+ if (qualified.length > 0) {
560
+ targets = qualified;
561
+ }
562
+ }
563
+ }
564
+
565
+ if (
566
+ targets.length === 0 &&
567
+ call.receiver === 'this' &&
568
+ caller.callerName != null &&
569
+ symbols.definePropertyReceivers
570
+ ) {
571
+ const receiverVarName = symbols.definePropertyReceivers.get(caller.callerName);
572
+ if (receiverVarName) {
573
+ const typeEntry = typeMap.get(receiverVarName);
574
+ const typeName = typeEntry
575
+ ? typeof typeEntry === 'string'
576
+ ? typeEntry
577
+ : (typeEntry as { type?: string }).type
578
+ : null;
579
+ if (typeName) {
580
+ const qualifiedName = `${typeName}.${call.name}`;
581
+ const qualified = lookup.byNameAndFile(qualifiedName, relPath);
582
+ if (qualified.length > 0) {
583
+ targets = [...qualified];
584
+ }
585
+ }
586
+ if (targets.length === 0) {
587
+ // Narrow to function/method kinds only to avoid matching unrelated
588
+ // variables or classes that share a name in the same file.
589
+ const sameFile = lookup
590
+ .byNameAndFile(call.name, relPath)
591
+ .filter((n) => n.kind === 'function' || n.kind === 'method');
592
+ if (sameFile.length > 0) {
593
+ targets = [...sameFile];
594
+ }
595
+ }
596
+ }
597
+ }
520
598
 
521
599
  for (const t of targets) {
522
600
  const edgeKey = `${caller.id}|${t.id}`;
@@ -542,6 +620,7 @@ function buildCallEdges(
542
620
  relPath,
543
621
  typeMap,
544
622
  seenCallEdges,
623
+ importedNames,
545
624
  );
546
625
  if (recv) {
547
626
  stmts.insertEdge.run(recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0);
@@ -15,7 +15,14 @@ import {
15
15
  MIGRATIONS,
16
16
  openDb,
17
17
  } from '../../../db/index.js';
18
- import { detectWorkspaces, loadConfig } from '../../../infrastructure/config.js';
18
+ import {
19
+ computeConfigHash,
20
+ detectWorkspaces,
21
+ getLastAppliedGlobalConfig,
22
+ getLastAppliedGlobalPath,
23
+ loadConfig,
24
+ promptForConsentIfNeeded,
25
+ } from '../../../infrastructure/config.js';
19
26
  import { debug, info, warn } from '../../../infrastructure/logger.js';
20
27
  import { loadNative } from '../../../infrastructure/native.js';
21
28
  import { toErrorMessage } from '../../../shared/errors.js';
@@ -114,6 +121,16 @@ function checkEngineSchemaMismatch(ctx: PipelineContext): void {
114
121
  );
115
122
  ctx.forceFullRebuild = true;
116
123
  }
124
+
125
+ // Config hash — promotes to full rebuild when build-relevant config changes
126
+ // (include/exclude/ignoreDirs/extensions/aliases/build.*).
127
+ // This closes the pre-existing config-change gap and covers the new global-config layer.
128
+ const currentConfigHash = computeConfigHash(ctx.config);
129
+ const prevConfigHash = meta('config_hash');
130
+ if (prevConfigHash && prevConfigHash !== currentConfigHash) {
131
+ info('Build-relevant config changed, promoting to full rebuild.');
132
+ ctx.forceFullRebuild = true;
133
+ }
117
134
  }
118
135
 
119
136
  function warnOnEmbeddingsWipe(ctx: PipelineContext): void {
@@ -172,7 +189,7 @@ function setupPipeline(ctx: PipelineContext): void {
172
189
  ctx.db = openDb(ctx.dbPath);
173
190
  initSchema(ctx.db);
174
191
 
175
- ctx.config = loadConfig(ctx.rootDir);
192
+ ctx.config = loadConfig(ctx.rootDir, { userConfig: ctx.opts.userConfig });
176
193
  // Merge caller-supplied excludes on top of the file-config excludes so
177
194
  // programmatic callers (e.g. benchmark scripts) can extend exclusion
178
195
  // without mutating .codegraphrc.json. Native orchestrator picks this up
@@ -186,6 +203,30 @@ function setupPipeline(ctx: PipelineContext): void {
186
203
  ctx.incremental =
187
204
  ctx.opts.incremental !== false && ctx.config.build && ctx.config.build.incremental !== false;
188
205
 
206
+ // ── Build-time global-config notice ──────────────────────────────
207
+ // Use the already-parsed and sanitized global config cached by loadConfig —
208
+ // avoids a second disk read and the TOCTOU window between loadConfig and here.
209
+ const appliedGlobalPath = getLastAppliedGlobalPath();
210
+ if (appliedGlobalPath) {
211
+ const buildAffectingKeys = [
212
+ 'include',
213
+ 'exclude',
214
+ 'ignoreDirs',
215
+ 'extensions',
216
+ 'aliases',
217
+ 'build',
218
+ ];
219
+ const globalData = getLastAppliedGlobalConfig();
220
+ if (globalData) {
221
+ const injectedKeys = buildAffectingKeys.filter((k) => k in globalData);
222
+ if (injectedKeys.length > 0) {
223
+ process.stderr.write(
224
+ `ℹ global config applied (${appliedGlobalPath}) — injecting: ${injectedKeys.join(', ')} · --no-user-config to ignore\n`,
225
+ );
226
+ }
227
+ }
228
+ }
229
+
189
230
  initializeEngine(ctx);
190
231
  checkEngineSchemaMismatch(ctx);
191
232
  warnOnEmbeddingsWipe(ctx);
@@ -348,6 +389,12 @@ export async function buildGraph(
348
389
  ctx.rootDir = rootDir;
349
390
 
350
391
  try {
392
+ // Interactive consent prompt — only fires when the caller opts in (build
393
+ // command with TTY), a global file exists, and the repo is undecided.
394
+ if (opts.promptForConsent) {
395
+ await promptForConsentIfNeeded(rootDir);
396
+ }
397
+
351
398
  setupPipeline(ctx);
352
399
 
353
400
  // ── JS-side fast-skip for native incremental (#1054) ──────────────