@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
@@ -23,9 +23,8 @@ import { CODEGRAPH_VERSION } from '../../../../shared/version.js';
23
23
  import { classifyNativeDrops, formatDropExtensionSummary, getInstalledWasmExtensions, NATIVE_SUPPORTED_EXTENSIONS, parseFilesWasmForBackfill, } from '../../../parser.js';
24
24
  import { computeConfidence } from '../../resolve.js';
25
25
  import { resolveThisDispatch } from '../cha.js';
26
- import { batchInsertEdges, batchInsertNodes, collectFiles as collectFilesUtil, fileHash, fileStat, readFileSafe, } from '../helpers.js';
26
+ import { batchInsertEdges, batchInsertNodes, CHA_DISPATCH_PENALTY, CHA_TYPED_DISPATCH_CONFIDENCE, collectFiles as collectFilesUtil, fileHash, fileStat, readFileSafe, } from '../helpers.js';
27
27
  import { NativeDbProxy } from '../native-db-proxy.js';
28
- import { CHA_DISPATCH_PENALTY } from './build-edges.js';
29
28
  import { closeNativeDb } from './native-db-lifecycle.js';
30
29
  // ── Native orchestrator helpers ───────────────────────────────────────
31
30
  /** Determine whether the native orchestrator should be skipped. Returns a reason string, or null if it should run. */
@@ -246,7 +245,7 @@ async function runPostNativeAnalysis(ctx, allFileSymbols, changedFiles) {
246
245
  return timing;
247
246
  }
248
247
  /**
249
- * Phase 8.5: CHA expansion post-pass for the native orchestrator path.
248
+ * Phase 8.6: CHA expansion post-pass for the native orchestrator path.
250
249
  *
251
250
  * The Rust build pipeline resolves typed receiver calls (e.g. `worker.doWork()`
252
251
  * where `worker: IWorker`) to the interface method declaration only. This
@@ -258,12 +257,25 @@ async function runPostNativeAnalysis(ctx, allFileSymbols, changedFiles) {
258
257
  * Note: `this`/`super` dispatch is handled separately by `runPostNativeThisDispatch`,
259
258
  * which WASM-re-parses JS/TS files to obtain raw call site receiver info.
260
259
  *
260
+ * `changedFiles` controls candidate scoping on incremental builds:
261
+ * - null → full build; scan all call→method edges (existing behaviour).
262
+ * - array → incremental; two cheap gate queries decide scope:
263
+ * Gate A: any class/interface/trait/struct/record nodes in changed files?
264
+ * If yes, a new implementor may have appeared — full scan required.
265
+ * Gate B: any `calls` edges from changed-file sources targeting
266
+ * class/constructor/function-kind nodes? If yes, the RTA set may
267
+ * have grown (also covers the older-schema fallback where
268
+ * constructor calls target `constructor`/`function` nodes instead
269
+ * of `class` nodes) — full scan required.
270
+ * If neither gate fires: scope `callToMethods` to `src.file IN changedFiles`
271
+ * (safe because no hierarchy or RTA evidence changed).
272
+ *
261
273
  * Returns the count of newly inserted CHA edges plus the set of files containing
262
274
  * the new edges' endpoints, so the caller can scope role re-classification to the
263
275
  * nodes whose fan-in/out actually changed. A zero count means no edges were added
264
276
  * and role re-classification is unnecessary.
265
277
  */
