Skip to content

Commit 431c0e5

Browse files
committed
fix: reconnect reverse-dep edges correctly when a sibling group's size also changes
reconnectReverseDepEdges (build-edges.ts) and its native mirror reconnect_reverse_dep_edges (detect_changes.rs) re-attach a reverse-dep caller's edge to its purged-and-reinserted target using a saved ordinal rank among same-(name, kind) siblings, falling back to nearest-line matching whenever the sibling count itself changed since save (a same-named sibling added or removed in the same edit). That fallback is exactly the nearest-line heuristic #1752 already proved unreliable once a same-named group shifts far enough, so the compound scenario (shift + count change in one incremental build) still produced call edges that diverged from a full rebuild. Replaces the ordinal/nearest-line two-tier heuristic with a single alignment: match old-to-new siblings by rank when the sibling count is unchanged (subsumes #1752's fix exactly), or by the single dominant line-shift that best explains the surviving (untouched) siblings when the count changed. Untouched siblings' own source text is never edited, so they all shift by the exact same delta regardless of how far the group has moved — finding that shift correctly identifies which old sibling was removed (or which new one was added) without guessing based on raw line proximity, which a uniform shift can fool (confirmed against this repo's own real #1752 fixture numbers, where a naive minimum-distance alignment picks the wrong element to drop). Adds `build.reverseDepAlignmentMaxGroupSize` (default 200) bounding the alignment's shift-histogram cost for pathologically large sibling groups, mirrored in both engines' DEFAULTS.
1 parent 25ba42f commit 431c0e5

10 files changed

Lines changed: 858 additions & 166 deletions

File tree

crates/codegraph-core/src/domain/graph/builder/pipeline.rs

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -175,32 +175,50 @@ fn early_exit_result(
175175
}
176176
}
177177

