From 1cb882a156003bc993b1ed3fa5dc1ebb7efe910e Mon Sep 17 00:00:00 2001 From: Safi Date: Sat, 11 Apr 2026 20:53:08 +0100 Subject: [PATCH] fix bugs #211, #216, #217, #222 and bump to 0.4.2 - extract.py: use str(path) for node IDs to prevent same-basename collision (#211) - build.py: normalize from/to edge keys before KeyError (#216) - export.py: guard ZeroDivisionError when graph has no edges (#217) - hooks.py: remove stale CODE_EXTS filter, rebuild on any changed file (#222) Co-Authored-By: Claude Sonnet 4.6 --- graphify/build.py | 6 ++++++ graphify/export.py | 4 ++-- graphify/extract.py | 18 +++++++++--------- graphify/hooks.py | 10 ++-------- pyproject.toml | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/graphify/build.py b/graphify/build.py index 3c3d80ca..4cc30f3a 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -42,6 +42,12 @@ def build_from_json(extraction: dict, *, directed: bool = False) -> nx.Graph: G.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"}) node_set = set(G.nodes()) for edge in extraction.get("edges", []): + if "source" not in edge and "from" in edge: + edge["source"] = edge["from"] + if "target" not in edge and "to" in edge: + edge["target"] = edge["to"] + if "source" not in edge or "target" not in edge: + continue src, tgt = edge["source"], edge["target"] if src not in node_set or tgt not in node_set: continue # skip edges to external/stdlib nodes - expected, not an error diff --git a/graphify/export.py b/graphify/export.py index d35edafc..0f54319d 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -346,7 +346,7 @@ def to_html( node_community = _node_community_map(communities) degree = dict(G.degree()) - max_deg = max(degree.values()) if degree else 1 + max_deg = max(degree.values(), default=1) or 1 # Build nodes list for vis.js vis_nodes = [] @@ -957,7 +957,7 @@ def to_svg( pos = nx.spring_layout(G, seed=42, k=2.0 / (G.number_of_nodes() ** 0.5 + 1)) degree = dict(G.degree()) - max_deg = max(degree.values()) if degree else 1 + max_deg = max(degree.values(), default=1) or 1 node_colors = [COMMUNITY_COLORS[node_community.get(n, 0) % len(COMMUNITY_COLORS)] for n in G.nodes()] node_sizes = [300 + 1200 * (degree.get(n, 1) / max_deg) for n in G.nodes()] diff --git a/graphify/extract.py b/graphify/extract.py index feaa399f..dd8be4a2 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -673,7 +673,7 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict: "weight": weight, }) - file_nid = _make_id(stem) + file_nid = _make_id(str(path)) add_node(file_nid, path.name, 1) def walk(node, parent_class_nid: str | None = None) -> None: @@ -1004,7 +1004,7 @@ def _extract_python_rationale(path: Path, result: dict) -> None: nodes = result["nodes"] edges = result["edges"] seen_ids = {n["id"] for n in nodes} - file_nid = _make_id(stem) + file_nid = _make_id(str(path)) def _get_docstring(body_node) -> tuple[str, int] | None: if not body_node: @@ -1200,7 +1200,7 @@ def extract_julia(path: Path) -> dict: "weight": weight, }) - file_nid = _make_id(stem) + file_nid = _make_id(str(path)) add_node(file_nid, path.name, 1) def _func_name_from_signature(sig_node) -> str | None: @@ -1415,7 +1415,7 @@ def extract_go(path: Path) -> dict: "weight": weight, }) - file_nid = _make_id(stem) + file_nid = _make_id(str(path)) add_node(file_nid, path.name, 1) def walk(node) -> None: @@ -1603,7 +1603,7 @@ def extract_rust(path: Path) -> dict: "weight": weight, }) - file_nid = _make_id(stem) + file_nid = _make_id(str(path)) add_node(file_nid, path.name, 1) def walk(node, parent_impl_nid: str | None = None) -> None: @@ -1761,7 +1761,7 @@ def extract_zig(path: Path) -> dict: "confidence": confidence, "source_file": str_path, "source_location": f"L{line}", "weight": weight}) - file_nid = _make_id(stem) + file_nid = _make_id(str(path)) add_node(file_nid, path.name, 1) def _extract_import(node) -> None: @@ -1916,7 +1916,7 @@ def extract_powershell(path: Path) -> dict: "confidence": confidence, "source_file": str_path, "source_location": f"L{line}", "weight": weight}) - file_nid = _make_id(stem) + file_nid = _make_id(str(path)) add_node(file_nid, path.name, 1) _PS_SKIP = frozenset({ @@ -2205,7 +2205,7 @@ def extract_objc(path: Path) -> dict: "confidence": confidence, "source_file": str_path, "source_location": f"L{line}", "weight": weight}) - file_nid = _make_id(stem) + file_nid = _make_id(str(path)) add_node(file_nid, path.name, 1) def _read(node) -> str: @@ -2403,7 +2403,7 @@ def extract_elixir(path: Path) -> dict: "confidence": confidence, "source_file": str_path, "source_location": f"L{line}", "weight": weight}) - file_nid = _make_id(stem) + file_nid = _make_id(str(path)) add_node(file_nid, path.name, 1) _IMPORT_KEYWORDS = frozenset({"alias", "import", "require", "use"}) diff --git a/graphify/hooks.py b/graphify/hooks.py index 92320272..39fdf89d 100644 --- a/graphify/hooks.py +++ b/graphify/hooks.py @@ -46,19 +46,13 @@ $GRAPHIFY_PYTHON -c " import os, sys from pathlib import Path -CODE_EXTS = { - '.py', '.ts', '.js', '.go', '.rs', '.java', '.cpp', '.c', '.rb', '.swift', - '.kt', '.cs', '.scala', '.php', '.cc', '.cxx', '.hpp', '.h', '.kts', -} - changed_raw = os.environ.get('GRAPHIFY_CHANGED', '') changed = [Path(f.strip()) for f in changed_raw.strip().splitlines() if f.strip()] -code_changed = [f for f in changed if f.suffix.lower() in CODE_EXTS and f.exists()] -if not code_changed: +if not changed: sys.exit(0) -print(f'[graphify hook] {len(code_changed)} code file(s) changed - rebuilding graph...') +print(f'[graphify hook] {len(changed)} file(s) changed - rebuilding graph...') try: from graphify.watch import _rebuild_code diff --git a/pyproject.toml b/pyproject.toml index 8f7c3e27..69e1b3b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.4.1" +version = "0.4.2" description = "AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, OpenClaw, Factory Droid, Trae) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = { file = "LICENSE" }