mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 18:37:12 +00:00
fix(watch): evict stale nodes in full re-extraction path when changed_paths is None (#1007)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+23
-9
@@ -340,7 +340,7 @@ def _rebuild_code(
|
||||
try:
|
||||
from graphify.extract import extract, _get_extractor
|
||||
from graphify.detect import detect
|
||||
from graphify.build import build_from_json
|
||||
from graphify.build import build_from_json, _norm_source_file as _nsf
|
||||
from graphify.cluster import cluster, remap_communities_to_previous, score_all
|
||||
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
|
||||
from graphify.report import generate
|
||||
@@ -375,10 +375,7 @@ def _rebuild_code(
|
||||
# File was deleted, renamed away, or filtered out by detect
|
||||
# (e.g. .gitignore, vendored). Either way, evict any
|
||||
# preserved nodes that still claim this source path.
|
||||
try:
|
||||
deleted_paths.add(cand.relative_to(project_root).as_posix())
|
||||
except ValueError:
|
||||
deleted_paths.add(cand.as_posix())
|
||||
deleted_paths.add(_nsf(str(cand), str(project_root)) or str(cand))
|
||||
if not wanted and not deleted_paths:
|
||||
print("[graphify watch] No tracked code files in change set - skipping rebuild.")
|
||||
return True
|
||||
@@ -412,10 +409,27 @@ def _rebuild_code(
|
||||
evict_sources: set[str] = set(deleted_paths)
|
||||
if changed_paths is not None:
|
||||
for p in extract_targets:
|
||||
try:
|
||||
evict_sources.add(p.relative_to(project_root).as_posix())
|
||||
except ValueError:
|
||||
evict_sources.add(p.as_posix())
|
||||
evict_sources.add(_nsf(str(p), str(project_root)) or str(p))
|
||||
else:
|
||||
# Full re-extraction: reconcile against current code files to
|
||||
# evict nodes from files deleted since the last run (#1007).
|
||||
_root_str = str(project_root)
|
||||
current_sources = {
|
||||
_nsf(str(p.relative_to(project_root)), _root_str)
|
||||
for p in code_files
|
||||
if p.is_relative_to(project_root)
|
||||
}
|
||||
for n in existing.get("nodes", []):
|
||||
sf = n.get("source_file")
|
||||
if not sf:
|
||||
continue
|
||||
if Path(sf).suffix.lower() not in _CODE_EXTENSIONS:
|
||||
continue
|
||||
norm = _nsf(sf, _root_str)
|
||||
if norm not in current_sources:
|
||||
evict_sources.add(sf)
|
||||
evict_sources.add(norm)
|
||||
deleted_paths.add(norm)
|
||||
preserved_nodes = [
|
||||
n for n in existing.get("nodes", [])
|
||||
if n["id"] not in new_ast_ids
|
||||
|
||||
@@ -141,6 +141,37 @@ def test_rebuild_lock_does_not_accumulate_pids_across_runs(tmp_path):
|
||||
assert not lock_path.exists()
|
||||
|
||||
|
||||
def test_rebuild_code_evicts_nodes_from_deleted_files(tmp_path):
|
||||
"""#1007: graphify update (_rebuild_code with no changed_paths) must remove
|
||||
nodes and edges from files deleted since the last run."""
|
||||
import json
|
||||
from graphify.watch import _rebuild_code
|
||||
|
||||
corpus = tmp_path / "corpus"
|
||||
corpus.mkdir()
|
||||
|
||||
(corpus / "auth.py").write_text(
|
||||
"def login(): pass\ndef logout(): pass\n", encoding="utf-8"
|
||||
)
|
||||
(corpus / "utils.py").write_text(
|
||||
"def format_date(): pass\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
assert _rebuild_code(corpus, acquire_lock=False) is True
|
||||
graph_path = corpus / "graphify-out" / "graph.json"
|
||||
data = json.loads(graph_path.read_text(encoding="utf-8"))
|
||||
node_labels_before = {n["label"] for n in data.get("nodes", [])}
|
||||
assert "format_date()" in node_labels_before
|
||||
|
||||
(corpus / "utils.py").unlink()
|
||||
|
||||
assert _rebuild_code(corpus, acquire_lock=False) is True
|
||||
data = json.loads(graph_path.read_text(encoding="utf-8"))
|
||||
node_labels_after = {n["label"] for n in data.get("nodes", [])}
|
||||
assert "format_date()" not in node_labels_after, "stale function node from deleted file must be evicted"
|
||||
assert "login()" in node_labels_after, "nodes from surviving file must be kept"
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="fcntl-only (POSIX)")
|
||||
def test_rebuild_lock_non_blocking_does_not_clobber_holder(tmp_path):
|
||||
"""GH-858: a non-blocking caller that fails to acquire the lock must not
|
||||
|
||||
Reference in New Issue
Block a user