266
- function runPostNativeCha(db) {
278
+ function runPostNativeCha(db, changedFiles) {
267
279
  const affectedFiles = new Set();
268
280
  const empty = { newEdgeCount: 0, affectedFiles };
269
281
  // Fast guard: no hierarchy edges → no CHA work
@@ -327,19 +339,113 @@ function runPostNativeCha(db) {
327
339
  if (noRtaEvidence) {
328
340
  debug('runPostNativeCha: no constructor-call evidence found — proceeding without RTA filter');
329
341
  }
342
+ // ── Incremental candidate scoping ──────────────────────────────────────────
343
+ // On incremental builds, two gate queries decide whether to restrict the
344
+ // candidate scan to changed-file call sites or run the full graph scan.
345
+ //
346
+ // Gate A: did a changed file add/change a class hierarchy node?
347
+ // A new `extends`/`implements` edge means a previously-untracked implementor
348
+ // is now in the hierarchy — unchanged call sites in OTHER files may gain new
349
+ // valid expansions, so the full scan is required.
350
+ // Note: *removed* class nodes are safe — Rust's `purge_changed_files` runs
351
+ // before this post-pass and deletes stale nodes and their hierarchy edges, so
352
+ // Gate A queries the post-purge DB. A deleted class returns no row here, which
353
+ // is correct: its stale CHA edges were already cleaned up by the Rust purge.
354
+ //
355
+ // Gate B: did a changed file add new RTA evidence (`new ConcreteX()`)?
356
+ // A new `calls` edge to a class/constructor/function-kind target means the
357
+ // instantiated set grew — previously RTA-filtered expansions in unchanged
358
+ // caller files become admissible, so the full scan is required.
359
+ // (`constructor`/`function` cover the older native engine fallback schema.)
360
+ //
361
+ // If neither gate fires, the hierarchy and RTA set are unchanged for all files
362
+ // outside changedFiles, so restricting to changed-file sources is safe.
363
+ let scopeToChangedFiles = false; // true → add WHERE src.file IN changedFiles
364
+ if (changedFiles !== null && changedFiles.length > 0) {
365
+ // Gate A: class/interface/trait/struct/record nodes in changed files?
366
+ const CHUNK_SIZE = 500;
367
+ let gateAFired = false;
368
+ for (let i = 0; i < changedFiles.length && !gateAFired; i += CHUNK_SIZE) {
369
+ const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
370
+ const ph = chunk.map(() => '?').join(',');
371
+ const row = db
372
+ .prepare(`SELECT 1 FROM nodes
373
+ WHERE file IN (${ph})
374
+ AND kind IN ('class', 'interface', 'trait', 'struct', 'record')
375
+ LIMIT 1`)
376
+ .get(...chunk);
377
+ if (row)
378
+ gateAFired = true;
379
+ }
380
+ // Gate B: calls from changed-file sources to class/instantiable-kind targets
381
+ // (also covers older-schema fallback and future CHA extensions to struct/record).
382
+ // Includes class/interface/trait/struct/record (future CHA extension safety) and
383
+ // constructor/function (older native engine schema fallback).
384
+ let gateBFired = false;
385
+ if (!gateAFired) {
386
+ for (let i = 0; i < changedFiles.length && !gateBFired; i += CHUNK_SIZE) {
387
+ const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
388
+ const ph = chunk.map(() => '?').join(',');
389
+ const row = db
390
+ .prepare(`SELECT 1 FROM edges e
391
+ JOIN nodes src ON e.source_id = src.id
392
+ JOIN nodes tgt ON e.target_id = tgt.id
393
+ WHERE e.kind = 'calls'
394
+ AND tgt.kind IN ('class', 'interface', 'trait', 'struct', 'record', 'constructor', 'function')
395
+ AND src.file IN (${ph})
396
+ LIMIT 1`)
397
+ .get(...chunk);
398
+ if (row)
399
+ gateBFired = true;
400
+ }
401
+ }
402
+ if (!gateAFired && !gateBFired) {
403
+ scopeToChangedFiles = true;
404
+ debug(`runPostNativeCha: neither gate fired — scoping candidate scan to ${changedFiles.length} changed file(s)`);
405
+ }
406
+ else {
407
+ debug(`runPostNativeCha: ${gateAFired ? 'Gate A (hierarchy)' : 'Gate B (RTA)'} fired — running full scan`);
408
+ }
409
+ }
330
410
  // 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();
411
+ // Include caller_file and method_file so affectedFiles can be populated for
412
+ // incremental role reclassification; confidence uses CHA_TYPED_DISPATCH_CONFIDENCE matching runChaPostPass.
413
+ // When scopeToChangedFiles is true, restrict to call sites in the changed files
414
+ // (safe because no hierarchy or RTA evidence changed outside those files).
415
+ let callToMethods;
416
+ if (scopeToChangedFiles && changedFiles && changedFiles.length > 0) {
417
+ const CHUNK_SIZE = 500;
418
+ const rows = [];
419
+ for (let i = 0; i < changedFiles.length; i += CHUNK_SIZE) {
420
+ const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
421
+ const ph = chunk.map(() => '?').join(',');
422
+ const chunkRows = db
423
+ .prepare(`SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
424
+ FROM edges e
425
+ JOIN nodes tgt ON e.target_id = tgt.id
426
+ JOIN nodes src ON e.source_id = src.id
427
+ WHERE e.kind = 'calls' AND tgt.kind = 'method'
428
+ AND INSTR(tgt.name, '.') > 0
429
+ AND (e.technique IS NULL OR e.technique != 'cha-expanded')
430
+ AND src.file IN (${ph})`)
431
+ .all(...chunk);
432
+ rows.push(...chunkRows);
433
+ }
434
+ callToMethods = rows;
435
+ }
436
+ else {
437
+ callToMethods = db
438
+ .prepare(`
439
+ SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
440
+ FROM edges e
441
+ JOIN nodes tgt ON e.target_id = tgt.id
442
+ JOIN nodes src ON e.source_id = src.id
443
+ WHERE e.kind = 'calls' AND tgt.kind = 'method'
444
+ AND INSTR(tgt.name, '.') > 0
445
+ AND (e.technique IS NULL OR e.technique != 'cha-expanded')
446
+ `)
447
+ .all();
448
+ }
343
449
  // Seed seen-pairs only from the source_ids we'll be expanding — avoids loading every
344
450
  // call edge in the DB (which would be O(all edges)) for large codebases.
345
451
  const seen = new Set();
@@ -384,17 +490,14 @@ function runPostNativeCha(db) {
384
490
  const qualifiedName = `${cls}.${methodSuffix}`;
385
491
  const methodNodes = findMethodStmt.all(qualifiedName);
386
492
  for (const methodNode of methodNodes) {
493
+ if (methodNode.id === source_id)
494
+ continue; // skip self-loops
387
495
  const key = `${source_id}|${methodNode.id}`;
388
496
  if (seen.has(key))
389
497
  continue;
390
498
  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']);
499
+ const conf = CHA_TYPED_DISPATCH_CONFIDENCE;
500
+ newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha-expanded']);
398
501
  newEdgeCount++;
399
502
  if (caller_file)
400
503
  affectedFiles.add(caller_file);
@@ -409,6 +512,9 @@ function runPostNativeCha(db) {
409
512
  }
410
513
  if (newEdges.length > 0) {
411
514
  db.transaction(() => batchInsertEdges(db, newEdges))();
515
+ // Account for post-pass edges excluded from the build summary line (#1452),
516
+ // mirroring the this/super dispatch post-pass insertion log.
517
+ debug(`CHA expansion post-pass: inserted ${newEdgeCount} edge(s)`);
412
518
  }
413
519
  return { newEdgeCount, affectedFiles };
414
520
  }
@@ -423,36 +529,52 @@ const THIS_DISPATCH_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs'
423
529
  * `this`/`super` receivers, then resolves them through the class hierarchy stored
424
530
  * in DB `extends` edges — mirroring what `buildChaPostPass` does on the WASM path.
425
531
  *
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.
532
+ * Also handles function-as-object-property methods (`f.h = function() { this.g() }`):
533
+ * these use `this` to reference sibling properties on the same object (`f`), so
534
+ * `resolveThisDispatch` resolves them by treating the dot-prefix of the caller name
535
+ * (`f` from `f.h`) as the class and looking up `f.g` directly — no `extends` edge needed.
536
+ *
537
+ * Runs when either `extends` edges exist (class inheritance) OR dot-named `method`
538
+ * nodes exist (func-prop assignments); skips only when neither is present.
428
539
  */
429
540
  async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild) {
430
- const t0 = Date.now();
541
+ const t0 = performance.now();
431
542
  const targetIds = new Set();
432
543
  // Files containing endpoints of newly inserted edges — lets the caller scope
433
544
  // role re-classification to the nodes whose fan-in/out actually changed.
434
545
  const affectedFiles = new Set();
435
- // Fast guard: need at least one extends edge for this/super to have meaning
546
+ // Fast guard: need at least one extends edge (class inheritance) OR a dot-named
547
+ // method node (func-prop assignment: `f.h = function() { this.g() }`) for
548
+ // this/super dispatch to produce any edges.
436
549
  const hasExtends = db.prepare(`SELECT 1 FROM edges WHERE kind = 'extends' LIMIT 1`).get();
437
- if (!hasExtends)
550
+ const hasFuncPropMethod = db
551
+ .prepare(`SELECT 1 FROM nodes WHERE kind = 'method' AND INSTR(name, '.') > 0 LIMIT 1`)
552
+ .get();
553
+ if (!hasExtends && !hasFuncPropMethod)
438
554
  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();
555
+ // Build parents map: child class → direct parent class (from `extends` edges).
556
+ // May be empty when only func-prop methods exist (no class inheritance) —
557
+ // resolveThisDispatch handles that case via direct class-prefix lookup.
558
+ const parentRows = hasExtends
559
+ ? db
560
+ .prepare(`
561
+ SELECT src.name AS child_name, tgt.name AS parent_name
562
+ FROM edges e
563
+ JOIN nodes src ON e.source_id = src.id
564
+ JOIN nodes tgt ON e.target_id = tgt.id
565
+ WHERE e.kind = 'extends'
566
+ `)
567
+ .all()
568
+ : [];
449
569
  const parents = new Map();
450
570
  for (const row of parentRows) {
451
571
  if (!parents.has(row.child_name))
452
572
  parents.set(row.child_name, row.parent_name);
453
573
  }
454
- if (parents.size === 0)
455
- return { elapsedMs: 0, targetIds, affectedFiles };
574
+ // Note: parents may be empty when hasFuncPropMethod but !hasExtends — that is
575
+ // intentional. resolveThisDispatch still resolves `this.g()` inside `f.h` by
576
+ // treating `f` (the dot-prefix of callerName `f.h`) as the class and looking
577
+ // up `f.g` directly via lookup.byName(), without traversing the parents chain.
456
578
  const chaCtx = {
457
579
  implementors: new Map(), // not needed for this/super resolution
458
580
  parents,
@@ -463,13 +585,11 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
463
585
  // On a full build we do NOT re-parse every JS/TS file — that would WASM-parse
464
586
  // the entire project on top of the native pass, causing a massive regression
465
587
  // (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.
588
+ // that are part of the class inheritance hierarchy (both subclass files with
589
+ // `super.X()` calls and parent-class files with `this.X()` calls) OR that
590
+ // contain dot-named method nodes (func-prop assignments whose bodies may call
591
+ // `this.sibling()`). Any file not in either set has no class or object context
592
+ // where `this`/`super` dispatch would produce new edges.
473
593
  let relFiles;
474
594
  if (isFullBuild || !changedFiles) {
475
595
  const rows = db
@@ -484,6 +604,21 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
484
604
  FROM edges e
485
605
  JOIN nodes tgt ON e.target_id = tgt.id
486
606
  WHERE e.kind = 'extends' AND tgt.file IS NOT NULL
607
+ UNION
608
+ -- Files with func-prop method definitions (e.g. f.h = function(){this.g()}).
609
+ -- Only include files where the method's owner prefix is NOT a known class name —
610
+ -- this keeps the re-parse set small (func-prop files only, not all class-method files).
611
+ -- AND name IS NOT NULL guards the NOT IN sub-select: if any class node had a NULL
612
+ -- name the entire NOT IN clause would silently return no rows (SQL NULL semantics).
613
+ SELECT n.file AS file
614
+ FROM nodes n
615
+ WHERE n.kind = 'method'
616
+ AND INSTR(n.name, '.') > 0
617
+ AND n.file IS NOT NULL
618
+ AND SUBSTR(n.name, 1, INSTR(n.name, '.') - 1) NOT IN (
619
+ SELECT name FROM nodes WHERE kind IN ('class', 'struct', 'interface', 'type')
620
+ AND name IS NOT NULL
621
+ )
487
622
  )
488
623
  `)
489
624
  .all();
@@ -603,8 +738,10 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
603
738
  const callerRow = findCallerByLineStmt.get(relPath, call.line, call.line);
604
739
  if (!callerRow)
605
740
  continue;
606
- const targets = resolveThisDispatch(call.name, callerRow.name, call.receiver, chaCtx, lookup);
741
+ const targets = resolveThisDispatch(call.name, callerRow.name, call.receiver, chaCtx, lookup, relPath);
607
742
  for (const t of targets) {
743
+ if (t.id === callerRow.id)
744
+ continue; // skip self-loops
608
745
  const key = `${callerRow.id}|${t.id}`;
609
746
  if (seen.has(key))
610
747
  continue;
@@ -612,7 +749,10 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
612
749
  const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
613
750
  if (conf <= 0)
614
751
  continue;
615
- newEdges.push([callerRow.id, t.id, 'calls', conf, 0, 'cha']);
752
+ // Tag super-dispatch edges distinctly so runPostNativeCha can exclude them
753
+ // from further CHA expansion (super calls are not virtual dispatch).
754
+ const technique = call.receiver === 'super' ? 'super-dispatch' : 'cha';
755
+ newEdges.push([callerRow.id, t.id, 'calls', conf, 0, technique]);
616
756
  targetIds.add(t.id);
617
757
  affectedFiles.add(relPath);
618
758
  if (t.file)
@@ -638,10 +778,10 @@ async function runPostNativeThisDispatch(db, rootDir, changedFiles, isFullBuild)
638
778
  symbols._tree = undefined;
639
779
  symbols._langId = undefined;
640
780
  }
641
- return { elapsedMs: Date.now() - t0, targetIds, affectedFiles };
781
+ return { elapsedMs: performance.now() - t0, targetIds, affectedFiles };
642
782
  }
643
783
  /** Format timing result from native orchestrator phases + JS post-processing. */
644
- function formatNativeTimingResult(p, structurePatchMs, analysisTiming, thisDispatchMs) {
784
+ function formatNativeTimingResult(p, structurePatchMs, analysisTiming, postPass) {
645
785
  return {
646
786
  phases: {
647
787
  setupMs: +(p.setupMs ?? 0).toFixed(1),
@@ -653,7 +793,11 @@ function formatNativeTimingResult(p, structurePatchMs, analysisTiming, thisDispa
653
793
  edgesMs: +(p.edgesMs ?? 0).toFixed(1),
654
794
  structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
655
795
  rolesMs: +(p.rolesMs ?? 0).toFixed(1),
656
- thisDispatchMs: +thisDispatchMs.toFixed(1),
796
+ gapDetectMs: +postPass.gapDetectMs.toFixed(1),
797
+ chaMs: +postPass.chaMs.toFixed(1),
798
+ thisDispatchMs: +postPass.thisDispatchMs.toFixed(1),
799
+ reclassifyMs: +postPass.reclassifyMs.toFixed(1),
800
+ techniqueBackfillMs: +postPass.techniqueBackfillMs.toFixed(1),
657
801
  astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
658
802
  complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
659
803
  cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
@@ -841,20 +985,83 @@ async function backfillNativeDroppedFiles(ctx, gap) {
841
985
  }
842
986
  if (missingAbs.length === 0)
843
987
  return;
988
+ // Parse all missing files via WASM first so we can distinguish real native
989
+ // extractor failures (WASM finds symbols but native didn't) from files the
990
+ // Rust engine legitimately skipped (gitignored artifacts, empty declaration
991
+ // files, etc. where WASM also produces 0 symbols). Both categories are
992
+ // backfilled — only the former triggers a WARN (#1566).
993
+ const wasmResults = await parseFilesWasmForBackfill(missingAbs, ctx.rootDir);
994
+ // Build two sets from wasmResults:
995
+ // wasmParsedFiles — rel-paths present in wasmResults (WASM succeeded, even 0 symbols)
996
+ // wasmFoundSymbols — subset where WASM found ≥1 symbol
997
+ // Files absent from wasmParsedFiles were skipped by WASM entirely (extension
998
+ // not in _extToLang, wasmExtractSymbols returned null, or a read error).
999
+ // Those files do NOT end up in the batchInsertNodes loop below.
1000
+ const wasmParsedFiles = new Set();
1001
+ const wasmFoundSymbols = new Set();
1002
+ for (const [relPath, symbols] of wasmResults) {
1003
+ wasmParsedFiles.add(relPath);
1004
+ if ((symbols.definitions?.length ?? 0) > 0 || (symbols.exports?.length ?? 0) > 0) {
1005
+ wasmFoundSymbols.add(relPath);
1006
+ }
1007
+ }
844
1008
  // Classify drops so users see per-extension reasons instead of just a count
845
1009
  // (#1011). `unsupported-by-native` is a legitimate parser limit (no Rust
846
1010
  // extractor); `native-extractor-failure` indicates a real native bug since
847
- // the language IS supported by the addon yet the file was dropped anyway.
1011
+ // the language IS supported by the addon yet WASM found symbols the native
1012
+ // engine should have extracted. Files where both engines produce 0 symbols
1013
+ // are legitimately empty (e.g. gitignored napi-generated declaration stubs)
1014
+ // and logged at debug level only.
848
1015
  const { byReason, totals } = classifyNativeDrops(missingRel);
849
1016
  if (totals['unsupported-by-native'] > 0) {
850
1017
  const buckets = byReason['unsupported-by-native'];
851
1018
  info(`Native orchestrator skipped ${totals['unsupported-by-native']} file(s) across ${buckets.size} extension(s) in languages without a Rust extractor; backfilling via WASM:${formatDropExtensionSummary(buckets)}`);
852
1019
  }
853
1020
  if (totals['native-extractor-failure'] > 0) {
854
- const buckets = byReason['native-extractor-failure'];
855
- warn(`Native orchestrator dropped ${totals['native-extractor-failure']} file(s) across ${buckets.size} extension(s) in natively-supported languages — likely a Rust extractor bug. Backfilling via WASM:${formatDropExtensionSummary(buckets)}`);
1021
+ // Three-way split of native-extractor-failure files:
1022
+ // realFailureBuckets — WASM found symbols real Rust extractor bug (WARN)
1023
+ // emptyFileBuckets — WASM parsed but found 0 symbols → gitignored/empty (debug)
1024
+ // These DO receive a file-node insert in the loop below.
1025
+ // wasmSkipBuckets — WASM skipped entirely (ext unknown or parse error) →
1026
+ // no file-node insert, and no WARN (debug only, distinct
1027
+ // message to avoid overstating backfill coverage).
1028
+ const allFailurePaths = byReason['native-extractor-failure'];
1029
+ const realFailureBuckets = new Map();
1030
+ const emptyFileBuckets = new Map();
1031
+ const wasmSkipBuckets = new Map();
1032
+ for (const [ext, paths] of allFailurePaths) {
1033
+ for (const relPath of paths) {
1034
+ let bucket;
1035
+ if (wasmFoundSymbols.has(relPath)) {
1036
+ bucket = realFailureBuckets;
1037
+ }
1038
+ else if (wasmParsedFiles.has(relPath)) {
1039
+ bucket = emptyFileBuckets;
1040
+ }
1041
+ else {
1042
+ bucket = wasmSkipBuckets;
1043
+ }
1044
+ let list = bucket.get(ext);
1045
+ if (!list) {
1046
+ list = [];
1047
+ bucket.set(ext, list);
1048
+ }
1049
+ list.push(relPath);
1050
+ }
1051
+ }
1052
+ if (realFailureBuckets.size > 0) {
1053
+ const realCount = [...realFailureBuckets.values()].reduce((s, a) => s + a.length, 0);
1054
+ warn(`Native orchestrator dropped ${realCount} file(s) across ${realFailureBuckets.size} extension(s) in natively-supported languages — likely a Rust extractor bug. Backfilling via WASM:${formatDropExtensionSummary(realFailureBuckets)}`);
1055
+ }
1056
+ if (emptyFileBuckets.size > 0) {
1057
+ const emptyCount = [...emptyFileBuckets.values()].reduce((s, a) => s + a.length, 0);
1058
+ debug(`Native orchestrator skipped ${emptyCount} file(s) in natively-supported languages that also produced 0 symbols via WASM (likely gitignored or empty); backfilling file nodes:${formatDropExtensionSummary(emptyFileBuckets)}`);
1059
+ }
1060
+ if (wasmSkipBuckets.size > 0) {
1061
+ const skipCount = [...wasmSkipBuckets.values()].reduce((s, a) => s + a.length, 0);
1062
+ debug(`Native orchestrator skipped ${skipCount} file(s) in natively-supported languages that WASM also could not parse (unregistered extension or parse error); no file-node inserted:${formatDropExtensionSummary(wasmSkipBuckets)}`);
1063
+ }
856
1064
  }
857
- const wasmResults = await parseFilesWasmForBackfill(missingAbs, ctx.rootDir);
858
1065
  const rows = [];
859
1066
  const exportKeys = [];
860
1067
  for (const [relPath, symbols] of wasmResults) {
@@ -1072,7 +1279,7 @@ export async function tryNativeOrchestrator(ctx) {
1072
1279
  // Even on no-op rebuilds, dropped-language files added since the last
1073
1280
  // full build are still missing from `nodes`/`file_hashes` (#1083), and
1074
1281
  // WASM-only files deleted from disk leave stale rows behind (#1073).
1075
- // The orchestrator's file_collector skipped them, so its earlyExit
1282
+ // The orchestrator's collect_files skipped them, so its earlyExit
1076
1283
  // doesn't imply DB consistency. Run the gap repair before returning.
1077
1284
  const gap = detectDroppedLanguageGap(ctx);
1078
1285
  if (gap.missingAbs.length > 0 || gap.staleRel.length > 0) {
@@ -1110,7 +1317,9 @@ export async function tryNativeOrchestrator(ctx) {
1110
1317
  schema_version: String(ctx.schemaVersion),
1111
1318
  built_at: new Date().toISOString(),
1112
1319
  });
1113
- info(`Native build orchestrator completed: ${result.nodeCount ?? 0} nodes, ${result.edgeCount ?? 0} edges, ${result.fileCount ?? 0} files`);
1320
+ // The build summary is logged after the JS edge-writing post-passes below
1321
+ // (dropped-language backfill, CHA, this/super dispatch) so the reported
1322
+ // counts include their edges (#1452).
1114
1323
  // ── Post-native structure + analysis ──────────────────────────────
1115
1324
  let analysisTiming = {
1116
1325
  astMs: +(p.astMs ?? 0),
@@ -1143,8 +1352,14 @@ export async function tryNativeOrchestrator(ctx) {
1143
1352
  ctx.nativeFirstProxy = false;
1144
1353
  }
1145
1354
  else if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
1146
- // DB reopen failed — return partial result
1147
- return formatNativeTimingResult(p, 0, analysisTiming, 0);
1355
+ // DB reopen failed — return partial result (no post-pass phases completed)
1356
+ return formatNativeTimingResult(p, 0, analysisTiming, {
1357
+ gapDetectMs: 0,
1358
+ chaMs: 0,
1359
+ thisDispatchMs: 0,
1360
+ reclassifyMs: 0,
1361
+ techniqueBackfillMs: 0,
1362
+ });
1148
1363
  }
1149
1364
  }
1150
1365
  // ── Edge-writing post-passes (run before structure so roles see full graph) ──
@@ -1153,36 +1368,42 @@ export async function tryNativeOrchestrator(ctx) {
1153
1368
  // stale native binaries). WASM handles those — backfill via WASM so both
1154
1369
  // engines process the same file set (#967).
1155
1370
  //
1156
- // Detect the gap once (fs walk + 2 DB queries, ~20–30ms) and use it for
1157
- // both gating and the backfill itself. On dirty incrementals/full builds
1158
- // the orchestrator signals trigger backfill, so the walk happens once
1159
- // (instead of redundantly inside backfill). On quiet incrementals we
1160
- // still pay the walk so we can detect brand-new files in dropped-language
1161
- // extensions a gap that the orchestrator's `detect_removed_files`
1162
- // filter (#1070) leaves open (#1083, #1091). The pre-check is cheap
1163
- // because the expensive part (WASM re-parse of the missing set) is
1164
- // gated below.
1165
- const removedCount = result.removedCount ?? 0;
1166
- const changedCount = result.changedCount ?? 0;
1371
+ // Detect the gap once (fs walk + 2 DB queries) and use it for both gating
1372
+ // and the backfill itself. On quiet incrementals we still pay the walk so
1373
+ // we can detect brand-new files in dropped-language extensions a gap that
1374
+ // the orchestrator's `detect_removed_files` filter (#1070) leaves open
1375
+ // (#1083, #1091). The pre-check is cheap because the expensive part (WASM
1376
+ // re-parse of the missing set) is gated below.
1377
+ const gapDetectStart = performance.now();
1167
1378
  const gap = detectDroppedLanguageGap(ctx);
1168
- if (result.isFullBuild ||
1169
- removedCount > 0 ||
1170
- changedCount > 0 ||
1171
- gap.missingAbs.length > 0 ||
1172
- gap.staleRel.length > 0) {
1379
+ const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0;
1380
+ if (backfillHappened) {
1173
1381
  await backfillNativeDroppedFiles(ctx, gap);
1174
1382
  }
1175
- // Phase 8.5: expand CHA call edges (interface dispatch → concrete implementations).
1383
+ const gapDetectMs = performance.now() - gapDetectStart;
1384
+ // Phase 8.5: this/super dispatch — hybrid WASM re-parse to resolve call sites
1385
+ // whose raw receiver info the Rust pipeline does not persist to DB.
1386
+ // Runs BEFORE the CHA expansion pass so that super.method() → Parent.method edges
1387
+ // (technique='cha') are in the DB when runPostNativeCha expands them to sibling
1388
+ // class overrides (e.g. PostMixin.m → B.m when PostMixin and B both extend A).
1389
+ const { elapsedMs: thisDispatchMs, targetIds: thisDispatchTargetIds, affectedFiles: thisDispatchAffectedFiles, } = await runPostNativeThisDispatch(ctx.db, ctx.rootDir, result.changedFiles, !!result.isFullBuild);
1390
+ // Phase 8.6: expand CHA call edges (interface dispatch → concrete implementations).
1176
1391
  // Returns the affected files so role re-classification below can be scoped to
1177
1392
  // the nodes whose fan-in/out actually changed.
1178
1393
  //
1394
+ // Runs AFTER this/super dispatch so super.method() edges are already in the DB.
1395
+ // The 'cha-expanded' technique tag on this pass's own output prevents re-expansion
1396
+ // of those edges in subsequent incremental builds, while 'cha'-tagged edges from
1397
+ // this/super dispatch remain eligible for expansion here.
1398
+ //
1179
1399
  // Function-as-object-property methods (`fn.method = function() {}`) are extracted
1180
1400
  // natively by the Rust engine (#1432) and resolved in-build by its edge builder, so
1181
1401
  // 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);
1402
+ const chaStart = performance.now();
1403
+ const { newEdgeCount: chaEdgeCount, affectedFiles: chaAffectedFiles } = runPostNativeCha(ctx.db,
1404
+ // null = full build (scan all call→method edges); array = incremental (gate queries decide scope)
1405
+ result.isFullBuild ? null : (result.changedFiles ?? null));
1406
+ const chaMs = performance.now() - chaStart;
1186
1407
  // Role re-classification after JS edge-writing post-passes.
1187
1408
  // The Rust orchestrator classifies roles before these post-passes (CHA,
1188
1409
  // this-dispatch) add edges, so roles for the edge endpoints are stale.
@@ -1191,6 +1412,7 @@ export async function tryNativeOrchestrator(ctx) {
1191
1412
  // files restores correctness without re-running the classifier over the
1192
1413
  // whole graph (which cost ~130ms per build on codegraph itself and was a
1193
1414
  // major part of the v3.12.0 native full-build benchmark regression).
1415
+ let reclassifyMs = 0;
1194
1416
  if (chaEdgeCount > 0 || thisDispatchTargetIds.size > 0) {
1195
1417
  const affectedFiles = [...new Set([...chaAffectedFiles, ...thisDispatchAffectedFiles])];
1196
1418
  // When edges were inserted but all their endpoint nodes have null `file`
@@ -1199,6 +1421,7 @@ export async function tryNativeOrchestrator(ctx) {
1199
1421
  // case — scoped classification with an empty set would be a no-op, leaving
1200
1422
  // roles stale for those nodes.
1201
1423
  const scopedFiles = affectedFiles.length > 0 ? affectedFiles : null;
1424
+ const reclassifyStart = performance.now();
1202
1425
  try {
1203
1426
  const { classifyNodeRoles } = (await import('../../../../features/structure.js'));
1204
1427
  classifyNodeRoles(ctx.db, scopedFiles);
@@ -1209,12 +1432,42 @@ export async function tryNativeOrchestrator(ctx) {
1209
1432
  catch (err) {
1210
1433
  debug(`Post-pass role re-classification failed: ${toErrorMessage(err)}`);
1211
1434
  }
1435
+ reclassifyMs = performance.now() - reclassifyStart;
1212
1436
  }
1213
1437
  // Backfill the `technique` column on `calls` edges written by the Rust
1214
1438
  // orchestrator, which does not write the column. Runs after all edge-writing
1215
1439
  // phases (including the WASM dropped-language backfill, CHA post-pass, and
1216
1440
  // this/super dispatch) so every new edge in this build cycle gets a label.
1441
+ const techniqueBackfillStart = performance.now();
1217
1442
  backfillEdgeTechniquesAfterNativeOrchestrator(ctx.db, !!result.isFullBuild, result.changedFiles);
1443
+ const techniqueBackfillMs = performance.now() - techniqueBackfillStart;
1444
+ // Re-count nodes/edges now that all edge-writing post-passes have run: the
1445
+ // Rust orchestrator captured its counts before the JS post-passes added
1446
+ // edges, so both its summary and build_meta under-report (#1452).
1447
+ //
1448
+ // Fast path: skip the COUNT(*) scan when no post-pass wrote any edges.
1449
+ // COUNT(*) on large tables (50K+ edges) is non-trivial, especially via the
1450
+ // NativeDbProxy napi-rs round-trip. When all post-passes were no-ops, the
1451
+ // Rust orchestrator's counts are still accurate — no re-count needed.
1452
+ let finalNodeCount = result.nodeCount ?? 0;
1453
+ let finalEdgeCount = result.edgeCount ?? 0;
1454
+ const postPassWroteData = backfillHappened || chaEdgeCount > 0 || thisDispatchTargetIds.size > 0;
1455
+ if (postPassWroteData) {
1456
+ try {
1457
+ const counts = ctx.db
1458
+ .prepare('SELECT (SELECT COUNT(*) FROM nodes) AS n, (SELECT COUNT(*) FROM edges) AS e')
1459
+ .get();
1460
+ if (counts.n !== finalNodeCount || counts.e !== finalEdgeCount) {
1461
+ finalNodeCount = counts.n;
1462
+ finalEdgeCount = counts.e;
1463
+ setBuildMeta(ctx.db, { node_count: finalNodeCount, edge_count: finalEdgeCount });
1464
+ }
1465
+ }
1466
+ catch (err) {
1467
+ debug(`Post-pass node/edge re-count failed: ${toErrorMessage(err)}`);
1468
+ }
1469
+ }
1470
+ info(`Native build orchestrator completed: ${finalNodeCount} nodes, ${finalEdgeCount} edges, ${result.fileCount ?? 0} files`);
1218
1471
  // ── Structure and analysis fallback (run after edge-writing so roles see full graph) ──
1219
1472
  // Reconstruct fileSymbols once for both structure and analysis to avoid two
1220
1473
  // expensive DB scans. The DB handoff above already ensured ctx.db is a proper
@@ -1229,6 +1482,12 @@ export async function tryNativeOrchestrator(ctx) {
1229
1482
  }
1230
1483
  }
1231
1484
  closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
1232
- return formatNativeTimingResult(p, structurePatchMs, analysisTiming, thisDispatchMs);
1485
+ return formatNativeTimingResult(p, structurePatchMs, analysisTiming, {
1486
+ gapDetectMs,
1487
+ chaMs,
1488
+ thisDispatchMs,
1489
+ reclassifyMs,
1490
+ techniqueBackfillMs,
1491
+ });
1233
1492
  }
1234
1493
  //# sourceMappingURL=native-orchestrator.js.map