From a26f24ef7bd5cb27f71adb9ddd23dd3bb2dbedcf Mon Sep 17 00:00:00 2001 From: Safi Date: Tue, 26 May 2026 14:53:36 +0100 Subject: [PATCH] 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 --- graphify/build.py | 11 +++++++++++ tests/test_build.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/graphify/build.py b/graphify/build.py index 245fea29..579d7a35 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -338,6 +338,17 @@ def build_merge( # Prune nodes and edges from deleted source files if 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 = [ n for n, d in G.nodes(data=True) if d.get("source_file") in prune_set diff --git a/tests/test_build.py b/tests/test_build.py index 58f2863c..7552aa6a 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -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" +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): """#F4: build_merge must refuse to read an existing graph.json that exceeds the size cap, rather than json.loads-ing it into memory."""