Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2b6a00d
fix: scope codegraph batch complexity targets to file paths
carlos-alm Jul 5, 2026
b28051a
fix: exclude parameters and interface/type members from dead-role cla…
carlos-alm Jul 5, 2026
17fbcba
fix: credit import-type usages as exports consumers
carlos-alm Jul 5, 2026
26918bb
fix: prevent fn-impact/query crash when -f/--file is passed
carlos-alm Jul 5, 2026
be6432c
fix: correct exported-symbol detection for literal and object-literal…
carlos-alm Jul 5, 2026
4ead840
fix: exclude primitive type keywords from ast --kind string matching
carlos-alm Jul 5, 2026
f6326bf
fix: resolve call edges through renamed import specifiers
carlos-alm Jul 5, 2026
590f560
fix: couple file_hashes updates with edge regeneration in incremental…
carlos-alm Jul 5, 2026
d3ea4a4
fix: compare signature-change diffs using new-file line ranges
carlos-alm Jul 5, 2026
c306812
feat: add environment doctor check for stale native binary and missin…
carlos-alm Jul 5, 2026
8bb5893
fix: eliminate non-deterministic ordering in community detection
carlos-alm Jul 6, 2026
46037b1
fix: sync update-graph.sh hook extension allowlist with EXTENSIONS
carlos-alm Jul 6, 2026
6a84cf9
fix: recompute directory structure metrics for affected directories o…
carlos-alm Jul 6, 2026
8d936d1
refactor: register hardcoded execFileSync/execSync maxBuffer values i…
carlos-alm Jul 6, 2026
11c84be
fix: gate blast-radius check on newly introduced risk, not pre-existi…
carlos-alm Jul 6, 2026
963301a
fix: gate identifier-argument dynamic call edges on callback-acceptin…
carlos-alm Jul 6, 2026
3e8035d
fix: gate Array.from's callback arg by position, not just callee name
carlos-alm Jul 6, 2026
879635e
fix: scope reexportedSymbols to actually-named re-export specifiers
carlos-alm Jul 6, 2026
0fe9fc3
fix: stop CFG block/edge count from overriding AST-derived cyclomatic…
carlos-alm Jul 6, 2026
ccf3c19
fix: backfill edges.technique for incrementally-inserted calls edges
carlos-alm Jul 6, 2026
2b4d65f
fix: scope findOneHopReverseDepFiles to calls edges only (#1853)
carlos-alm Jul 6, 2026
e1ec018
fix: resolve merge conflicts with main (docs check acknowledged)
carlos-alm Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion src/domain/graph/builder/incremental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { bulkNodeIdsByFile, purgeFileData } from '../../../db/index.js';
import { debug, warn } from '../../../infrastructure/logger.js';
import { normalizePath } from '../../../shared/constants.js';
import { normalizePath, TS_NATIVE_CONFIDENCE_FLOOR } from '../../../shared/constants.js';
import type {
BetterSqlite3Database,
EngineOpts,
Expand Down Expand Up @@ -786,6 +786,66 @@ function buildCallEdges(
return edgesAdded;
}

// ── technique backfill (#1744) ──────────────────────────────────────────

/**
* Backfill `technique = 'ts-native'` on `calls` edges just written by
* `buildCallEdges`/`emitIncrementalCallEdges` above, which insert edges via
* `stmts.insertEdge.run(...)` without ever setting `technique`, leaving it
* NULL.
*
* `'ts-native'` is not an engine marker — it is the resolution-technique
* label the full-build paths apply to every directly name/type-resolved
* `calls` edge, in both the WASM/JS pipeline (`emitDirectCallEdgesForCall` in
* stages/build-edges.ts) and the native pipeline (`buildCallEdgesNative`,
* same file), as opposed to `'points-to'` (alias/pts fallback) or `'cha'` /
* `'super-dispatch'` (virtual-dispatch expansion). `incremental.ts`'s call
* resolution (`resolveCallTargets` + the same-class/defineProperty
* fallbacks in `applyThisReceiverFallbacks`) implements only that same
* direct-resolution cascade — it has no pts or CHA/RTA post-pass of its own
* — so every edge it emits is the direct-resolution case and always
* belongs under `'ts-native'`, regardless of which engine parsed the file.
*
* Mirrors `applyEdgeTechniquesAfterNativeInsert` (full-build JS pipeline,
* stages/build-edges.ts) and `backfillEdgeTechniquesAfterNativeOrchestrator`
* (stages/native-orchestrator.ts): scope to the just-rebuilt files' source
* nodes, backfill NULL technique to 'ts-native', then lift any resulting
* ts-native edge below the confidence floor up to it — so a `calls` edge's
* `technique` (and, transitively, its confidence floor) no longer depends on
* whether it was last touched by a full build or a single-file watch rebuild.
*
* Scoped to `touchedFiles` (the rebuilt file + any reverse-dep cascade
* files), not a full-table scan. Chunked to stay within SQLite's
* SQLITE_LIMIT_VARIABLE_NUMBER (999 on older builds).
*/
function backfillIncrementalEdgeTechniques(
db: BetterSqlite3Database,
touchedFiles: readonly string[],
): void {
if (touchedFiles.length === 0) return;
const CHUNK_SIZE = 500;
const tx = db.transaction(() => {
for (let i = 0; i < touchedFiles.length; i += CHUNK_SIZE) {
const chunk = touchedFiles.slice(i, i + CHUNK_SIZE);
const placeholders = chunk.map(() => '?').join(',');
db.prepare(
`UPDATE edges SET technique = 'ts-native'
WHERE kind = 'calls' AND technique IS NULL
AND source_id IN (SELECT id FROM nodes WHERE file IN (${placeholders}))`,
).run(...chunk);
// Lift resolved ts-native edges below the confidence floor for this
// chunk, matching the floor lift the full-build native paths apply.
db.prepare(
`UPDATE edges SET confidence = ?
WHERE kind = 'calls' AND technique = 'ts-native'
AND confidence > 0 AND confidence < ?
AND source_id IN (SELECT id FROM nodes WHERE file IN (${placeholders}))`,
).run(TS_NATIVE_CONFIDENCE_FLOOR, TS_NATIVE_CONFIDENCE_FLOOR, ...chunk);
}
});
tx();
}

// ── Main entry point ────────────────────────────────────────────────────

/** Build the "this file was deleted" result returned by `rebuildFile`. */
Expand Down Expand Up @@ -1008,6 +1068,13 @@ export async function rebuildFile(
cache,
);
edgesAdded += cascadeEdges;

// Backfill technique='ts-native' (and the confidence floor) for this
// rebuild's calls edges — buildCallEdges above inserts edges without a
// technique value, unlike a full rebuild of either engine, which always
// tags directly-resolved calls edges 'ts-native' (#1744).
backfillIncrementalEdgeTechniques(db, [relPath, ...reverseDeps]);

// Include pre-deletion edge counts from reverse deps so the net delta
// (edgesAdded - edgesBefore) is correct even when the cascade re-inserts
// their edges unchanged.
Expand Down
54 changes: 49 additions & 5 deletions src/domain/graph/builder/stages/native-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1834,6 +1834,43 @@ async function backfillNativeDroppedFiles(
cleanupThisDispatchWasmTrees(wasmResults);
}

/**
* One-hop reverse dependents of `files`: files with a `calls` edge whose
* TARGET is a node inside one of `files`. The Rust incremental pipeline's own
* reverse-dep cascade (`reconnect_reverse_dep_edges` in detect_changes.rs)
* re-creates edges FROM these files INTO the changed files whenever the
* changed files' nodes are purged + reinserted with new IDs — without ever
* writing `technique` — so `backfillEdgeTechniquesAfterNativeOrchestrator`
* must reach them too, not just `changedFiles` itself (#1744). Mirrors the
* one-hop `findReverseDeps` query in `builder/incremental.ts`.
*
* Scoped to `e.kind = 'calls'` since that's the only edge kind the caller's
* backfill UPDATE touches — including other edge kinds (`imports`,
* `reexports`, `inherits`, ...) would only widen `scopeFiles` with files that
* have nothing for the UPDATE to change.
*
* Chunked to stay within SQLite's SQLITE_LIMIT_VARIABLE_NUMBER (999 on older
* builds).
*/
function findOneHopReverseDepFiles(db: BetterSqlite3Database, files: readonly string[]): string[] {
const found = new Set<string>();
const CHUNK_SIZE = 500;
for (let i = 0; i < files.length; i += CHUNK_SIZE) {
const chunk = files.slice(i, i + CHUNK_SIZE);
const placeholders = chunk.map(() => '?').join(',');
const rows = db
.prepare(
`SELECT DISTINCT n_src.file AS file FROM edges e
JOIN nodes n_src ON e.source_id = n_src.id
JOIN nodes n_tgt ON e.target_id = n_tgt.id
WHERE e.kind = 'calls' AND n_tgt.file IN (${placeholders}) AND n_src.kind != 'directory'`,
)
.all(...chunk) as Array<{ file: string }>;
for (const r of rows) found.add(r.file);
}
return [...found];
}

/**
* Backfill the `technique` column on `calls` edges written by the native Rust
* orchestrator, which does not write the column itself. Also lifts any
Expand All @@ -1842,8 +1879,9 @@ async function backfillNativeDroppedFiles(
* reflected in the call-confidence metric.
*
* For full builds, all `calls` edges in the DB are new so a global UPDATE is
* correct. For incremental builds, only changed-file source nodes are updated
* to avoid overwriting previously-set technique values on unchanged edges.
* correct. For incremental builds, only changed-file source nodes — plus
* their one-hop reverse dependents (#1744) — are updated to avoid overwriting
* previously-set technique values on unchanged edges.
*/
function backfillEdgeTechniquesAfterNativeOrchestrator(
db: BetterSqlite3Database,
Expand All @@ -1868,12 +1906,18 @@ function backfillEdgeTechniquesAfterNativeOrchestrator(
).run(TS_NATIVE_CONFIDENCE_FLOOR, TS_NATIVE_CONFIDENCE_FLOOR);
return;
}
// Incremental: scope to source nodes whose file is one of the changed files.
// Incremental: scope to source nodes whose file is one of the changed files,
// plus one-hop reverse dependents — callers whose outgoing edges into a
// changed file were reconnected by Rust's own cascade and so also carry a
// fresh, untagged technique value (#1744).
const scopeFiles = [
...new Set([...changedFiles, ...findOneHopReverseDepFiles(db, changedFiles)]),
];
// Chunk to stay within SQLite's SQLITE_LIMIT_VARIABLE_NUMBER (999 on older builds).
const CHUNK_SIZE = 500;
const tx = db.transaction(() => {
for (let i = 0; i < changedFiles.length; i += CHUNK_SIZE) {
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
for (let i = 0; i < scopeFiles.length; i += CHUNK_SIZE) {
const chunk = scopeFiles.slice(i, i + CHUNK_SIZE);
const placeholders = chunk.map(() => '?').join(',');
db.prepare(
`UPDATE edges SET technique = 'ts-native'
Expand Down
7 changes: 4 additions & 3 deletions src/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ export const EXTENSIONS: ArrayCompatSet<string> = withArrayCompat(new Set(SUPPOR
* (confidence = 0.0) are intentionally excluded and must remain at 0.0 so
* they stay below DEFAULT_MIN_CONFIDENCE and never surface in normal queries.
*
* Used in `build-edges.ts` (in-memory + `applyEdgeTechniquesAfterNativeInsert`)
* and `native-orchestrator.ts` (`backfillEdgeTechniquesAfterNativeOrchestrator`).
* Centralised here so all three insertion paths apply the same value.
* Used in `build-edges.ts` (in-memory + `applyEdgeTechniquesAfterNativeInsert`),
* `native-orchestrator.ts` (`backfillEdgeTechniquesAfterNativeOrchestrator`),
* and `incremental.ts` (`backfillIncrementalEdgeTechniques`, #1744).
* Centralised here so all insertion paths apply the same value.
*/
export const TS_NATIVE_CONFIDENCE_FLOOR = 0.5;

Expand Down
Loading
Loading