fix(dedup): make id prefix reconstruction Unicode aware - #3559
ayushcodes10 wants to merge 7 commits into
Conversation
Toward issue 3352: _id_prefixes reimplements id slugification with its own ASCII only regex instead of the canonical normalize_id every real extractor uses to mint an id, so it silently drops non Latin characters. This adds the import with no behavior change yet; the swap lands in the next commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Fixes issue 3352. _id_prefixes reimplemented id segment slugification with its own ASCII only regex, so any path segment made of non Latin characters (Korean, CJK, Cyrillic and so on) collapsed to nothing instead of being preserved. _defines_id, the only real caller, uses these prefixes to recognize when a node's own source_file is the file its id encodes, and _collision_rank already implements the definer wins rule this issue asks for. With the wrong prefixes, a node minted from a non ASCII path never matched, so a genuine defining page lost the definer wins tiebreak and the survivor fell through to plain arrival order, exactly the symptom this issue reports. Swapping to normalize_id, the same recipe every real extractor mints an id with, makes the reconstructed prefixes match the ids that are actually minted for such a file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Confirms the exact repro shape from the issue: the reconstructed prefix set for a Korean file path preserves every segment instead of collapsing them to nothing, matching what the file's own id is actually minted as. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Confirms the layer above _id_prefixes actually benefits from the fix: a node whose id and source_file both encode a Korean path is recognized as defining that id, and a node sharing that id from a different file is correctly recognized as merely referencing it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Runs deduplicate_entities on the defining and referencing Korean node pair in both arrival orders and asserts the defining file own node survives either way, matching the concrete case the issue reports: a page whose filename equals the concept name losing its own node to a page that merely mentions it in passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Confirms the reconstructed prefix set for an ASCII path is byte for byte identical to what the old ASCII only regex produced, so the fix changes behavior only for paths the old code was already getting wrong. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. 1 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_id\_prefixes changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_id\_prefixes behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"source\_file":"'h\\u00e9llo w\\u00f6rld'"\}, the old code produced \{'h\_llo\_w\_rld'\} but the new code produces \{'héllo\_wörld'\}. Paste that input straight into a regression test.
Graphify review — findings
Fixes the definer-wins dedup tiebreak for non-Latin filenames: _id_prefixes now slugifies each path segment with the Unicode-aware normalize_id used to mint real ids, instead of an ASCII-only regex that dropped Korean/CJK/Cyrillic characters entirely and left a defining page unable to claim the id it encodes. ASCII paths produce the same prefixes as before, so the only behavioral change is that non-Latin defining pages now win the tiebreak against pages that merely reference the same entity.
No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 725 functions depend on the 429 functions this change touches.
Health — this change adds coupling hotspots:
- new:
deduplicate_entities()— 77 callers, 24 callees - new:
build_merge()— 76 callers, 14 callees - new:
build()— 52 callers, 6 callees - new:
dispatch_command()— 2 callers, 124 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 — 725 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: 494 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
275 of 275 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/test_affected_cli.py— full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— full-run-safetytests/test_astro_import_ids.py— full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— impact, full-run-safetytests/test_build_merge_dedup_scope.py— impact, full-run-safetytests/test_build_merge_hyperedges_and_prune.py— impact, full-run-safetytests/test_build_merge_shrink_guard.py— impact, full-run-safetytests/test_builtin_global_type_refs.py— full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— impact, full-run-safetytests/test_case_sensitive_resolution.py— full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— impact, full-run-safetytests/test_cpp_nested_and_cli.py— full-run-safetytests/test_cpp_objc_cross_file_calls.py— full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— impact, full-run-safetytests/test_cross_language_call_resolution.py— full-run-safetytests/test_cross_repo_member_calls.py— full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— full-run-safetytests/test_csharp_enum_members.py— full-run-safety- … and 225 more
non-code file(s) changed (
CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
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
Behavior changes: \_id\_prefixes changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_id\_prefixes behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"source\_file":"'h\\u00e9llo w\\u00f6rld'"\}, the old code produced \{'h\_llo\_w\_rld'\} but the new code produces \{'héllo\_wörld'\}. Paste that input straight into a regression test.
· 7 more finding(s) on lines outside this diff (see the check run).
|
Shipped in v0.9.62 (now on PyPI: |
Fixes #3352.
Summary
The reported symptom: when two markdown pages mention the same entity, the page that actually defines the concept can lose its own node to a page that merely mentions it in passing. The reporter's concrete case is a page named exactly after the concept (Korean filename) losing its node to an unrelated page that just references it.
dedup.pyalready has the mechanism the issue's own "Suggestion" section asks for:_collision_rankranks a node whosesource_filedefines its id (_defines_id) ahead of one that merely references it, specifically so the defining page wins. That mechanism is correct and already tested for ASCII paths. The bug is one level down:_id_prefixes— which_defines_iduses to reconstruct what id a node extracted from a givensource_filewould legitimately mint — reimplemented id slugification with its own ASCII-only regex (re.compile(r"[^a-z0-9]+")) instead of the canonicalnormalize_id()every real extractor actually mints an id with. For a path segment made of non-Latin characters (Korean, CJK, Cyrillic, ...), the ASCII regex strips it entirely instead of preserving it, so the reconstructed prefix never matches the id that was actually minted for that file._defines_idthen never recognizes the defining page as defining anything, and_collision_rankfalls through to plain arrival order — exactly the symptom reported.Verified directly against current code before writing the fix:
_id_prefixes('concepts/작업 단위 폴더 + README 진입점 컨벤션.md')returned{'concepts_readme', 'readme'}(every Korean character dropped), while the id an extractor actually mints for that path (via_file_stem+make_id) isconcepts_작업_단위_폴더_readme_진입점_컨벤션— completely different from any prefix the old code could ever reconstruct.Changes
graphify/dedup.py:_id_prefixesnow slugifies each path segment withnormalize_id(fromgraphify.ids, the same Unicode-aware casefold→NFKC→[^\w]+recipe every extractor uses) instead of the local ASCII-only regex, which is now unused and removed.tests/test_dedup.py: five new tests —_id_prefixespreserving non-Latin segments,_defines_idrecognizing a non-Latin defining path, an end-to-enddeduplicate_entitiestest reproducing the issue's own Korean defining/referencing pair in both arrival orders, and a negative control confirming an ordinary ASCII path's reconstructed prefixes are byte-for-byte unchanged.CHANGELOG.md: entry under 0.9.61 (unreleased).Testing
python -m pytest tests/test_dedup.py -k "korean or non_latin or ascii_path_unchanged" -q— 5 passedpython -m pytest -q— 5490 passed, 68 skipped (pre-existing unrelated failures excluded:tests/test_ollama_retry_cap.pymissing the optionalopenaimodule, andtest_ts_normalizer_scales_linearly_on_large_files/test_hyperedge_convex_hull_js_is_geometrically_sound, both known environment-specific flakes unrelated to this change)python -m tools.skillgen --check— OK🤖 Generated with Claude Code
https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh