mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 18:37:12 +00:00
relativize symbol node IDs for root-level files to match spec (fixes #1096)
The #1033 remap relativized file node IDs but symbol IDs still embedded the absolute parent-dir stem, so a root-level file's symbols became <rootdir>_main_run while the file node was correctly main and the skill.md spec (and semantic subagent) wants main_run -- splitting every top-level file's symbols into AST/semantic ghost pairs. Extend the remap chokepoint to also canonicalize the symbol stem prefix (old _file_node_id(path) -> new _file_node_id(rel)), gated by source_file so two files sharing a prefix cannot cross-contaminate. raw_calls.caller_nid is rewritten too, since the cross-file call pass consumes it after the remap and would otherwise dangle. No-op for nested files (immediate parent identical in absolute and relative form). The originally-filed root cause (facts-collector path form) was a stale-cache red herring; the real bug is the symbol-level continuation of #1033. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10404,6 +10404,14 @@ def extract(
|
||||
# subagents generate (#1033). Resolve before relativizing so paths passed in
|
||||
# relative form still anchor to the (resolved) root.
|
||||
id_remap: dict[str, str] = {}
|
||||
# Symbol node IDs embed the file stem as a prefix (_file_node_id of the path
|
||||
# the extractor saw). For a root-level file that stem picks up the absolute
|
||||
# parent directory name, so a symbol becomes <rootdir>_main_run while the
|
||||
# file node is correctly relativized to main and the skill.md spec wants
|
||||
# main_run -- splitting the symbol into AST/semantic ghosts (#1096). Relativize
|
||||
# the symbol prefix the same way, gated by source_file so two files sharing a
|
||||
# prefix can't cross-contaminate. Keyed by resolved path -> (old_pref, new_pref).
|
||||
prefix_remap: dict[Path, tuple[str, str]] = {}
|
||||
for path in paths:
|
||||
old_id = _make_id(str(path))
|
||||
try:
|
||||
@@ -10416,6 +10424,9 @@ def extract(
|
||||
new_id = _file_node_id(rel)
|
||||
if old_id != new_id:
|
||||
id_remap[old_id] = new_id
|
||||
old_pref = _file_node_id(path)
|
||||
if old_pref != new_id:
|
||||
prefix_remap[path.resolve()] = (old_pref, new_id)
|
||||
if id_remap:
|
||||
for n in all_nodes:
|
||||
if n.get("id") in id_remap:
|
||||
@@ -10425,6 +10436,40 @@ def extract(
|
||||
e["source"] = id_remap[e["source"]]
|
||||
if e.get("target") in id_remap:
|
||||
e["target"] = id_remap[e["target"]]
|
||||
if prefix_remap:
|
||||
sym_remap: dict[str, str] = {}
|
||||
for n in all_nodes:
|
||||
sf = n.get("source_file")
|
||||
if not sf:
|
||||
continue
|
||||
try:
|
||||
entry = prefix_remap.get(Path(sf).resolve())
|
||||
except Exception:
|
||||
continue
|
||||
if entry is None:
|
||||
continue
|
||||
old_pref, new_pref = entry
|
||||
nid = n.get("id", "")
|
||||
if nid.startswith(old_pref + "_"):
|
||||
new_nid = new_pref + nid[len(old_pref):]
|
||||
if new_nid != nid:
|
||||
sym_remap[nid] = new_nid
|
||||
if sym_remap:
|
||||
for n in all_nodes:
|
||||
if n.get("id") in sym_remap:
|
||||
n["id"] = sym_remap[n["id"]]
|
||||
for e in all_edges:
|
||||
if e.get("source") in sym_remap:
|
||||
e["source"] = sym_remap[e["source"]]
|
||||
if e.get("target") in sym_remap:
|
||||
e["target"] = sym_remap[e["target"]]
|
||||
# raw_calls carry caller_nid (a symbol id) consumed by the cross-file
|
||||
# call pass below, after this remap — rewrite it too or those edges
|
||||
# would dangle on their (stale) source.
|
||||
for rc in all_raw_calls:
|
||||
cn = rc.get("caller_nid")
|
||||
if cn in sym_remap:
|
||||
rc["caller_nid"] = sym_remap[cn]
|
||||
|
||||
_merge_swift_extensions(per_file, all_nodes, all_edges)
|
||||
_disambiguate_colliding_node_ids(all_nodes, all_edges, all_raw_calls, root)
|
||||
|
||||
@@ -54,6 +54,45 @@ def test_top_level_file_node_id_is_bare_stem(tmp_path):
|
||||
assert "setup_py" not in ids
|
||||
|
||||
|
||||
def test_top_level_file_SYMBOL_ids_use_bare_stem(tmp_path):
|
||||
"""A SYMBOL in a root-level file must use the bare-stem prefix (`setup_configure`),
|
||||
not pick up the project-root directory name (`<rootdir>_setup_configure`). The
|
||||
semantic subagent emits the bare-stem form per skill.md, so an absolute-parent
|
||||
stem here splits the symbol into two ghost nodes (#1096). Pass ABSOLUTE paths,
|
||||
as the CLI does, to exercise the root-relative remap."""
|
||||
f = tmp_path / "main.py"
|
||||
f.write_text("def run():\n return 1\n")
|
||||
|
||||
extraction = extract([f.resolve()], cache_root=tmp_path)
|
||||
ids = {n["id"] for n in extraction["nodes"]}
|
||||
|
||||
assert "main_run" in ids, f"expected bare-stem symbol 'main_run', got {sorted(ids)}"
|
||||
# The root directory name must NOT appear in any symbol id.
|
||||
rootname = tmp_path.name.lower().replace("-", "_")
|
||||
assert not any(rootname in i for i in ids), (
|
||||
f"root dir name leaked into ids: {sorted(ids)}"
|
||||
)
|
||||
|
||||
# contains edge file -> symbol must connect with the canonical ids.
|
||||
contains = [e for e in extraction["edges"]
|
||||
if e["relation"] == "contains" and e["target"] == "main_run"]
|
||||
assert contains and contains[0]["source"] == "main"
|
||||
|
||||
|
||||
def test_nested_file_symbol_ids_unchanged(tmp_path):
|
||||
"""Regression guard: nested files (immediate parent identical in abs/rel form)
|
||||
must be completely unaffected by the symbol remap."""
|
||||
sub = tmp_path / "sub"
|
||||
sub.mkdir()
|
||||
f = sub / "mod.py"
|
||||
f.write_text("def work():\n return 2\n")
|
||||
|
||||
extraction = extract([f.resolve()], cache_root=tmp_path)
|
||||
ids = {n["id"] for n in extraction["nodes"]}
|
||||
assert "sub_mod" in ids
|
||||
assert "sub_mod_work" in ids
|
||||
|
||||
|
||||
def test_symbol_and_file_ids_share_the_same_stem(tmp_path):
|
||||
"""Symbol ids already use {parent}_{stem}_{name}; the file node must share
|
||||
that stem prefix so 'contains' edges connect file -> symbol."""
|
||||
|
||||
Reference in New Issue
Block a user