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 <noreply@anthropic.com>
This commit is contained in:
Safi
2026-04-11 20:53:08 +01:00
parent 68abce8a04
commit 1cb882a156
5 changed files with 20 additions and 20 deletions
+6
View File
@@ -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
+2 -2
View File
@@ -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()]
+9 -9
View File
@@ -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"})
+2 -8
View File
@@ -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
+1 -1
View File
@@ -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" }