fix(dedup): preserve pre-existing self-loops during entity deduplication

This commit is contained in:
abhirupone committed 2026-09-25 15:13:18 +05:30
1 parent 4c735618f3
commit 80c0d8b1ff
2 files changed
+46 -1

No files matched your search

+4 -1
View File
@@ -1030,7 +1030,10 @@ def deduplicate_entities(
# Remove legacy keys so they don't leak into edge attrs in graph.json.
e.pop("from", None)
e.pop("to", None)
if e["source"] != e["target"]:
# Drop only self-loops created by the merge (an edge whose distinct
# endpoints collapsed into one node); preserve pre-existing self-loops
# (e.g. recursive calls, self-referencing foreign keys) (#3809).
if e["source"] != e["target"] or src == tgt:
deduped_edges.append(e)
return deduped_nodes, deduped_edges
+42
View File
@@ -94,6 +94,48 @@ def test_self_loops_dropped_after_merge():
assert result_edges == []
def test_preexisting_self_loops_preserved_after_unrelated_merge():
# #3809: Pre-existing self-loops (e.g. recursive calls, self-referencing FKs)
# must survive entity deduplication even when an unrelated merge occurs.
# Only self-loops created by the merge itself should be dropped.
nodes = [
{"id": "walk", "label": "walk()", "source_file": "src/tree.py", "file_type": "code"},
{"id": "categories", "label": "categories", "source_file": "schema.sql", "file_type": "code"},
{"id": "cache", "label": "Cache", "source_file": "docs/guide.md", "file_type": "concept"},
{"id": "cache_2", "label": "cache", "source_file": "docs/guide.md", "file_type": "concept"},
]
edges = [
{"source": "walk", "target": "walk", "relation": "calls"},
{"source": "categories", "target": "categories", "relation": "references"},
# An edge between the duplicate concepts that collapses upon merge
{"source": "cache_2", "target": "cache", "relation": "references"},
]
_, result_edges = deduplicate_entities(nodes, edges, communities={})
loops = [(e["source"], e["target"], e.get("relation")) for e in result_edges if e["source"] == e["target"]]
assert ("walk", "walk", "calls") in loops
assert ("categories", "categories", "references") in loops
# The collapsed edge cache_2 -> cache must NOT produce a self-loop
assert len(loops) == 2
def test_preexisting_self_loop_on_merged_node_rewires_to_winner():
# If a merged node itself had a pre-existing self-loop, the self-loop
# is rewired to the winner, not dropped.
nodes = [
{"id": "cache", "label": "Cache", "source_file": "docs/guide.md", "file_type": "concept"},
{"id": "cache_2", "label": "cache", "source_file": "docs/guide.md", "file_type": "concept"},
]
edges = [
{"source": "cache_2", "target": "cache_2", "relation": "references"},
]
_, result_edges = deduplicate_entities(nodes, edges, communities={})
assert len(result_edges) == 1
assert result_edges[0]["source"] == "cache"
assert result_edges[0]["target"] == "cache"
def test_community_boost_aids_merge():
# Two nodes in same community with score in 0.75-0.85 zone get boosted
nodes = _make_nodes("AuthManager", "Auth Manager")