178+
/// `(saved_reverse_dep_edges, saved_sibling_groups, removal_reverse_deps,
179+
/// removed_file_neighbors)` — see `save_and_purge_changed`.
180+
type SaveAndPurgeResult = (
181+
Vec<detect_changes::SavedReverseDepEdge>,
182+
HashMap<detect_changes::SiblingGroupKey, Vec<i64>>,
183+
Vec<String>,
184+
Vec<String>,
185+
);
186+
178187
/// Save reverse-dep edges (and reverse-deps of removed files) before purging
179188
/// changed files. Mirrors the JS save-then-purge sequence in `build-edges.ts`
180-
/// (#1012). Returns `(saved_reverse_dep_edges, removal_reverse_deps,
181-
/// removed_file_neighbors)` so the pipeline can reconnect edges after Stage 5,
182-
/// reclassify roles in Stage 8, and (#1839) fold a removed file's
183-
/// cross-directory neighbors into Stage 8's directory-metrics refresh.
189+
/// (#1012). Returns `(saved_reverse_dep_edges, saved_sibling_groups,
190+
/// removal_reverse_deps, removed_file_neighbors)` so the pipeline can
191+
/// reconnect edges after Stage 5, reclassify roles in Stage 8, and (#1839)
192+
/// fold a removed file's cross-directory neighbors into Stage 8's
193+
/// directory-metrics refresh.
184194
fn save_and_purge_changed(
185195
conn: &Connection,
186196
parse_changes: &[&detect_changes::ChangedFile],
187197
change_result: &detect_changes::ChangeResult,
188198
opts: &BuildOpts,
189199
root_dir: &str,
190-
) -> (Vec<detect_changes::SavedReverseDepEdge>, Vec<String>, Vec<String>) {
200+
) -> SaveAndPurgeResult {
191201
let mut saved_reverse_dep_edges: Vec<detect_changes::SavedReverseDepEdge> = Vec::new();
202+
let mut saved_sibling_groups: HashMap<detect_changes::SiblingGroupKey, Vec<i64>> =
203+
HashMap::new();
192204
let mut removal_reverse_deps: Vec<String> = Vec::new();
193205

194206
if change_result.is_full_build {
195207
let has_embeddings = detect_changes::has_embeddings(conn);
196208
detect_changes::clear_all_graph_data(conn, has_embeddings);
197-
return (saved_reverse_dep_edges, removal_reverse_deps, Vec::new());
209+
return (
210+
saved_reverse_dep_edges,
211+
saved_sibling_groups,
212+
removal_reverse_deps,
213+
Vec::new(),
214+
);
198215
}
199216

200217
let changed_paths: Vec<String> = parse_changes.iter().map(|c| c.rel_path.clone()).collect();
201218

202219
if !opts.no_reverse_deps.unwrap_or(false) {
203-
saved_reverse_dep_edges = detect_changes::save_reverse_dep_edges(conn, &changed_paths);
220+
(saved_reverse_dep_edges, saved_sibling_groups) =
221+
detect_changes::save_reverse_dep_edges(conn, &changed_paths);
204222

205223
if !change_result.removed.is_empty() {
206224
let removed_set: HashSet<String> = change_result.removed.iter().cloned().collect();
@@ -225,7 +243,12 @@ fn save_and_purge_changed(
225243
.collect();
226244
detect_changes::purge_changed_files(conn, &files_to_purge, &[]);
227245

228-
(saved_reverse_dep_edges, removal_reverse_deps, removed_file_neighbors)
246+
(
247+
saved_reverse_dep_edges,
248+
saved_sibling_groups,
249+
removal_reverse_deps,
250+
removed_file_neighbors,
251+
)
229252
}
230253

231254
/// Parse a changed-file slice in parallel and key the results by relative path.
@@ -286,11 +309,18 @@ fn resolve_pipeline_imports(
286309
fn reconnect_saved_reverse_dep_edges(
287310
conn: &Connection,
288311
saved: &[detect_changes::SavedReverseDepEdge],
312+
saved_sibling_groups: &HashMap<detect_changes::SiblingGroupKey, Vec<i64>>,
313+
max_align_group_size: usize,
289314
) {
290315
if saved.is_empty() {
291316
return;
292317
}
293-
let (reconnected, dropped) = detect_changes::reconnect_reverse_dep_edges(conn, saved);
318+
let (reconnected, dropped) = detect_changes::reconnect_reverse_dep_edges(
319+
conn,
320+
saved,
321+
saved_sibling_groups,
322+
max_align_group_size,
323+
);
294324
if dropped > 0 {
295325
eprintln!(
296326
"[codegraph] reconnect_reverse_dep_edges: {reconnected} reconnected, {dropped} dropped (target nodes not found)"
@@ -528,7 +558,7 @@ pub fn run_pipeline(
528558
// Stage 3b: save reverse-dep edges (incremental) or clear all (full),
529559
// then purge changed files. Returns the saved edges for Stage 7
530560
// reconnect and the removal reverse-dep set for Stage 8 reclassification.
531-
let (saved_reverse_dep_edges, removal_reverse_deps, removed_file_neighbors) =
561+
let (saved_reverse_dep_edges, saved_sibling_groups, removal_reverse_deps, removed_file_neighbors) =
532562
save_and_purge_changed(conn, &parse_changes, &change_result, &opts, root_dir);
533563

534564
// ── Stage 4: Parse files ───────────────────────────────────────────
@@ -639,7 +669,12 @@ pub fn run_pipeline(
639669
)
640670
.map_err(|e| format!("call edge insertion failed: {e}"))?;
641671

642-
reconnect_saved_reverse_dep_edges(conn, &saved_reverse_dep_edges);
672+
reconnect_saved_reverse_dep_edges(
673+
conn,
674+
&saved_reverse_dep_edges,
675+
&saved_sibling_groups,
676+
config.build.reverse_dep_alignment_max_group_size,
677+
);
643678

644679
// Now that edges reflect this revision, commit file_hashes for the
645680
// changed files (#1731). Deferred from Stage 5 — see the comment there.

0 commit comments

Comments
 (0)