chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""Single source of truth for node-ID normalization.
|
||||
|
||||
Three independent producers must agree on node IDs or the graph splits a single
|
||||
entity into disconnected ghost nodes:
|
||||
|
||||
1. The AST extractor (``extract._make_id``) — deterministic, per-language.
|
||||
2. The semantic subagents (LLM) — follow the node-ID spec in the skill prompt.
|
||||
3. The graph builder (``build._normalize_id``) — reconciles edge endpoints when
|
||||
the LLM emits IDs with slightly different punctuation or casing than the AST.
|
||||
|
||||
Historically the normalization recipe was copy-pasted into ``extract._make_id``
|
||||
and ``build._normalize_id`` and kept in sync only by mirrored docstrings, which
|
||||
is exactly how the recurring ID-drift bug class crept in (#811 Unicode collapse,
|
||||
#550 same-filename collisions, #1033 AST-vs-LLM file-node mismatch, #1104). This
|
||||
module exists so the recipe lives in one place and the two callers can no longer
|
||||
diverge.
|
||||
|
||||
The recipe: NFKC-normalize (so composed/decomposed Unicode forms collapse),
|
||||
replace runs of non-word characters with a single underscore (``re.UNICODE`` so
|
||||
CJK/Cyrillic/Arabic/accented-Latin letters survive instead of collapsing to a
|
||||
per-file node), collapse repeated underscores, strip leading/trailing
|
||||
underscores, and casefold.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
__all__ = ["normalize_id", "make_id"]
|
||||
|
||||
|
||||
def normalize_id(s: str) -> str:
|
||||
r"""Normalize a single ID string to its canonical form.
|
||||
|
||||
Idempotent: ``normalize_id(normalize_id(s)) == normalize_id(s)``.
|
||||
"""
|
||||
s = unicodedata.normalize("NFKC", s)
|
||||
s = re.sub(r"[^\w]+", "_", s, flags=re.UNICODE)
|
||||
s = re.sub(r"_+", "_", s)
|
||||
return s.strip("_").casefold()
|
||||
|
||||
|
||||
def make_id(*parts: str) -> str:
|
||||
"""Build a canonical node ID from one or more name parts.
|
||||
|
||||
Parts are joined with ``_`` (after stripping stray ``_``/``.`` edges from each
|
||||
part) and then run through :func:`normalize_id`, so the result is identical to
|
||||
what the builder produces from the joined string.
|
||||
"""
|
||||
return normalize_id("_".join(p.strip("_.") for p in parts if p))
|
||||
Reference in New Issue
Block a user