Skip to content

fix: correct blast-radius/fn-impact computation for line-shifted declarations on incremental rebuild#1866

Closed
carlos-alm wants to merge 1 commit into
fix/issue-1751-closedbpair-test-coveragefrom
fix/issue-1752-blast-radius-incremental-stale
Closed

fix: correct blast-radius/fn-impact computation for line-shifted declarations on incremental rebuild#1866
carlos-alm wants to merge 1 commit into
fix/issue-1751-closedbpair-test-coveragefrom
fix/issue-1752-blast-radius-incremental-stale

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • reconnectReverseDepEdges/reconnect_reverse_dep_edges re-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.ts has four object-literal close() {} 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/fnImpactData are 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.
  • Fixed by recording each target's 1-based ordinal rank among same-(name,kind) siblings before purge (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 — clean
  • cargo test --release — 443 passed
  • Native addon rebuilt, codesigned, verified directly
  • A/B proof on both engines: reverted to pre-fix source, rebuilt, reproduced the exact bug signature; restored fix, reconfirmed pass
  • Scenario matrix: 3 shift positions × 4 touched-sibling variants × 2 engines = 24 combinations, all pass against full-rebuild ground truth
  • 5 repeated cycles + a 3-step chained incremental sequence (no full rebuild in between) — stable

Filed #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.

…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-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 close() {} methods in connection.ts), the old nearest-line heuristic could reconnect a saved reverse-dep edge to the wrong sibling after a purge-and-reinsert cycle shifted the whole group. The fix records each target's 1-based ordinal rank among same-(name,kind) siblings before purge and uses that ordinal — not line proximity — to reselect the correct candidate after reinsertion.

  • Both engines updated in lockstep: detect-changes.ts + build-edges.ts (WASM/JS) and detect_changes.rs (native) receive identical computeNodeOrdinals/pickReconnectTarget logic, so the fix applies regardless of which engine is active.
  • Candidate-list caching added: reconnectReverseDepEdges and reconnect_reverse_dep_edges now cache the SELECT id, line FROM nodes … query result per (name, kind, file) group, avoiding redundant DB round-trips when multiple callers share a target.
  • Test coverage: Four Rust unit tests cover pick_reconnect_target and compute_ordinals in isolation, plus a full end-to-end Rust integration test and a WASM-path integration test (issue-1752-reverse-dep-sibling-reconnect.test.ts) that replays the exact real-world bug shape against a fresh full-rebuild ground truth.

Confidence Score: 5/5

Safe 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

Filename Overview
crates/codegraph-core/src/domain/graph/builder/stages/detect_changes.rs Adds compute_ordinals, pick_reconnect_target, and updates save/reconnect functions with ordinal-based logic and candidate caching; comprehensive unit + integration tests included.
src/domain/graph/builder/stages/detect-changes.ts Adds computeNodeOrdinals and wires tgtOrdinal/tgtSiblingCount into savedReverseDepEdges before purge; mirrors the Rust implementation faithfully.
src/domain/graph/builder/stages/build-edges.ts Adds pickReconnectTarget and refactors reconnectReverseDepEdges to use ordinal-based selection with a per-group candidate cache; logic is correct and consistent with the Rust mirror.
src/domain/graph/builder/context.ts Adds tgtOrdinal and tgtSiblingCount fields to the SavedReverseDepEdge interface; straightforward type update.
tests/integration/issue-1752-reverse-dep-sibling-reconnect.test.ts New WASM-path regression test that builds incrementally, then compares target lines against an independent full-rebuild reference; also guards against the edge-collapsing variant of the bug.

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
Loading
%%{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
Loading

Reviews (1): Last reviewed commit: "fix: correct blast-radius/fn-impact comp..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

5 functions changed12 callers affected across 5 files

  • PipelineContext in src/domain/graph/builder/context.ts:21 (4 transitive callers)
  • pickReconnectTarget in src/domain/graph/builder/stages/build-edges.ts:1991 (3 transitive callers)
  • reconnectReverseDepEdges in src/domain/graph/builder/stages/build-edges.ts:2024 (3 transitive callers)
  • computeNodeOrdinals in src/domain/graph/builder/stages/detect-changes.ts:395 (4 transitive callers)
  • addReverseDeps in src/domain/graph/builder/stages/detect-changes.ts:438 (4 transitive callers)

@carlos-alm carlos-alm deleted the branch fix/issue-1751-closedbpair-test-coverage July 6, 2026 18:24
@carlos-alm carlos-alm closed this Jul 6, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 6, 2026
@carlos-alm

Copy link
Copy Markdown
Contributor Author

Recreated as #1916 — this PR was inadvertently closed when its stacked base branch was deleted after #1910 merged. Continuing there.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant