fix stale nodes persisting after file deletion when manifest uses absolute paths
prune_set in build_merge now includes relative-path variants of each deleted file so manifest absolute paths (e.g. /home/user/corpus/module_b/utils.py) match graph node source_file values (e.g. module_b/utils.py) regardless of OS or run context. Fixes #1007. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
d9debdf39a
commit
a26f24ef7b
@@ -338,6 +338,17 @@ def build_merge(
|
|||||||
# Prune nodes and edges from deleted source files
|
# Prune nodes and edges from deleted source files
|
||||||
if prune_sources:
|
if prune_sources:
|
||||||
prune_set = set(prune_sources)
|
prune_set = set(prune_sources)
|
||||||
|
# Manifest stores absolute paths; graph nodes store relative paths.
|
||||||
|
# Add relative variants so both formats match regardless of OS or
|
||||||
|
# whether paths were relativized at build time (#1007).
|
||||||
|
if root is not None:
|
||||||
|
import os as _os
|
||||||
|
_root = Path(root)
|
||||||
|
for p in list(prune_sources):
|
||||||
|
try:
|
||||||
|
prune_set.add(str(Path(p).relative_to(_root)).replace(_os.sep, "/"))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
to_remove = [
|
to_remove = [
|
||||||
n for n, d in G.nodes(data=True)
|
n for n, d in G.nodes(data=True)
|
||||||
if d.get("source_file") in prune_set
|
if d.get("source_file") in prune_set
|
||||||
|
|||||||
@@ -346,6 +346,36 @@ def test_build_from_json_relative_source_file_unchanged(tmp_path):
|
|||||||
assert G.nodes["foo_bar"]["source_file"] == "src/foo.py"
|
assert G.nodes["foo_bar"]["source_file"] == "src/foo.py"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_merge_prune_absolute_paths_match_relative_nodes(tmp_path):
|
||||||
|
"""#1007: manifest stores absolute paths, graph nodes store relative paths.
|
||||||
|
prune_sources with absolute paths must still remove the right nodes."""
|
||||||
|
import json
|
||||||
|
from graphify.export import to_json
|
||||||
|
|
||||||
|
root = tmp_path / "corpus"
|
||||||
|
root.mkdir()
|
||||||
|
graph_path = tmp_path / "graph.json"
|
||||||
|
|
||||||
|
# Simulate a graph with relative source_file paths (as built normally)
|
||||||
|
chunk = {"nodes": [
|
||||||
|
{"id": "n1", "label": "login", "file_type": "code", "source_file": "module_a/auth.py"},
|
||||||
|
{"id": "n2", "label": "format_date", "file_type": "code", "source_file": "module_b/utils.py"},
|
||||||
|
], "edges": []}
|
||||||
|
G0 = build([chunk], dedup=False)
|
||||||
|
import networkx as nx
|
||||||
|
graph_path.write_text(
|
||||||
|
json.dumps(nx.node_link_data(G0, edges="edges")), encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
# prune_sources from manifest — absolute paths
|
||||||
|
deleted_abs = [str(root / "module_b" / "utils.py")]
|
||||||
|
G1 = build_merge([], graph_path, prune_sources=deleted_abs, dedup=False, root=root)
|
||||||
|
|
||||||
|
node_labels = {d["label"] for _, d in G1.nodes(data=True)}
|
||||||
|
assert "format_date" not in node_labels, "stale node from deleted file should be pruned"
|
||||||
|
assert "login" in node_labels, "unrelated node must survive"
|
||||||
|
|
||||||
|
|
||||||
def test_build_merge_rejects_oversized_existing_graph(monkeypatch, tmp_path):
|
def test_build_merge_rejects_oversized_existing_graph(monkeypatch, tmp_path):
|
||||||
"""#F4: build_merge must refuse to read an existing graph.json that
|
"""#F4: build_merge must refuse to read an existing graph.json that
|
||||||
exceeds the size cap, rather than json.loads-ing it into memory."""
|
exceeds the size cap, rather than json.loads-ing it into memory."""
|
||||||
|
|||||||
Reference in New Issue
Block a user