mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-28 17:26:48 +00:00
fix(build): ghost-merge keys on full source_file, not bare basename (#2068)
build_from_json's #1145 ghost-duplicate merge keyed on (Path(source_file).name, label), discarding the directory, so unrelated nodes from different files sharing a common basename (index.md, README.md, ...) and a generic label were silently merged onto one survivor with their edges rewired — corrupting multi-corpus doc graphs. The AST/LLM ghost twins the merge legitimately targets always share the same source_file, so keying on the full normalized source_file preserves #1145 while making cross-directory false merges impossible. This subsumes the #1753/#1257 cross-file ambiguity guard (now removed as dead code). Independent of the #2032 label pass. Updated the #1257 test to the now-correct precise merge.
This commit is contained in:
+20
-26
@@ -625,9 +625,9 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
|
||||
# populates source_location, so those ghosts survived. Extended fix: use
|
||||
# _origin=="ast" as the canonical signal. AST nodes always win; any non-AST
|
||||
# node sharing (basename, label) with an AST node is a ghost.
|
||||
_loc_nodes: dict[tuple[str, str], str] = {} # (basename, label) -> canonical node id
|
||||
_loc_nodes: dict[tuple[str, str], str] = {} # (source_file, label) -> canonical node id
|
||||
_loc_collisions: set[tuple[str, str]] = set() # keys shared by 2+ AST nodes
|
||||
_noloc_nodes: dict[tuple[str, str], str] = {} # (basename, label) -> ghost node id
|
||||
_noloc_nodes: dict[tuple[str, str], str] = {} # (source_file, label) -> ghost node id
|
||||
|
||||
# Pass 1: collect canonical nodes — AST-origin nodes take precedence over LLM nodes.
|
||||
# When 2+ AST nodes share a key (same-named symbols in same-named files across
|
||||
@@ -643,36 +643,31 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
|
||||
attrs = G.nodes[nid]
|
||||
label = str(attrs.get("label", "")).strip()
|
||||
sf = str(attrs.get("source_file", ""))
|
||||
basename = Path(sf).name if sf else ""
|
||||
if not label or not basename:
|
||||
if not label or not sf:
|
||||
continue
|
||||
is_ast = attrs.get("_origin") == "ast"
|
||||
if attrs.get("source_location") or is_ast:
|
||||
key = (basename, label)
|
||||
# Key on the FULL normalized source_file, not the bare basename
|
||||
# (#2068): the AST/LLM ghost twins of #1145 always share the same
|
||||
# source_file (different ids, same file), so full-path keying still
|
||||
# collapses them, while unrelated same-basename nodes in DIFFERENT
|
||||
# directories (docs/a/index.md vs docs/b/index.md) now get distinct
|
||||
# keys and are never falsely merged. This subsumes the #1753/#1257
|
||||
# cross-file ambiguity guard, which is why the non-AST branch below
|
||||
# no longer needs it.
|
||||
key = (sf, label)
|
||||
if is_ast:
|
||||
# Two AST nodes on the same key is an ambiguous collision.
|
||||
# Two AST nodes on the same key (same file, same label) is an
|
||||
# ambiguous collision.
|
||||
if key in _loc_nodes and G.nodes[_loc_nodes[key]].get("_origin") == "ast":
|
||||
_loc_collisions.add(key)
|
||||
# AST-origin nodes always overwrite a prior non-AST entry.
|
||||
_loc_nodes[key] = nid
|
||||
else:
|
||||
existing = _loc_nodes.get(key)
|
||||
if existing is None:
|
||||
_loc_nodes[key] = nid
|
||||
elif (
|
||||
G.nodes[existing].get("_origin") != "ast"
|
||||
and str(G.nodes[existing].get("source_file", "")) != sf
|
||||
):
|
||||
# Two NON-AST nodes sharing (basename, label) but coming from
|
||||
# DIFFERENT files are distinct concepts (e.g. a same-named
|
||||
# concept in dir_a/update.md and dir_b/update.md), not an AST
|
||||
# ghost/canonical twin. Merging them would drop a real node
|
||||
# and pick the survivor arbitrarily via iteration order
|
||||
# (#1753). Mark the key ambiguous so Pass 2 leaves both, the
|
||||
# same conservatism the AST/AST case uses (#1257). A genuine
|
||||
# same-file duplicate (identical source_file) is not flagged
|
||||
# and still collapses.
|
||||
_loc_collisions.add(key)
|
||||
# First non-AST node for this (file, label) wins as canonical; a
|
||||
# later same-key node is a genuine same-file duplicate and still
|
||||
# collapses in Pass 2.
|
||||
_loc_nodes.setdefault(key, nid)
|
||||
|
||||
# Pass 2: find ghosts — non-AST nodes that have an AST canonical twin.
|
||||
for nid in sorted(node_set):
|
||||
@@ -681,10 +676,9 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
|
||||
continue # AST nodes are never ghosts
|
||||
label = str(attrs.get("label", "")).strip()
|
||||
sf = str(attrs.get("source_file", ""))
|
||||
basename = Path(sf).name if sf else ""
|
||||
if not label or not basename:
|
||||
if not label or not sf:
|
||||
continue
|
||||
key = (basename, label)
|
||||
key = (sf, label)
|
||||
if key in _loc_collisions:
|
||||
continue # ambiguous key: no safe canonical winner, leave ghost intact
|
||||
if key in _loc_nodes and _loc_nodes[key] != nid:
|
||||
|
||||
Reference in New Issue
Block a user