You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(daemon): stop heal loop duplicating symbols on re-extraction
The on-demand heal path re-extracted "changed" files with a bare
`extractor.extract` that never removed the file's existing nodes. Because
`insert_node` always allocates a fresh id, every heal duplicated the file's
symbols, accumulating edge-less phantom orphans that inflated the daemon's
live symbol count and orphan report relative to a clean in-process build
(orphans 933 vs 177 on a 921-file Spring monorepo).
Extract the surgical core of `reindex_file_in_place` into
`reindex_file_in_place_core` (remove old nodes -> re-extract -> re-link
cross-file edges; idempotent on an unchanged file) and route the heal loop
through it, so heal and the `reindex` IPC fast-path can no longer diverge.
Add `reindex_file_in_place_core_is_idempotent_on_unchanged_file` regression
test. Verified on the live daemon: symbol/edge counts stay stable across
repeated heal passes.
"note":"cross_file_edges_staled counts re-link ATTEMPTS that succeeded at the graph level; dedup no-ops count once per attempt, not once per underlying edge. cross_file_edges_dropped = edges whose file-side endpoint no longer exists post-reindex.",
1476
+
})
1477
+
.to_string(),
1478
+
error:None,
1479
+
}
1480
+
}
1481
+
1482
+
/// Surgical single-file reindex shared by the `reindex` IPC fast-path and the
1483
+
/// daemon's on-demand heal loop. In order: remove the file's existing nodes
1484
+
/// (which drops incident edges), re-extract the file, and re-link captured
1485
+
/// cross-file edges. Idempotent on an unchanged file — re-applying it nets zero
1486
+
/// node change — so the heal loop can re-run it on a quiescent tree without
1487
+
/// duplicating symbols. (The heal loop previously re-extracted additively,
1488
+
/// WITHOUT this REMOVE phase, leaking a fresh duplicate of every symbol in the
1489
+
/// healed file on each pass — `insert_node` always allocates a new id — which
1490
+
/// inflated the daemon's live symbol count and orphan report relative to a
1491
+
/// clean in-process build.)
1492
+
///
1493
+
/// Removes the file's old nodes (which drops all incident edges via petgraph),
1494
+
/// then re-extracts into the live graph.
1444
1495
///
1445
1496
/// Telemetry note: `edges_removed_with_evidence` reports only edges whose
1446
1497
/// `evidence_file` matched the reindexed path. `remove_node` additionally
"note":"cross_file_edges_staled counts re-link ATTEMPTS that succeeded at the graph level; dedup no-ops count once per attempt, not once per underlying edge. cross_file_edges_dropped = edges whose file-side endpoint no longer exists post-reindex.",
1701
-
})
1702
-
.to_string(),
1703
-
error:None,
1736
+
FileReindexTelemetry{
1737
+
nodes_removed,
1738
+
nodes_inserted,
1739
+
edges_removed_with_evidence,
1740
+
edges_inserted,
1741
+
cross_file_edges_staled,
1742
+
cross_file_edges_dropped,
1743
+
file_missing: source.is_none(),
1704
1744
}
1705
1745
}
1706
1746
@@ -3815,6 +3855,83 @@ mod tests {
3815
3855
);
3816
3856
}
3817
3857
3858
+
/// Regression guard for the daemon heal-loop symbol-duplication bug.
3859
+
///
3860
+
/// The on-demand heal path re-runs `reindex_file_in_place_core` on every
3861
+
/// file `filter_real_changes` reports. That primitive MUST be idempotent on
3862
+
/// an unchanged file — REMOVE the file's nodes, then re-extract — so a heal
3863
+
/// over a quiescent tree nets zero node change. The heal loop previously used
3864
+
/// a bare additive `extractor.extract`; because `insert_node` always
3865
+
/// allocates a fresh id and never removes the old nodes, every heal
3866
+
/// duplicated the file's symbols, leaking edge-less phantom orphans that
3867
+
/// inflated the daemon's live symbol count and orphan report relative to a
3868
+
/// clean in-process build. This test pins the idempotency invariant and
3869
+
/// contrasts it with the additive path that caused the leak.
0 commit comments