fix(dedup): pick the richer duplicate as survivor and keep the losers' fields (#3372) - #3425
fix(dedup): pick the richer duplicate as survivor and keep the losers' fields (#3372)#3425abhay-codes07 wants to merge 1 commit into
Conversation
…' fields (Graphify-Labs#3372) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Reworks dedup survivor selection in _pick_winner to rank candidates by content richness before falling back to ID length, so the established, field-rich node wins instead of whichever duplicate has the shortest ID; the chunk-suffix check still takes precedence, and ID length only breaks ties between equally-rich nodes. Adds _content_richness, which scores populated non-identity/placement fields and weights attributes and _merged_from by entry count. Back-fills the chosen survivor with any fields only its losers carried via _merge_missing_attributes (never-override, deterministic by unique_nodes order), substituting the enriched node into the returned list so previously-discarded loser data is preserved.
Worth a look
- _pick_winner survivor and _collision_rank survivor can disagree, causing edges to rewire to a dropped node —
graphify/dedup.py:864· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 418 functions depend on the 71 functions this change touches.
Health — this change adds coupling hotspots:
- new:
deduplicate_entities()— 75 callers, 22 callees - new:
build_merge()— 62 callers, 13 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
build()— 40 callers, 4 callees - new:
_prune()— 9 callers, 3 callees - new:
_llm_tiebreak()— 1 callers, 10 callees - new:
test_poisoned_manifest_is_healed()— 0 callers, 6 callees
Verification — 418 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 197 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
15 of 260 test file(s) selected (6%) via static blast radius.
tests/test_build.py— impacttests/test_build_merge_hyperedges_and_prune.py— impacttests/test_build_merge_shrink_guard.py— impacttests/test_carried_hyperedge_remap.py— impacttests/test_corrupt_graph_json.py— impacttests/test_cross_extension_reexport_self_cycle.py— impacttests/test_dedup.py— impacttests/test_dedup_remaps_hyperedges.py— impacttests/test_dedup_survivor_richness.py— impact, changed-testtests/test_global_graph.py— impacttests/test_go_qualified_resolution.py— impacttests/test_no_dedup_flag.py— impacttests/test_non_string_node_ids.py— impacttests/test_prune_sweeps_orphans.py— impacttests/test_unverified_semantic_shrink.py— impact
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_pick\_winner.
The verifier did not have enough to check \_pick\_winner, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
· 7 more finding(s) on lines outside this diff (see the check run).
|
On the advisory: the |
|
Shipped in v0.9.57 — landed on |
Closes #3372.
The problem
_pick_winnerchose the dedup survivor by ID shape alone — no chunk suffix, then shorter ID. A passing one-line mention of an entity on a shallow page mints a shorter node id than the entity's dedicated page in a nested category folder, so the shallow stub won every time the pattern occurred, and the losers are dropped wholesale in the component collapse — the established node'sdescription,attributes,confidenceand_merged_fromhistory vanished from the graph. Reproduced on 0.9.56 with the issue's two-node shape: thesources/notes.mdstub survives and every rich field is gone.The change
Two complementary pieces, both in
dedup.py:_content_richnesscounts populated fields beyond the identity/placement baseline (id, label, source_file, …), weightingattributesby entry count and_merged_fromby history length — a node that already absorbed merges is the established record._pick_winnerscores(chunk-suffix, −richness, id length): the chunk-suffix rule still dominates (a_c3fragment never wins, however rich), and ID length still breaks ties between equally-rich candidates, preserving the old deterministic ordering exactly where the old signal was actually doing its job. All 139 existing dedup tests pass unchanged._merge_missing_attributesthe same-source collision path already uses (Defaultdedup=Truediscards a same-ID node's attributes instead of merging them — LLM semantic summaries are silently lost #2091), in deterministicunique_nodesorder. Content loss becomes a union.Tests
tests/test_dedup_survivor_richness.py— 6 tests: the issue's exact shape keeps the enriched node (id, attributes, merge history intact); a field only the loser carried is folded into the survivor without overriding anything; equally-bare candidates keep the shorter-id tiebreak; a chunk-suffixed id never beats a clean one regardless of richness; the richness measure ignores placement fields; and edges rewire to the rich survivor. With the fix reverted, 4 of 6 fail (the tiebreak and chunk-suffix controls rightly keep passing). The dedup suites are unchanged (139 passed); the full suite matches a fresh same-versionv8(0.9.56) baseline.🤖 Generated with Claude Code
https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q