From 148e24766224eb99f6a3825a05cb6e82e0bcea77 Mon Sep 17 00:00:00 2001 From: Safi Date: Mon, 6 Apr 2026 22:10:21 +0100 Subject: [PATCH] Fix 6 bugs: hook exit code, token budget, file node detection, duplicate function, atomic cache writes, deleted file tracking --- graphify/analyze.py | 12 ++++++++---- graphify/cache.py | 9 ++++++++- graphify/detect.py | 5 +++++ graphify/export.py | 6 +----- graphify/hooks.py | 4 +++- graphify/serve.py | 4 ++-- 6 files changed, 27 insertions(+), 13 deletions(-) diff --git a/graphify/analyze.py b/graphify/analyze.py index 6ba2e9d0b..42a237db8 100644 --- a/graphify/analyze.py +++ b/graphify/analyze.py @@ -16,12 +16,16 @@ def _is_file_node(G: nx.Graph, node_id: str) -> bool: These are synthetic nodes created by the AST extractor and should be excluded from god nodes, surprising connections, and knowledge gap reporting. """ - label = G.nodes[node_id].get("label", "") + attrs = G.nodes[node_id] + label = attrs.get("label", "") if not label: return False - # File-level hub: label is a filename with a code extension - if label.split(".")[-1] in ("py", "ts", "js", "go", "rs", "java", "rb", "cpp", "c", "h"): - return True + # File-level hub: label matches the actual source filename (not just any label ending in .py) + source_file = attrs.get("source_file", "") + if source_file: + from pathlib import Path as _Path + if label == _Path(source_file).name: + return True # Method stub: AST extractor labels methods as '.method_name()' if label.startswith(".") and label.endswith("()"): return True diff --git a/graphify/cache.py b/graphify/cache.py index e983c0c34..08824a485 100644 --- a/graphify/cache.py +++ b/graphify/cache.py @@ -3,6 +3,7 @@ from __future__ import annotations import hashlib import json +import os from pathlib import Path @@ -46,7 +47,13 @@ def save_cached(path: Path, result: dict, root: Path = Path(".")) -> None: """ h = file_hash(path) entry = cache_dir(root) / f"{h}.json" - entry.write_text(json.dumps(result)) + tmp = entry.with_suffix(".tmp") + try: + tmp.write_text(json.dumps(result)) + os.replace(tmp, entry) + except Exception: + tmp.unlink(missing_ok=True) + raise def cached_files(root: Path = Path(".")) -> set[str]: diff --git a/graphify/detect.py b/graphify/detect.py index 040ec2e4a..896080868 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -266,9 +266,14 @@ def detect_incremental(root: Path, manifest_path: str = _MANIFEST_PATH) -> dict: else: unchanged_files[ftype].append(f) + # Files in manifest that no longer exist - their cached nodes are now ghost nodes + current_files = {f for flist in full["files"].values() for f in flist} + deleted_files = [f for f in manifest if f not in current_files] + new_total = sum(len(v) for v in new_files.values()) full["incremental"] = True full["new_files"] = new_files full["unchanged_files"] = unchanged_files full["new_total"] = new_total + full["deleted_files"] = deleted_files return full diff --git a/graphify/export.py b/graphify/export.py index 124eeb81d..15b22c8d8 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -8,6 +8,7 @@ from pathlib import Path import networkx as nx from networkx.readwrite import json_graph from graphify.security import sanitize_label +from graphify.analyze import _node_community_map COMMUNITY_COLORS = [ "#4E79A7", "#F28E2B", "#E15759", "#76B7B2", "#59A14F", @@ -17,11 +18,6 @@ COMMUNITY_COLORS = [ MAX_NODES_FOR_VIZ = 5_000 -def _node_community_map(communities: dict[int, list[str]]) -> dict[str, int]: - """Invert communities dict: node_id -> community_id.""" - return {n: cid for cid, nodes in communities.items() for n in nodes} - - def _html_styles() -> str: return """