@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
|
@@ -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.
|
|
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
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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
|
-
*
|
|
427
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
455
|
-
|
|
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
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
// file not in
|
|
470
|
-
//
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
855
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1162
|
-
|
|
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
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
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
|
|
1183
|
-
|
|
1184
|
-
//
|
|
1185
|
-
|
|
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,
|
|
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
|