Summary
Follow-up from #1865 (fixed by PR #2014). The fix there replaces the ordinal/nearest-line heuristic with a dominant-line-shift alignment that correctly reconnects reverse-dep edges when a same-(name, kind) sibling group shifts AND its size changes by a same-named sibling being added or removed in the same edit — verified for both the add-only and remove-only cases on both engines.
There is one remaining theoretical gap, not covered by #1865's literal reproduction: if a same-named/same-kind sibling is both added and removed in the same edit (e.g. sibling B is renamed away while a new, unrelated sibling E is added elsewhere in the same group), the group's sibling count stays the same (e.g. 4 -> 4), so the alignment takes the "count unchanged -> match by rank" fast path — but the identity has actually changed. Since nothing about raw line numbers can distinguish "this is genuinely the same declaration, just shifted" from "a different declaration happens to now occupy this rank," this compound case can still silently mis-wire an edge.
This is a fundamentally different class of gap than #1865 — it requires an actual identity signal beyond line position, e.g. per-declaration content hashing (hash each sibling's own body text, persisted per-node so it survives a purge+reinsert cycle), as originally suggested as a possible direction in #1865's own issue text. It would need a new persisted column on nodes (and its Rust migration mirror), populated at node-insertion time in both engines, and is a substantially larger, schema-touching change — out of scope for a targeted line-alignment fix.
Reproduction (theoretical — not yet verified with a concrete fixture)
- File
conn.ts has 4 same-named/same-kind siblings (e.g. close() in openA..openD).
- Edit: rename
openB's close to shutdown (removes one from the group) AND simultaneously add a new close() to a 5th function openE inserted at a line position that keeps the total group size at 4.
- Incremental rebuild: the ordinal-by-rank fast path (count unchanged: 4 -> 4) will match old rank i to new rank i, but if
openE's new close() lands at the same rank openB's old close() held, a reverse-dep caller of B could get silently reconnected to E instead of dropped.
Suggested direction
Per-declaration content hashing: compute a hash of each declaration's own source text (line..endLine) at insertion time, persist it on the node row, and use it as the primary reconnect-matching signal (falling back to the #1865 alignment when hashes are unavailable, e.g. on DBs from before this feature). Needs a schema migration in both src/db/migrations.ts and crates/codegraph-core/src/db/connection.rs, and hash computation wired into both the WASM/JS and native node-insertion paths.
How this was found
Identified during design of the #1865 fix — the dominant-shift alignment provably handles any count change correctly (verified via unit tests using this repo's own real fixture line numbers), but a net-zero-count compound edit is an inherent blind spot for any line-position-only heuristic, not a bug in that specific fix.
Summary
Follow-up from #1865 (fixed by PR #2014). The fix there replaces the ordinal/nearest-line heuristic with a dominant-line-shift alignment that correctly reconnects reverse-dep edges when a same-(name, kind) sibling group shifts AND its size changes by a same-named sibling being added or removed in the same edit — verified for both the add-only and remove-only cases on both engines.
There is one remaining theoretical gap, not covered by #1865's literal reproduction: if a same-named/same-kind sibling is both added and removed in the same edit (e.g. sibling B is renamed away while a new, unrelated sibling E is added elsewhere in the same group), the group's sibling count stays the same (e.g. 4 -> 4), so the alignment takes the "count unchanged -> match by rank" fast path — but the identity has actually changed. Since nothing about raw line numbers can distinguish "this is genuinely the same declaration, just shifted" from "a different declaration happens to now occupy this rank," this compound case can still silently mis-wire an edge.
This is a fundamentally different class of gap than #1865 — it requires an actual identity signal beyond line position, e.g. per-declaration content hashing (hash each sibling's own body text, persisted per-node so it survives a purge+reinsert cycle), as originally suggested as a possible direction in #1865's own issue text. It would need a new persisted column on
nodes(and its Rust migration mirror), populated at node-insertion time in both engines, and is a substantially larger, schema-touching change — out of scope for a targeted line-alignment fix.Reproduction (theoretical — not yet verified with a concrete fixture)
conn.tshas 4 same-named/same-kind siblings (e.g.close()inopenA..openD).openB'sclosetoshutdown(removes one from the group) AND simultaneously add a newclose()to a 5th functionopenEinserted at a line position that keeps the total group size at 4.openE's new close() lands at the same rank openB's old close() held, a reverse-dep caller of B could get silently reconnected to E instead of dropped.Suggested direction
Per-declaration content hashing: compute a hash of each declaration's own source text (line..endLine) at insertion time, persist it on the node row, and use it as the primary reconnect-matching signal (falling back to the #1865 alignment when hashes are unavailable, e.g. on DBs from before this feature). Needs a schema migration in both
src/db/migrations.tsandcrates/codegraph-core/src/db/connection.rs, and hash computation wired into both the WASM/JS and native node-insertion paths.How this was found
Identified during design of the #1865 fix — the dominant-shift alignment provably handles any count change correctly (verified via unit tests using this repo's own real fixture line numbers), but a net-zero-count compound edit is an inherent blind spot for any line-position-only heuristic, not a bug in that specific fix.