Merge pull request #1248 from teemow/fix/dedup-pass2-winner-union

fix(dedup): pick pass-2 winner from the verified pair only (#1247)
This commit is contained in:
Safi
2026-06-11 23:21:39 +01:00
committed by GitHub
2 changed files with 35 additions and 3 deletions
+5 -3
View File
@@ -293,9 +293,11 @@ def deduplicate_entities(
sf_b = neighbor.get("source_file") or ""
if sf_a != sf_b:
continue
all_group = norm_to_nodes.get(norm_label, [node]) + \
norm_to_nodes.get(neighbor_norm, [neighbor])
winner = _pick_winner(all_group)
# Pick the winner from the verified pair only. Selecting it
# from the union of both normalized-label groups pulls
# never-compared nodes (same label, different source_file)
# into the merge, bypassing the #1046/#1178 guards.
winner = _pick_winner([node, neighbor])
uf.union(winner["id"], node_id)
uf.union(winner["id"], neighbor_id)
fuzzy_merges += 1
+30
View File
@@ -208,6 +208,36 @@ def test_prefix_extension_symbols_not_merged():
)
def test_pass2_winner_union_does_not_pull_in_uncompared_same_label_nodes():
"""Pass 2's winner selection must consider only the verified pair (#1247).
Picking the winner from the union of both normalized-label groups pulls
never-compared nodes into the merge: here A ("Session Manager", auth.md)
and B ("Session Manager", billing.md) are deliberately kept distinct by
the cross-file identical-label guards (#1046, #1178), yet when the
A-C fuzzy match ("Session Managr" typo) fires, _pick_winner over
[A, B, C] selects B (shortest id) and unions B with both A and C —
merging B although it was never compared against anything.
"""
nodes = [
{"id": "session_manager_auth", "label": "Session Manager",
"source_file": "auth.md"},
{"id": "sm", "label": "Session Manager",
"source_file": "billing.md"},
{"id": "session_managr_notes", "label": "Session Managr",
"source_file": "notes.md"},
]
result_nodes, _ = deduplicate_entities(nodes, [], communities={})
ids = {n["id"] for n in result_nodes}
# B must survive as a distinct node: identical label across different
# source files is exactly what the #1046/#1178 guards keep separate.
assert "sm" in ids, (
"uncompared cross-file node 'sm' was absorbed via pass-2 winner-union"
)
# The verified fuzzy pair (A, C) still merges — only one of them survives.
assert len(result_nodes) == 2
def test_prefix_guard_does_not_block_same_length_typos():
"""The prefix-extension guard must not fire for same-length pairs — only strict
prefix-extensions (one is a substring of the other) should be blocked (#1201).