fix: correct blast-radius/fn-impact computation for line-shifted declarations on incremental rebuild#1866
Conversation
…arations on incremental rebuild
Root cause: reconnectReverseDepEdges (build-edges.ts, WASM/JS engine) and
its native mirror reconnect_reverse_dep_edges (crates/codegraph-core's
detect_changes.rs) re-attach a reverse-dependency caller's edge to its
purged-and-reinserted target using only (name, kind, file) plus "nearest
to the old line" as a tiebreak. When a file contains multiple distinct
symbols sharing the same name and kind -- e.g. several object-literal
close() {} methods returned from different functions in the same file, a
pattern this repo's own src/db/connection.ts uses four times -- nearest-
line is not a reliable way to tell them apart: once unrelated code
shifts the whole same-named group, an old reference line can end up
numerically closer to a different sibling's new line than to its own,
silently re-attaching the edge to the wrong symbol (and collapsing
distinct edges together via INSERT OR IGNORE while leaving another
candidate untargeted). A full rebuild is immune because it re-resolves
every call from scratch using real call-site information, not line
proximity.
Root-caused by replaying this repo's own last 35 real commits as a
sequence of incremental builds and diffing the result against a full
rebuild of the identical final source: node tables came out byte-
identical, but 5 reverse-dep callers ended up wired to close@line 433
instead of the correct close@line 580.
Fixed by recording each target's 1-based ordinal rank (by line) among
its same-(name,kind) siblings at save time, and using that ordinal --
not line proximity -- to re-select the correct candidate after
purge+reinsert, falling back to nearest-line only when the sibling
count itself changed since save (a genuinely ambiguous case).
Changes:
- src/domain/graph/builder/context.ts: extend savedReverseDepEdges with
tgtOrdinal/tgtSiblingCount.
- src/domain/graph/builder/stages/detect-changes.ts: compute each
target's ordinal/sibling-count before purge (computeNodeOrdinals).
- src/domain/graph/builder/stages/build-edges.ts: reconnect using the
saved ordinal instead of nearest-line (pickReconnectTarget).
- crates/codegraph-core/.../detect_changes.rs: identical fix on the
native engine (compute_ordinals, pick_reconnect_target), plus Rust
unit/integration tests covering the ordinal match, the sibling-count-
changed fallback, and a full save/purge/reconnect round trip.
Native engine: fixed in source and manually verified by full read-
through (borrow-checker/type correctness), but NOT compiled or run via
cargo test in this session -- the environment's disk ran critically low
(shared machine, other concurrent sessions) and a from-scratch napi/
cargo build for this crate was not safe to attempt. The TS-side fix is
complete, tested (full suite green), and independently verified via a
controlled A/B swap against the pre-fix code reproducing the exact
divergence this fix resolves.
A separate, pre-existing bug was found and filed independently
(#1863): resolveByGlobal's receiver-less call
resolution matches every same-named candidate clearing a loose
directory-proximity confidence threshold and creates an edge to each of
them, rather than picking the best match. That bug reproduces on a
from-scratch full build too (not incremental-specific) and is out of
scope for this fix.
docs check acknowledged: internal correctness fix to incremental-build
edge reconnection; no CLI surface, language support, or documented
architecture/design decision changed.
Fixes #1752
Impact: 5 functions changed, 12 affected
Greptile SummaryThis PR fixes a silent edge-mismatch bug in incremental rebuild: when multiple nodes in a file share the same name and kind (e.g. four
Confidence Score: 5/5Safe to merge — the fix is self-contained, both engines are updated in lockstep, and the change introduces no new failure modes on the hot path. The ordinal-rank approach is algorithmically sound: ordinals are computed from the pre-purge DB snapshot, candidates are queried post-reinsert with the same ascending-line sort, and the index arithmetic is straightforward. Both the TypeScript (WASM) and Rust (native) implementations are structurally identical and verified against each other. The known remaining gap (sibling count changes) is explicitly documented, filed as #1865, and handled gracefully via the nearest-line fallback rather than silently corrupting data. Test coverage is thorough: four focused Rust unit tests, one Rust end-to-end integration test, and one WASM integration test that compares against a fresh full-rebuild ground truth — exactly the right shape for catching regressions in this area. No files require special attention. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant D as detect-changes / detect_changes.rs
participant DB as SQLite (graph.db)
participant B as build-edges / reconnect fn
Note over D,DB: Incremental rebuild — changed file detected
D->>DB: "computeNodeOrdinals(file)<br/>SELECT name, kind, line FROM nodes WHERE file=?"
DB-->>D: rows (pre-purge snapshot)
Note over D: Group by (name,kind), sort by line,<br/>assign 1-based ordinals
D->>DB: saveEdgesStmt: SELECT reverse-dep edges for file
DB-->>D: rows with source_id, tgt_name, tgt_kind, tgt_line, …
Note over D: Attach tgtOrdinal + tgtSiblingCount<br/>from ordinals map → savedReverseDepEdges
D->>DB: "purge: DELETE nodes WHERE file=?<br/>(and their edges)"
D->>DB: re-insert updated nodes (new IDs, shifted lines)
B->>DB: "candidatesStmt: SELECT id, line FROM nodes<br/>WHERE name=? AND kind=? AND file=? ORDER BY line"
DB-->>B: candidates list (post-reinsert, cached per group)
Note over B: pickReconnectTarget:<br/>if count unchanged → use saved ordinal<br/>else → nearest-line fallback
B->>DB: INSERT OR IGNORE INTO edges (source_id, new_target_id, …)
Note over B: Edge reconnected to the CORRECT sibling
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant D as detect-changes / detect_changes.rs
participant DB as SQLite (graph.db)
participant B as build-edges / reconnect fn
Note over D,DB: Incremental rebuild — changed file detected
D->>DB: "computeNodeOrdinals(file)<br/>SELECT name, kind, line FROM nodes WHERE file=?"
DB-->>D: rows (pre-purge snapshot)
Note over D: Group by (name,kind), sort by line,<br/>assign 1-based ordinals
D->>DB: saveEdgesStmt: SELECT reverse-dep edges for file
DB-->>D: rows with source_id, tgt_name, tgt_kind, tgt_line, …
Note over D: Attach tgtOrdinal + tgtSiblingCount<br/>from ordinals map → savedReverseDepEdges
D->>DB: "purge: DELETE nodes WHERE file=?<br/>(and their edges)"
D->>DB: re-insert updated nodes (new IDs, shifted lines)
B->>DB: "candidatesStmt: SELECT id, line FROM nodes<br/>WHERE name=? AND kind=? AND file=? ORDER BY line"
DB-->>B: candidates list (post-reinsert, cached per group)
Note over B: pickReconnectTarget:<br/>if count unchanged → use saved ordinal<br/>else → nearest-line fallback
B->>DB: INSERT OR IGNORE INTO edges (source_id, new_target_id, …)
Note over B: Edge reconnected to the CORRECT sibling
Reviews (1): Last reviewed commit: "fix: correct blast-radius/fn-impact comp..." | Re-trigger Greptile |
Codegraph Impact Analysis5 functions changed → 12 callers affected across 5 files
|
Summary
reconnectReverseDepEdges/reconnect_reverse_dep_edgesre-attach a reverse-dep caller's edge to its purged-and-reinserted target using(name, kind, file)plus nearest-old-line as a tiebreak. When unrelated code shifts a whole group of same-named/same-kind siblings (e.g.connection.tshas four object-literalclose() {}methods), an old reference line can end up numerically closer to the wrong sibling's new line, silently reattaching the edge to it — losing edges on one sibling while duplicating them on another.checkMaxBlastRadius/fnImpactDataare pure node-ID graph traversal with no line-keyed logic — confirmed the defect is entirely upstream in edge reconnection, no interaction with titan-gate: flat blast-radius threshold false-positives on additive-only changes to high-fan-in core functions #1740's diff-handling changes in the same file.computeNodeOrdinals/compute_ordinals), and reconnecting using that saved ordinal instead of line proximity (pickReconnectTarget/pick_reconnect_target), falling back to nearest-line only if the sibling count itself changed.Closes #1752
Test plan
npm test— full suite green (3502 passed, 0 failed)npm run lint— cleancargo test --release— 443 passedFiled #1865 for a genuinely separate, pre-existing (not a regression) gap found during stress-testing: when a same-named/same-kind sibling is also added or removed in the same edit that shifts the group, the ordinal-based fix's own line-proximity fallback still diverges from a full rebuild — suggests per-declaration content hashing as the fix.
Stacked on #1862 (base branch
fix/issue-1751-closedbpair-test-coverage) — only the builder/detect-changes/test diff is this issue's change.