test(dedup): pin chained-collapse and kept-single-member for hyperedge rewire (#2805)

Adds the two cases the deep-dive flagged: a chained collapse (a_old, a_mid -> a)
lands directly on the final survivor (the whole fix rests on the union-find
remap being fully flattened), and a hyperedge collapsing to one distinct member
is kept, not dropped (pinning the deliberate sub-two-member policy). Adds the
CHANGELOG entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-08-17 15:35:53 +01:00
co-authored by Claude Opus 4.8
parent 0d145fe110
commit 5929edf984
2 changed files with 20 additions and 0 deletions
+1
View File
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## 0.9.46 (unreleased)
- Fix: when node dedup merges two nodes, any hyperedge that listed the merged-away node as a member now rewires that member to the survivor instead of silently dropping it, so a grouping no longer loses participants on dedup (#2805, thanks @abhay-codes07).
- Fix: pruning a source file now also sweeps the external-import placeholder nodes it strands at degree 0, instead of leaving them to accumulate in the node count, `GRAPH_REPORT.md`, and exports; genuinely-isolated real nodes (which carry a `source_file`) are never touched (#2807, thanks @abhay-codes07).
- Fix: when two edges connect the same node pair with different relations, the graph builder now keeps the more specific relation (`calls`, `imports`, `inherits`, ...) instead of letting a generic `references`/`uses`/`mentions` overwrite it; previously a real `calls` could be downgraded to `references` and then dropped from the call graph (#2803, thanks @abhay-codes07).
+19
View File
@@ -125,3 +125,22 @@ def test_an_empty_remap_changes_nothing():
hes = [{"id": "h", "nodes": ["a", "b", "c"]}]
_remap_hyperedge_members(hes, {})
assert hes[0]["nodes"] == ["a", "b", "c"]
def test_chained_collapse_lands_on_the_final_survivor():
"""A dedup remap built from union-find is fully flattened (path-compressed),
so a member of a chained component (a_old -> a_mid -> a) rewires directly to
the final survivor in a single lookup, never to an intermediate."""
hes = [{"id": "h", "nodes": ["a_old", "a_mid", "b"]}]
# what components()/UnionFind produces: every non-winner maps to the winner
_remap_hyperedge_members(hes, {"a_old": "a", "a_mid": "a"})
assert hes[0]["nodes"] == ["a", "b"]
def test_a_hyperedge_collapsing_to_one_member_is_kept():
"""Sub-two-member hyperedges are kept by design (build_from_json only drops
the zero-valid-member case). Pin it so a future refactor doesn't silently
start dropping a 1-member group after a collapse."""
hes = [{"id": "h", "nodes": ["a_old", "a_new"]}]
_remap_hyperedge_members(hes, {"a_old": "a", "a_new": "a"})
assert hes[0]["nodes"] == ["a"] # collapsed to one, still present