@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.
- package/README.md +71 -35
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +2 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/batch.d.ts.map +1 -1
- package/dist/cli/commands/batch.js +1 -0
- package/dist/cli/commands/batch.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +6 -1
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/config.d.ts +3 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +272 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/triage.js +1 -1
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/shared/options.d.ts +2 -1
- package/dist/cli/shared/options.d.ts.map +1 -1
- package/dist/cli/shared/options.js +11 -1
- package/dist/cli/shared/options.js.map +1 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/db/migrations.js +1 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/domain/graph/builder/call-resolver.d.ts +12 -8
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
- package/dist/domain/graph/builder/call-resolver.js +93 -38
- package/dist/domain/graph/builder/call-resolver.js.map +1 -1
- package/dist/domain/graph/builder/cha.d.ts +9 -1
- package/dist/domain/graph/builder/cha.d.ts.map +1 -1
- package/dist/domain/graph/builder/cha.js +17 -2
- package/dist/domain/graph/builder/cha.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +8 -0
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +22 -3
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +1 -1
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +37 -2
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts +0 -2
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +88 -318
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +4 -0
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.js +341 -82
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/parser.d.ts +4 -5
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +46 -15
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +10 -2
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
- package/dist/domain/wasm-worker-pool.js +2 -0
- package/dist/domain/wasm-worker-pool.js.map +1 -1
- package/dist/domain/wasm-worker-protocol.d.ts +1 -0
- package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
- package/dist/extractors/cpp.d.ts.map +1 -1
- package/dist/extractors/cpp.js +42 -1
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/cuda.d.ts.map +1 -1
- package/dist/extractors/cuda.js +42 -1
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/helpers.d.ts +11 -0
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +40 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +8 -7
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.js +137 -6
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/features/structure-query.d.ts +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +6 -6
- package/dist/features/structure-query.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/config.d.ts +77 -4
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +395 -21
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/infrastructure/registry.d.ts +27 -0
- package/dist/infrastructure/registry.d.ts.map +1 -1
- package/dist/infrastructure/registry.js +59 -1
- package/dist/infrastructure/registry.js.map +1 -1
- package/dist/presentation/structure.d.ts +1 -1
- package/dist/presentation/structure.d.ts.map +1 -1
- package/dist/presentation/structure.js +2 -2
- package/dist/presentation/structure.js.map +1 -1
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-gleam.wasm +0 -0
- package/package.json +7 -8
- package/src/cli/commands/audit.ts +2 -1
- package/src/cli/commands/batch.ts +1 -0
- package/src/cli/commands/build.ts +6 -1
- package/src/cli/commands/config.ts +353 -0
- package/src/cli/commands/triage.ts +1 -1
- package/src/cli/index.ts +10 -0
- package/src/cli/shared/options.ts +11 -1
- package/src/cli/types.ts +2 -0
- package/src/db/migrations.ts +1 -1
- package/src/domain/graph/builder/call-resolver.ts +99 -41
- package/src/domain/graph/builder/cha.ts +18 -1
- package/src/domain/graph/builder/helpers.ts +24 -4
- package/src/domain/graph/builder/incremental.ts +1 -0
- package/src/domain/graph/builder/pipeline.ts +49 -2
- package/src/domain/graph/builder/stages/build-edges.ts +130 -399
- package/src/domain/graph/builder/stages/detect-changes.ts +1 -1
- package/src/domain/graph/builder/stages/finalize.ts +4 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +396 -92
- package/src/domain/graph/builder/stages/resolve-imports.ts +1 -1
- package/src/domain/parser.ts +45 -14
- package/src/domain/wasm-worker-entry.ts +10 -2
- package/src/domain/wasm-worker-pool.ts +1 -0
- package/src/domain/wasm-worker-protocol.ts +1 -0
- package/src/extractors/cpp.ts +44 -1
- package/src/extractors/cuda.ts +44 -1
- package/src/extractors/helpers.ts +43 -0
- package/src/extractors/java.ts +8 -7
- package/src/extractors/javascript.ts +127 -6
- package/src/features/structure-query.ts +7 -7
- package/src/index.ts +5 -1
- package/src/infrastructure/config.ts +481 -22
- package/src/infrastructure/registry.ts +82 -1
- package/src/presentation/structure.ts +3 -3
- package/src/types.ts +41 -0
- package/grammars/tree-sitter-erlang.wasm +0 -0
|
@@ -49,13 +49,14 @@ import type { PipelineContext } from '../context.js';
|
|
|
49
49
|
import {
|
|
50
50
|
batchInsertEdges,
|
|
51
51
|
batchInsertNodes,
|
|
52
|
+
CHA_DISPATCH_PENALTY,
|
|
53
|
+
CHA_TYPED_DISPATCH_CONFIDENCE,
|
|
52
54
|
collectFiles as collectFilesUtil,
|
|
53
55
|
fileHash,
|
|
54
56
|
fileStat,
|
|
55
57
|
readFileSafe,
|
|
56
58
|
} from '../helpers.js';
|
|
57
59
|
import { NativeDbProxy } from '../native-db-proxy.js';
|
|
58
|
-
import { CHA_DISPATCH_PENALTY } from './build-edges.js';
|
|
59
60
|
import { closeNativeDb } from './native-db-lifecycle.js';
|
|
60
61
|
|
|
61
62
|
// ── Native orchestrator types ──────────────────────────────────────────
|
|
@@ -389,7 +390,7 @@ async function runPostNativeAnalysis(
|
|
|
389
390
|
}
|
|
390
391
|
|
|
391
392
|
/**
|
|
392
|
-
* Phase 8.
|
|
393
|
+
* Phase 8.6: CHA expansion post-pass for the native orchestrator path.
|
|
393
394
|
*
|
|
394
395
|
* The Rust build pipeline resolves typed receiver calls (e.g. `worker.doWork()`
|
|
395
396
|
* where `worker: IWorker`) to the interface method declaration only. This
|
|
@@ -401,12 +402,28 @@ async function runPostNativeAnalysis(
|
|
|
401
402
|
* Note: `this`/`super` dispatch is handled separately by `runPostNativeThisDispatch`,
|
|
402
403
|
* which WASM-re-parses JS/TS files to obtain raw call site receiver info.
|
|
403
404
|
*
|
|
405
|
+
* `changedFiles` controls candidate scoping on incremental builds:
|
|
406
|
+
* - null → full build; scan all call→method edges (existing behaviour).
|
|
407
|
+
* - array → incremental; two cheap gate queries decide scope:
|
|
408
|
+
* Gate A: any class/interface/trait/struct/record nodes in changed files?
|
|
409
|
+
* If yes, a new implementor may have appeared — full scan required.
|
|
410
|
+
* Gate B: any `calls` edges from changed-file sources targeting
|
|
411
|
+
* class/constructor/function-kind nodes? If yes, the RTA set may
|
|
412
|
+
* have grown (also covers the older-schema fallback where
|
|
413
|
+
* constructor calls target `constructor`/`function` nodes instead
|
|
414
|
+
* of `class` nodes) — full scan required.
|
|
415
|
+
* If neither gate fires: scope `callToMethods` to `src.file IN changedFiles`
|
|
416
|
+
* (safe because no hierarchy or RTA evidence changed).
|
|
417
|
+
*
|
|
404
418
|
* Returns the count of newly inserted CHA edges plus the set of files containing
|
|
405
419
|
* the new edges' endpoints, so the caller can scope role re-classification to the
|
|
406
420
|
* nodes whose fan-in/out actually changed. A zero count means no edges were added
|
|
407
421
|
* and role re-classification is unnecessary.
|
|
408
422
|
*/
|
|
409
|
-
function runPostNativeCha(
|
|
423
|
+
function runPostNativeCha(
|
|
424
|
+
db: BetterSqlite3Database,
|
|
425
|
+
changedFiles: string[] | null,
|
|
426
|
+
): {
|
|
410
427
|
newEdgeCount: number;
|
|
411
428
|
affectedFiles: Set<string>;
|
|
412
429
|
} {
|
|
@@ -474,19 +491,142 @@ function runPostNativeCha(db: BetterSqlite3Database): {
|
|
|
474
491
|
debug('runPostNativeCha: no constructor-call evidence found — proceeding without RTA filter');
|
|
475
492
|
}
|
|
476
493
|
|
|
494
|
+
// ── Incremental candidate scoping ──────────────────────────────────────────
|
|
495
|
+
// On incremental builds, two gate queries decide whether to restrict the
|
|
496
|
+
// candidate scan to changed-file call sites or run the full graph scan.
|
|
497
|
+
//
|
|
498
|
+
// Gate A: did a changed file add/change a class hierarchy node?
|
|
499
|
+
// A new `extends`/`implements` edge means a previously-untracked implementor
|
|
500
|
+
// is now in the hierarchy — unchanged call sites in OTHER files may gain new
|
|
501
|
+
// valid expansions, so the full scan is required.
|
|
502
|
+
// Note: *removed* class nodes are safe — Rust's `purge_changed_files` runs
|
|
503
|
+
// before this post-pass and deletes stale nodes and their hierarchy edges, so
|
|
504
|
+
// Gate A queries the post-purge DB. A deleted class returns no row here, which
|
|
505
|
+
// is correct: its stale CHA edges were already cleaned up by the Rust purge.
|
|
506
|
+
//
|
|
507
|
+
// Gate B: did a changed file add new RTA evidence (`new ConcreteX()`)?
|
|
508
|
+
// A new `calls` edge to a class/constructor/function-kind target means the
|
|
509
|
+
// instantiated set grew — previously RTA-filtered expansions in unchanged
|
|
510
|
+
// caller files become admissible, so the full scan is required.
|
|
511
|
+
// (`constructor`/`function` cover the older native engine fallback schema.)
|
|
512
|
+
//
|
|
513
|
+
// If neither gate fires, the hierarchy and RTA set are unchanged for all files
|
|
514
|
+
// outside changedFiles, so restricting to changed-file sources is safe.
|
|
515
|
+
let scopeToChangedFiles = false; // true → add WHERE src.file IN changedFiles
|
|
516
|
+
if (changedFiles !== null && changedFiles.length > 0) {
|
|
517
|
+
// Gate A: class/interface/trait/struct/record nodes in changed files?
|
|
518
|
+
const CHUNK_SIZE = 500;
|
|
519
|
+
let gateAFired = false;
|
|
520
|
+
for (let i = 0; i < changedFiles.length && !gateAFired; i += CHUNK_SIZE) {
|
|
521
|
+
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
|
|
522
|
+
const ph = chunk.map(() => '?').join(',');
|
|
523
|
+
const row = db
|
|
524
|
+
.prepare(
|
|
525
|
+
`SELECT 1 FROM nodes
|
|
526
|
+
WHERE file IN (${ph})
|
|
527
|
+
AND kind IN ('class', 'interface', 'trait', 'struct', 'record')
|
|
528
|
+
LIMIT 1`,
|
|
529
|
+
)
|
|
530
|
+
.get(...chunk);
|
|
531
|
+
if (row) gateAFired = true;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Gate B: calls from changed-file sources to class/instantiable-kind targets
|
|
535
|
+
// (also covers older-schema fallback and future CHA extensions to struct/record).
|
|
536
|
+
// Includes class/interface/trait/struct/record (future CHA extension safety) and
|
|
537
|
+
// constructor/function (older native engine schema fallback).
|
|
538
|
+
let gateBFired = false;
|
|
539
|
+
if (!gateAFired) {
|
|
540
|
+
for (let i = 0; i < changedFiles.length && !gateBFired; i += CHUNK_SIZE) {
|
|
541
|
+
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
|
|
542
|
+
const ph = chunk.map(() => '?').join(',');
|
|
543
|
+
const row = db
|
|
544
|
+
.prepare(
|
|
545
|
+
`SELECT 1 FROM edges e
|
|
546
|
+
JOIN nodes src ON e.source_id = src.id
|
|
547
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
548
|
+
WHERE e.kind = 'calls'
|
|
549
|
+
AND tgt.kind IN ('class', 'interface', 'trait', 'struct', 'record', 'constructor', 'function')
|
|
550
|
+
AND src.file IN (${ph})
|
|
551
|
+
LIMIT 1`,
|
|
552
|
+
)
|
|
553
|
+
.get(...chunk);
|
|
554
|
+
if (row) gateBFired = true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (!gateAFired && !gateBFired) {
|
|
559
|
+
scopeToChangedFiles = true;
|
|
560
|
+
debug(
|
|
561
|
+
`runPostNativeCha: neither gate fired — scoping candidate scan to ${changedFiles.length} changed file(s)`,
|
|
562
|
+
);
|
|
563
|
+
} else {
|
|
564
|
+
debug(
|
|
565
|
+
`runPostNativeCha: ${gateAFired ? 'Gate A (hierarchy)' : 'Gate B (RTA)'} fired — running full scan`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
477
570
|
// Find existing call edges targeting qualified methods (e.g., 'IWorker.doWork').
|
|
478
|
-
// Include
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
571
|
+
// Include caller_file and method_file so affectedFiles can be populated for
|
|
572
|
+
// incremental role reclassification; confidence uses CHA_TYPED_DISPATCH_CONFIDENCE matching runChaPostPass.
|
|
573
|
+
// When scopeToChangedFiles is true, restrict to call sites in the changed files
|
|
574
|
+
// (safe because no hierarchy or RTA evidence changed outside those files).
|
|
575
|
+
let callToMethods: Array<{
|
|
576
|
+
source_id: number;
|
|
577
|
+
caller_name: string;
|
|
578
|
+
method_name: string;
|
|
579
|
+
caller_file: string | null;
|
|
580
|
+
}>;
|
|
581
|
+
if (scopeToChangedFiles && changedFiles && changedFiles.length > 0) {
|
|
582
|
+
const CHUNK_SIZE = 500;
|
|
583
|
+
const rows: Array<{
|
|
584
|
+
source_id: number;
|
|
585
|
+
caller_name: string;
|
|
586
|
+
method_name: string;
|
|
587
|
+
caller_file: string | null;
|
|
588
|
+
}> = [];
|
|
589
|
+
for (let i = 0; i < changedFiles.length; i += CHUNK_SIZE) {
|
|
590
|
+
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
|
|
591
|
+
const ph = chunk.map(() => '?').join(',');
|
|
592
|
+
const chunkRows = db
|
|
593
|
+
.prepare(
|
|
594
|
+
`SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
|
|
595
|
+
FROM edges e
|
|
596
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
597
|
+
JOIN nodes src ON e.source_id = src.id
|
|
598
|
+
WHERE e.kind = 'calls' AND tgt.kind = 'method'
|
|
599
|
+
AND INSTR(tgt.name, '.') > 0
|
|
600
|
+
AND (e.technique IS NULL OR e.technique != 'cha-expanded')
|
|
601
|
+
AND src.file IN (${ph})`,
|
|
602
|
+
)
|
|
603
|
+
.all(...chunk) as Array<{
|
|
604
|
+
source_id: number;
|
|
605
|
+
caller_name: string;
|
|
606
|
+
method_name: string;
|
|
607
|
+
caller_file: string | null;
|
|
608
|
+
}>;
|
|
609
|
+
rows.push(...chunkRows);
|
|
610
|
+
}
|
|
611
|
+
callToMethods = rows;
|
|
612
|
+
} else {
|
|
613
|
+
callToMethods = db
|
|
614
|
+
.prepare(`
|
|
615
|
+
SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
|
|
616
|
+
FROM edges e
|
|
617
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
618
|
+
JOIN nodes src ON e.source_id = src.id
|
|
619
|
+
WHERE e.kind = 'calls' AND tgt.kind = 'method'
|
|
620
|
+
AND INSTR(tgt.name, '.') > 0
|
|
621
|
+
AND (e.technique IS NULL OR e.technique != 'cha-expanded')
|
|
622
|
+
`)
|
|
623
|
+
.all() as Array<{
|
|
624
|
+
source_id: number;
|
|
625
|
+
caller_name: string;
|
|
626
|
+
method_name: string;
|
|
627
|
+
caller_file: string | null;
|
|
628
|
+
}>;
|
|
629
|
+
}
|
|
490
630
|
|
|
491
631
|
// Seed seen-pairs only from the source_ids we'll be expanding — avoids loading every
|
|
492
632
|
// call edge in the DB (which would be O(all edges)) for large codebases.
|
|
@@ -540,16 +680,12 @@ function runPostNativeCha(db: BetterSqlite3Database): {
|
|
|
540
680
|
method_file: string | null;
|
|
541
681
|
}>;
|
|
542
682
|
for (const methodNode of methodNodes) {
|
|
683
|
+
if (methodNode.id === source_id) continue; // skip self-loops
|
|
543
684
|
const key = `${source_id}|${methodNode.id}`;
|
|
544
685
|
if (seen.has(key)) continue;
|
|
545
686
|
seen.add(key);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const conf =
|
|
549
|
-
computeConfidence(caller_file ?? '', methodNode.method_file ?? '', null) -
|
|
550
|
-
CHA_DISPATCH_PENALTY;
|
|
551
|
-
if (conf <= 0) continue;
|
|
552
|
-
newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha']);
|
|
687
|
+
const conf = CHA_TYPED_DISPATCH_CONFIDENCE;
|
|
688
|
+
newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha-expanded']);
|
|
553
689
|
newEdgeCount++;
|
|
554
690
|
if (caller_file) affectedFiles.add(caller_file);
|
|
555
691
|
if (methodNode.method_file) affectedFiles.add(methodNode.method_file);
|
|
@@ -564,6 +700,9 @@ function runPostNativeCha(db: BetterSqlite3Database): {
|
|
|
564
700
|
|
|
565
701
|
if (newEdges.length > 0) {
|
|
566
702
|
db.transaction(() => batchInsertEdges(db, newEdges))();
|
|
703
|
+
// Account for post-pass edges excluded from the build summary line (#1452),
|
|
704
|
+
// mirroring the this/super dispatch post-pass insertion log.
|
|
705
|
+
debug(`CHA expansion post-pass: inserted ${newEdgeCount} edge(s)`);
|
|
567
706
|
}
|
|
568
707
|
return { newEdgeCount, affectedFiles };
|
|
569
708
|
}
|
|
@@ -580,8 +719,13 @@ const THIS_DISPATCH_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs'
|
|
|
580
719
|
* `this`/`super` receivers, then resolves them through the class hierarchy stored
|
|
581
720
|
* in DB `extends` edges — mirroring what `buildChaPostPass` does on the WASM path.
|
|
582
721
|
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
722
|
+
* Also handles function-as-object-property methods (`f.h = function() { this.g() }`):
|
|
723
|
+
* these use `this` to reference sibling properties on the same object (`f`), so
|
|
724
|
+
* `resolveThisDispatch` resolves them by treating the dot-prefix of the caller name
|
|
725
|
+
* (`f` from `f.h`) as the class and looking up `f.g` directly — no `extends` edge needed.
|
|
726
|
+
*
|
|
727
|
+
* Runs when either `extends` edges exist (class inheritance) OR dot-named `method`
|
|
728
|
+
* nodes exist (func-prop assignments); skips only when neither is present.
|
|
585
729
|
*/
|
|
586
730
|
async function runPostNativeThisDispatch(
|
|
587
731
|
db: BetterSqlite3Database,
|
|
@@ -589,31 +733,44 @@ async function runPostNativeThisDispatch(
|
|
|
589
733
|
changedFiles: string[] | undefined,
|
|
590
734
|
isFullBuild: boolean,
|
|
591
735
|
): Promise<{ elapsedMs: number; targetIds: Set<number>; affectedFiles: Set<string> }> {
|
|
592
|
-
const t0 =
|
|
736
|
+
const t0 = performance.now();
|
|
593
737
|
const targetIds = new Set<number>();
|
|
594
738
|
// Files containing endpoints of newly inserted edges — lets the caller scope
|
|
595
739
|
// role re-classification to the nodes whose fan-in/out actually changed.
|
|
596
740
|
const affectedFiles = new Set<string>();
|
|
597
|
-
// Fast guard: need at least one extends edge for this/super to have meaning
|
|
598
|
-
const hasExtends = db.prepare(`SELECT 1 FROM edges WHERE kind = 'extends' LIMIT 1`).get();
|
|
599
|
-
if (!hasExtends) return { elapsedMs: 0, targetIds, affectedFiles };
|
|
600
741
|
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
742
|
+
// Fast guard: need at least one extends edge (class inheritance) OR a dot-named
|
|
743
|
+
// method node (func-prop assignment: `f.h = function() { this.g() }`) for
|
|
744
|
+
// this/super dispatch to produce any edges.
|
|
745
|
+
const hasExtends = db.prepare(`SELECT 1 FROM edges WHERE kind = 'extends' LIMIT 1`).get();
|
|
746
|
+
const hasFuncPropMethod = db
|
|
747
|
+
.prepare(`SELECT 1 FROM nodes WHERE kind = 'method' AND INSTR(name, '.') > 0 LIMIT 1`)
|
|
748
|
+
.get();
|
|
749
|
+
if (!hasExtends && !hasFuncPropMethod) return { elapsedMs: 0, targetIds, affectedFiles };
|
|
750
|
+
|
|
751
|
+
// Build parents map: child class → direct parent class (from `extends` edges).
|
|
752
|
+
// May be empty when only func-prop methods exist (no class inheritance) —
|
|
753
|
+
// resolveThisDispatch handles that case via direct class-prefix lookup.
|
|
754
|
+
const parentRows = hasExtends
|
|
755
|
+
? (db
|
|
756
|
+
.prepare(`
|
|
757
|
+
SELECT src.name AS child_name, tgt.name AS parent_name
|
|
758
|
+
FROM edges e
|
|
759
|
+
JOIN nodes src ON e.source_id = src.id
|
|
760
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
761
|
+
WHERE e.kind = 'extends'
|
|
762
|
+
`)
|
|
763
|
+
.all() as Array<{ child_name: string; parent_name: string }>)
|
|
764
|
+
: [];
|
|
611
765
|
|
|
612
766
|
const parents = new Map<string, string>();
|
|
613
767
|
for (const row of parentRows) {
|
|
614
768
|
if (!parents.has(row.child_name)) parents.set(row.child_name, row.parent_name);
|
|
615
769
|
}
|
|
616
|
-
|
|
770
|
+
// Note: parents may be empty when hasFuncPropMethod but !hasExtends — that is
|
|
771
|
+
// intentional. resolveThisDispatch still resolves `this.g()` inside `f.h` by
|
|
772
|
+
// treating `f` (the dot-prefix of callerName `f.h`) as the class and looking
|
|
773
|
+
// up `f.g` directly via lookup.byName(), without traversing the parents chain.
|
|
617
774
|
|
|
618
775
|
const chaCtx: ChaContext = {
|
|
619
776
|
implementors: new Map(), // not needed for this/super resolution
|
|
@@ -626,13 +783,11 @@ async function runPostNativeThisDispatch(
|
|
|
626
783
|
// On a full build we do NOT re-parse every JS/TS file — that would WASM-parse
|
|
627
784
|
// the entire project on top of the native pass, causing a massive regression
|
|
628
785
|
// (measured: +358% ms/file on codegraph itself). Instead we restrict to files
|
|
629
|
-
// that are part of the class inheritance hierarchy
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
// file not in
|
|
633
|
-
//
|
|
634
|
-
// the direct-call edge) or have no class context — and will be skipped by
|
|
635
|
-
// `resolveThisDispatch` anyway.
|
|
786
|
+
// that are part of the class inheritance hierarchy (both subclass files with
|
|
787
|
+
// `super.X()` calls and parent-class files with `this.X()` calls) OR that
|
|
788
|
+
// contain dot-named method nodes (func-prop assignments whose bodies may call
|
|
789
|
+
// `this.sibling()`). Any file not in either set has no class or object context
|
|
790
|
+
// where `this`/`super` dispatch would produce new edges.
|
|
636
791
|
let relFiles: string[];
|
|
637
792
|
if (isFullBuild || !changedFiles) {
|
|
638
793
|
const rows = db
|
|
@@ -647,6 +802,21 @@ async function runPostNativeThisDispatch(
|
|
|
647
802
|
FROM edges e
|
|
648
803
|
JOIN nodes tgt ON e.target_id = tgt.id
|
|
649
804
|
WHERE e.kind = 'extends' AND tgt.file IS NOT NULL
|
|
805
|
+
UNION
|
|
806
|
+
-- Files with func-prop method definitions (e.g. f.h = function(){this.g()}).
|
|
807
|
+
-- Only include files where the method's owner prefix is NOT a known class name —
|
|
808
|
+
-- this keeps the re-parse set small (func-prop files only, not all class-method files).
|
|
809
|
+
-- AND name IS NOT NULL guards the NOT IN sub-select: if any class node had a NULL
|
|
810
|
+
-- name the entire NOT IN clause would silently return no rows (SQL NULL semantics).
|
|
811
|
+
SELECT n.file AS file
|
|
812
|
+
FROM nodes n
|
|
813
|
+
WHERE n.kind = 'method'
|
|
814
|
+
AND INSTR(n.name, '.') > 0
|
|
815
|
+
AND n.file IS NOT NULL
|
|
816
|
+
AND SUBSTR(n.name, 1, INSTR(n.name, '.') - 1) NOT IN (
|
|
817
|
+
SELECT name FROM nodes WHERE kind IN ('class', 'struct', 'interface', 'type')
|
|
818
|
+
AND name IS NOT NULL
|
|
819
|
+
)
|
|
650
820
|
)
|
|
651
821
|
`)
|
|
652
822
|
.all() as Array<{ file: string }>;
|
|
@@ -786,15 +956,20 @@ async function runPostNativeThisDispatch(
|
|
|
786
956
|
call.receiver as 'this' | 'super',
|
|
787
957
|
chaCtx,
|
|
788
958
|
lookup,
|
|
959
|
+
relPath,
|
|
789
960
|
);
|
|
790
961
|
|
|
791
962
|
for (const t of targets) {
|
|
963
|
+
if (t.id === callerRow.id) continue; // skip self-loops
|
|
792
964
|
const key = `${callerRow.id}|${t.id}`;
|
|
793
965
|
if (seen.has(key)) continue;
|
|
794
966
|
seen.add(key);
|
|
795
967
|
const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
|
|
796
968
|
if (conf <= 0) continue;
|
|
797
|
-
|
|
969
|
+
// Tag super-dispatch edges distinctly so runPostNativeCha can exclude them
|
|
970
|
+
// from further CHA expansion (super calls are not virtual dispatch).
|
|
971
|
+
const technique = call.receiver === 'super' ? 'super-dispatch' : 'cha';
|
|
972
|
+
newEdges.push([callerRow.id, t.id, 'calls', conf, 0, technique]);
|
|
798
973
|
targetIds.add(t.id);
|
|
799
974
|
affectedFiles.add(relPath);
|
|
800
975
|
if (t.file) affectedFiles.add(t.file);
|
|
@@ -821,7 +996,15 @@ async function runPostNativeThisDispatch(
|
|
|
821
996
|
(symbols as { _tree?: unknown; _langId?: unknown })._langId = undefined;
|
|
822
997
|
}
|
|
823
998
|
|
|
824
|
-
return { elapsedMs:
|
|
999
|
+
return { elapsedMs: performance.now() - t0, targetIds, affectedFiles };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
interface PostPassTimings {
|
|
1003
|
+
gapDetectMs: number;
|
|
1004
|
+
chaMs: number;
|
|
1005
|
+
thisDispatchMs: number;
|
|
1006
|
+
reclassifyMs: number;
|
|
1007
|
+
techniqueBackfillMs: number;
|
|
825
1008
|
}
|
|
826
1009
|
|
|
827
1010
|
/** Format timing result from native orchestrator phases + JS post-processing. */
|
|
@@ -829,7 +1012,7 @@ function formatNativeTimingResult(
|
|
|
829
1012
|
p: Record<string, number>,
|
|
830
1013
|
structurePatchMs: number,
|
|
831
1014
|
analysisTiming: { astMs: number; complexityMs: number; cfgMs: number; dataflowMs: number },
|
|
832
|
-
|
|
1015
|
+
postPass: PostPassTimings,
|
|
833
1016
|
): BuildResult {
|
|
834
1017
|
return {
|
|
835
1018
|
phases: {
|
|
@@ -842,7 +1025,11 @@ function formatNativeTimingResult(
|
|
|
842
1025
|
edgesMs: +(p.edgesMs ?? 0).toFixed(1),
|
|
843
1026
|
structureMs: +((p.structureMs ?? 0) + structurePatchMs).toFixed(1),
|
|
844
1027
|
rolesMs: +(p.rolesMs ?? 0).toFixed(1),
|
|
845
|
-
|
|
1028
|
+
gapDetectMs: +postPass.gapDetectMs.toFixed(1),
|
|
1029
|
+
chaMs: +postPass.chaMs.toFixed(1),
|
|
1030
|
+
thisDispatchMs: +postPass.thisDispatchMs.toFixed(1),
|
|
1031
|
+
reclassifyMs: +postPass.reclassifyMs.toFixed(1),
|
|
1032
|
+
techniqueBackfillMs: +postPass.techniqueBackfillMs.toFixed(1),
|
|
846
1033
|
astMs: +(analysisTiming.astMs ?? 0).toFixed(1),
|
|
847
1034
|
complexityMs: +(analysisTiming.complexityMs ?? 0).toFixed(1),
|
|
848
1035
|
cfgMs: +(analysisTiming.cfgMs ?? 0).toFixed(1),
|
|
@@ -1043,10 +1230,35 @@ async function backfillNativeDroppedFiles(
|
|
|
1043
1230
|
|
|
1044
1231
|
if (missingAbs.length === 0) return;
|
|
1045
1232
|
|
|
1233
|
+
// Parse all missing files via WASM first so we can distinguish real native
|
|
1234
|
+
// extractor failures (WASM finds symbols but native didn't) from files the
|
|
1235
|
+
// Rust engine legitimately skipped (gitignored artifacts, empty declaration
|
|
1236
|
+
// files, etc. where WASM also produces 0 symbols). Both categories are
|
|
1237
|
+
// backfilled — only the former triggers a WARN (#1566).
|
|
1238
|
+
const wasmResults = await parseFilesWasmForBackfill(missingAbs, ctx.rootDir);
|
|
1239
|
+
|
|
1240
|
+
// Build two sets from wasmResults:
|
|
1241
|
+
// wasmParsedFiles — rel-paths present in wasmResults (WASM succeeded, even 0 symbols)
|
|
1242
|
+
// wasmFoundSymbols — subset where WASM found ≥1 symbol
|
|
1243
|
+
// Files absent from wasmParsedFiles were skipped by WASM entirely (extension
|
|
1244
|
+
// not in _extToLang, wasmExtractSymbols returned null, or a read error).
|
|
1245
|
+
// Those files do NOT end up in the batchInsertNodes loop below.
|
|
1246
|
+
const wasmParsedFiles = new Set<string>();
|
|
1247
|
+
const wasmFoundSymbols = new Set<string>();
|
|
1248
|
+
for (const [relPath, symbols] of wasmResults) {
|
|
1249
|
+
wasmParsedFiles.add(relPath);
|
|
1250
|
+
if ((symbols.definitions?.length ?? 0) > 0 || (symbols.exports?.length ?? 0) > 0) {
|
|
1251
|
+
wasmFoundSymbols.add(relPath);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1046
1255
|
// Classify drops so users see per-extension reasons instead of just a count
|
|
1047
1256
|
// (#1011). `unsupported-by-native` is a legitimate parser limit (no Rust
|
|
1048
1257
|
// extractor); `native-extractor-failure` indicates a real native bug since
|
|
1049
|
-
// the language IS supported by the addon yet
|
|
1258
|
+
// the language IS supported by the addon yet WASM found symbols the native
|
|
1259
|
+
// engine should have extracted. Files where both engines produce 0 symbols
|
|
1260
|
+
// are legitimately empty (e.g. gitignored napi-generated declaration stubs)
|
|
1261
|
+
// and logged at debug level only.
|
|
1050
1262
|
const { byReason, totals } = classifyNativeDrops(missingRel);
|
|
1051
1263
|
if (totals['unsupported-by-native'] > 0) {
|
|
1052
1264
|
const buckets = byReason['unsupported-by-native'];
|
|
@@ -1055,12 +1267,54 @@ async function backfillNativeDroppedFiles(
|
|
|
1055
1267
|
);
|
|
1056
1268
|
}
|
|
1057
1269
|
if (totals['native-extractor-failure'] > 0) {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1270
|
+
// Three-way split of native-extractor-failure files:
|
|
1271
|
+
// realFailureBuckets — WASM found symbols → real Rust extractor bug (WARN)
|
|
1272
|
+
// emptyFileBuckets — WASM parsed but found 0 symbols → gitignored/empty (debug)
|
|
1273
|
+
// These DO receive a file-node insert in the loop below.
|
|
1274
|
+
// wasmSkipBuckets — WASM skipped entirely (ext unknown or parse error) →
|
|
1275
|
+
// no file-node insert, and no WARN (debug only, distinct
|
|
1276
|
+
// message to avoid overstating backfill coverage).
|
|
1277
|
+
const allFailurePaths = byReason['native-extractor-failure'];
|
|
1278
|
+
const realFailureBuckets = new Map<string, string[]>();
|
|
1279
|
+
const emptyFileBuckets = new Map<string, string[]>();
|
|
1280
|
+
const wasmSkipBuckets = new Map<string, string[]>();
|
|
1281
|
+
for (const [ext, paths] of allFailurePaths) {
|
|
1282
|
+
for (const relPath of paths) {
|
|
1283
|
+
let bucket: Map<string, string[]>;
|
|
1284
|
+
if (wasmFoundSymbols.has(relPath)) {
|
|
1285
|
+
bucket = realFailureBuckets;
|
|
1286
|
+
} else if (wasmParsedFiles.has(relPath)) {
|
|
1287
|
+
bucket = emptyFileBuckets;
|
|
1288
|
+
} else {
|
|
1289
|
+
bucket = wasmSkipBuckets;
|
|
1290
|
+
}
|
|
1291
|
+
let list = bucket.get(ext);
|
|
1292
|
+
if (!list) {
|
|
1293
|
+
list = [];
|
|
1294
|
+
bucket.set(ext, list);
|
|
1295
|
+
}
|
|
1296
|
+
list.push(relPath);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (realFailureBuckets.size > 0) {
|
|
1300
|
+
const realCount = [...realFailureBuckets.values()].reduce((s, a) => s + a.length, 0);
|
|
1301
|
+
warn(
|
|
1302
|
+
`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)}`,
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
if (emptyFileBuckets.size > 0) {
|
|
1306
|
+
const emptyCount = [...emptyFileBuckets.values()].reduce((s, a) => s + a.length, 0);
|
|
1307
|
+
debug(
|
|
1308
|
+
`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)}`,
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
if (wasmSkipBuckets.size > 0) {
|
|
1312
|
+
const skipCount = [...wasmSkipBuckets.values()].reduce((s, a) => s + a.length, 0);
|
|
1313
|
+
debug(
|
|
1314
|
+
`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)}`,
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1062
1317
|
}
|
|
1063
|
-
const wasmResults = await parseFilesWasmForBackfill(missingAbs, ctx.rootDir);
|
|
1064
1318
|
|
|
1065
1319
|
const rows: unknown[][] = [];
|
|
1066
1320
|
const exportKeys: unknown[][] = [];
|
|
@@ -1302,7 +1556,7 @@ export async function tryNativeOrchestrator(
|
|
|
1302
1556
|
// Even on no-op rebuilds, dropped-language files added since the last
|
|
1303
1557
|
// full build are still missing from `nodes`/`file_hashes` (#1083), and
|
|
1304
1558
|
// WASM-only files deleted from disk leave stale rows behind (#1073).
|
|
1305
|
-
// The orchestrator's
|
|
1559
|
+
// The orchestrator's collect_files skipped them, so its earlyExit
|
|
1306
1560
|
// doesn't imply DB consistency. Run the gap repair before returning.
|
|
1307
1561
|
const gap = detectDroppedLanguageGap(ctx);
|
|
1308
1562
|
if (gap.missingAbs.length > 0 || gap.staleRel.length > 0) {
|
|
@@ -1344,9 +1598,9 @@ export async function tryNativeOrchestrator(
|
|
|
1344
1598
|
built_at: new Date().toISOString(),
|
|
1345
1599
|
});
|
|
1346
1600
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
)
|
|
1601
|
+
// The build summary is logged after the JS edge-writing post-passes below
|
|
1602
|
+
// (dropped-language backfill, CHA, this/super dispatch) so the reported
|
|
1603
|
+
// counts include their edges (#1452).
|
|
1350
1604
|
|
|
1351
1605
|
// ── Post-native structure + analysis ──────────────────────────────
|
|
1352
1606
|
let analysisTiming = {
|
|
@@ -1381,8 +1635,14 @@ export async function tryNativeOrchestrator(
|
|
|
1381
1635
|
ctx.db = openDb(ctx.dbPath);
|
|
1382
1636
|
ctx.nativeFirstProxy = false;
|
|
1383
1637
|
} else if (!ctx.nativeFirstProxy && !handoffWalAfterNativeBuild(ctx)) {
|
|
1384
|
-
// DB reopen failed — return partial result
|
|
1385
|
-
return formatNativeTimingResult(p, 0, analysisTiming,
|
|
1638
|
+
// DB reopen failed — return partial result (no post-pass phases completed)
|
|
1639
|
+
return formatNativeTimingResult(p, 0, analysisTiming, {
|
|
1640
|
+
gapDetectMs: 0,
|
|
1641
|
+
chaMs: 0,
|
|
1642
|
+
thisDispatchMs: 0,
|
|
1643
|
+
reclassifyMs: 0,
|
|
1644
|
+
techniqueBackfillMs: 0,
|
|
1645
|
+
});
|
|
1386
1646
|
}
|
|
1387
1647
|
}
|
|
1388
1648
|
|
|
@@ -1393,41 +1653,25 @@ export async function tryNativeOrchestrator(
|
|
|
1393
1653
|
// stale native binaries). WASM handles those — backfill via WASM so both
|
|
1394
1654
|
// engines process the same file set (#967).
|
|
1395
1655
|
//
|
|
1396
|
-
// Detect the gap once (fs walk + 2 DB queries
|
|
1397
|
-
//
|
|
1398
|
-
//
|
|
1399
|
-
//
|
|
1400
|
-
//
|
|
1401
|
-
//
|
|
1402
|
-
|
|
1403
|
-
// because the expensive part (WASM re-parse of the missing set) is
|
|
1404
|
-
// gated below.
|
|
1405
|
-
const removedCount = result.removedCount ?? 0;
|
|
1406
|
-
const changedCount = result.changedCount ?? 0;
|
|
1656
|
+
// Detect the gap once (fs walk + 2 DB queries) and use it for both gating
|
|
1657
|
+
// and the backfill itself. On quiet incrementals we still pay the walk so
|
|
1658
|
+
// we can detect brand-new files in dropped-language extensions — a gap that
|
|
1659
|
+
// the orchestrator's `detect_removed_files` filter (#1070) leaves open
|
|
1660
|
+
// (#1083, #1091). The pre-check is cheap because the expensive part (WASM
|
|
1661
|
+
// re-parse of the missing set) is gated below.
|
|
1662
|
+
const gapDetectStart = performance.now();
|
|
1407
1663
|
const gap = detectDroppedLanguageGap(ctx);
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
removedCount > 0 ||
|
|
1411
|
-
changedCount > 0 ||
|
|
1412
|
-
gap.missingAbs.length > 0 ||
|
|
1413
|
-
gap.staleRel.length > 0
|
|
1414
|
-
) {
|
|
1664
|
+
const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0;
|
|
1665
|
+
if (backfillHappened) {
|
|
1415
1666
|
await backfillNativeDroppedFiles(ctx, gap);
|
|
1416
1667
|
}
|
|
1417
|
-
|
|
1418
|
-
// Phase 8.5: expand CHA call edges (interface dispatch → concrete implementations).
|
|
1419
|
-
// Returns the affected files so role re-classification below can be scoped to
|
|
1420
|
-
// the nodes whose fan-in/out actually changed.
|
|
1421
|
-
//
|
|
1422
|
-
// Function-as-object-property methods (`fn.method = function() {}`) are extracted
|
|
1423
|
-
// natively by the Rust engine (#1432) and resolved in-build by its edge builder, so
|
|
1424
|
-
// no WASM re-parse post-pass is needed for them. `Foo.prototype.bar = fn` likewise.
|
|
1425
|
-
const { newEdgeCount: chaEdgeCount, affectedFiles: chaAffectedFiles } = runPostNativeCha(
|
|
1426
|
-
ctx.db as unknown as BetterSqlite3Database,
|
|
1427
|
-
);
|
|
1668
|
+
const gapDetectMs = performance.now() - gapDetectStart;
|
|
1428
1669
|
|
|
1429
1670
|
// Phase 8.5: this/super dispatch — hybrid WASM re-parse to resolve call sites
|
|
1430
1671
|
// whose raw receiver info the Rust pipeline does not persist to DB.
|
|
1672
|
+
// Runs BEFORE the CHA expansion pass so that super.method() → Parent.method edges
|
|
1673
|
+
// (technique='cha') are in the DB when runPostNativeCha expands them to sibling
|
|
1674
|
+
// class overrides (e.g. PostMixin.m → B.m when PostMixin and B both extend A).
|
|
1431
1675
|
const {
|
|
1432
1676
|
elapsedMs: thisDispatchMs,
|
|
1433
1677
|
targetIds: thisDispatchTargetIds,
|
|
@@ -1439,6 +1683,26 @@ export async function tryNativeOrchestrator(
|
|
|
1439
1683
|
!!result.isFullBuild,
|
|
1440
1684
|
);
|
|
1441
1685
|
|
|
1686
|
+
// Phase 8.6: expand CHA call edges (interface dispatch → concrete implementations).
|
|
1687
|
+
// Returns the affected files so role re-classification below can be scoped to
|
|
1688
|
+
// the nodes whose fan-in/out actually changed.
|
|
1689
|
+
//
|
|
1690
|
+
// Runs AFTER this/super dispatch so super.method() edges are already in the DB.
|
|
1691
|
+
// The 'cha-expanded' technique tag on this pass's own output prevents re-expansion
|
|
1692
|
+
// of those edges in subsequent incremental builds, while 'cha'-tagged edges from
|
|
1693
|
+
// this/super dispatch remain eligible for expansion here.
|
|
1694
|
+
//
|
|
1695
|
+
// Function-as-object-property methods (`fn.method = function() {}`) are extracted
|
|
1696
|
+
// natively by the Rust engine (#1432) and resolved in-build by its edge builder, so
|
|
1697
|
+
// no WASM re-parse post-pass is needed for them. `Foo.prototype.bar = fn` likewise.
|
|
1698
|
+
const chaStart = performance.now();
|
|
1699
|
+
const { newEdgeCount: chaEdgeCount, affectedFiles: chaAffectedFiles } = runPostNativeCha(
|
|
1700
|
+
ctx.db as unknown as BetterSqlite3Database,
|
|
1701
|
+
// null = full build (scan all call→method edges); array = incremental (gate queries decide scope)
|
|
1702
|
+
result.isFullBuild ? null : (result.changedFiles ?? null),
|
|
1703
|
+
);
|
|
1704
|
+
const chaMs = performance.now() - chaStart;
|
|
1705
|
+
|
|
1442
1706
|
// Role re-classification after JS edge-writing post-passes.
|
|
1443
1707
|
// The Rust orchestrator classifies roles before these post-passes (CHA,
|
|
1444
1708
|
// this-dispatch) add edges, so roles for the edge endpoints are stale.
|
|
@@ -1447,6 +1711,7 @@ export async function tryNativeOrchestrator(
|
|
|
1447
1711
|
// files restores correctness without re-running the classifier over the
|
|
1448
1712
|
// whole graph (which cost ~130ms per build on codegraph itself and was a
|
|
1449
1713
|
// major part of the v3.12.0 native full-build benchmark regression).
|
|
1714
|
+
let reclassifyMs = 0;
|
|
1450
1715
|
if (chaEdgeCount > 0 || thisDispatchTargetIds.size > 0) {
|
|
1451
1716
|
const affectedFiles = [...new Set([...chaAffectedFiles, ...thisDispatchAffectedFiles])];
|
|
1452
1717
|
// When edges were inserted but all their endpoint nodes have null `file`
|
|
@@ -1455,6 +1720,7 @@ export async function tryNativeOrchestrator(
|
|
|
1455
1720
|
// case — scoped classification with an empty set would be a no-op, leaving
|
|
1456
1721
|
// roles stale for those nodes.
|
|
1457
1722
|
const scopedFiles = affectedFiles.length > 0 ? affectedFiles : null;
|
|
1723
|
+
const reclassifyStart = performance.now();
|
|
1458
1724
|
try {
|
|
1459
1725
|
const { classifyNodeRoles } = (await import('../../../../features/structure.js')) as {
|
|
1460
1726
|
classifyNodeRoles: (
|
|
@@ -1471,13 +1737,45 @@ export async function tryNativeOrchestrator(
|
|
|
1471
1737
|
} catch (err) {
|
|
1472
1738
|
debug(`Post-pass role re-classification failed: ${toErrorMessage(err)}`);
|
|
1473
1739
|
}
|
|
1740
|
+
reclassifyMs = performance.now() - reclassifyStart;
|
|
1474
1741
|
}
|
|
1475
1742
|
|
|
1476
1743
|
// Backfill the `technique` column on `calls` edges written by the Rust
|
|
1477
1744
|
// orchestrator, which does not write the column. Runs after all edge-writing
|
|
1478
1745
|
// phases (including the WASM dropped-language backfill, CHA post-pass, and
|
|
1479
1746
|
// this/super dispatch) so every new edge in this build cycle gets a label.
|
|
1747
|
+
const techniqueBackfillStart = performance.now();
|
|
1480
1748
|
backfillEdgeTechniquesAfterNativeOrchestrator(ctx.db, !!result.isFullBuild, result.changedFiles);
|
|
1749
|
+
const techniqueBackfillMs = performance.now() - techniqueBackfillStart;
|
|
1750
|
+
|
|
1751
|
+
// Re-count nodes/edges now that all edge-writing post-passes have run: the
|
|
1752
|
+
// Rust orchestrator captured its counts before the JS post-passes added
|
|
1753
|
+
// edges, so both its summary and build_meta under-report (#1452).
|
|
1754
|
+
//
|
|
1755
|
+
// Fast path: skip the COUNT(*) scan when no post-pass wrote any edges.
|
|
1756
|
+
// COUNT(*) on large tables (50K+ edges) is non-trivial, especially via the
|
|
1757
|
+
// NativeDbProxy napi-rs round-trip. When all post-passes were no-ops, the
|
|
1758
|
+
// Rust orchestrator's counts are still accurate — no re-count needed.
|
|
1759
|
+
let finalNodeCount = result.nodeCount ?? 0;
|
|
1760
|
+
let finalEdgeCount = result.edgeCount ?? 0;
|
|
1761
|
+
const postPassWroteData = backfillHappened || chaEdgeCount > 0 || thisDispatchTargetIds.size > 0;
|
|
1762
|
+
if (postPassWroteData) {
|
|
1763
|
+
try {
|
|
1764
|
+
const counts = (ctx.db as unknown as BetterSqlite3Database)
|
|
1765
|
+
.prepare('SELECT (SELECT COUNT(*) FROM nodes) AS n, (SELECT COUNT(*) FROM edges) AS e')
|
|
1766
|
+
.get() as { n: number; e: number };
|
|
1767
|
+
if (counts.n !== finalNodeCount || counts.e !== finalEdgeCount) {
|
|
1768
|
+
finalNodeCount = counts.n;
|
|
1769
|
+
finalEdgeCount = counts.e;
|
|
1770
|
+
setBuildMeta(ctx.db, { node_count: finalNodeCount, edge_count: finalEdgeCount });
|
|
1771
|
+
}
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
debug(`Post-pass node/edge re-count failed: ${toErrorMessage(err)}`);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
info(
|
|
1777
|
+
`Native build orchestrator completed: ${finalNodeCount} nodes, ${finalEdgeCount} edges, ${result.fileCount ?? 0} files`,
|
|
1778
|
+
);
|
|
1481
1779
|
|
|
1482
1780
|
// ── Structure and analysis fallback (run after edge-writing so roles see full graph) ──
|
|
1483
1781
|
// Reconstruct fileSymbols once for both structure and analysis to avoid two
|
|
@@ -1501,5 +1799,11 @@ export async function tryNativeOrchestrator(
|
|
|
1501
1799
|
}
|
|
1502
1800
|
|
|
1503
1801
|
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
|
|
1504
|
-
return formatNativeTimingResult(p, structurePatchMs, analysisTiming,
|
|
1802
|
+
return formatNativeTimingResult(p, structurePatchMs, analysisTiming, {
|
|
1803
|
+
gapDetectMs,
|
|
1804
|
+
chaMs,
|
|
1805
|
+
thisDispatchMs,
|
|
1806
|
+
reclassifyMs,
|
|
1807
|
+
techniqueBackfillMs,
|
|
1808
|
+
});
|
|
1505
1809
|
}
|