From 79a9200f09b0cf36f757475b4c0f433b8baed280 Mon Sep 17 00:00:00 2001 From: Safi Date: Tue, 14 Apr 2026 09:28:01 +0100 Subject: [PATCH] v0.4.13: Verilog support, HiDPI hyperedge fix, null label guards, AGENTS.md python3 fix Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 7 +++ graphify/__main__.py | 4 +- graphify/detect.py | 2 +- graphify/export.py | 25 ++++------ graphify/extract.py | 106 +++++++++++++++++++++++++++++++++++++++++++ graphify/serve.py | 6 +-- pyproject.toml | 3 +- 7 files changed, 129 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c416877..24bed77d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) +## 0.4.13 (2026-04-14) + +- Add: Verilog/SystemVerilog support — `.v` and `.sv` files extracted via tree-sitter-verilog (modules, functions, tasks, package imports, module instantiations with `instantiates` edges) (#325) +- Fix: hyperedge polygons render correctly on HiDPI/Retina displays — `afterDrawing` callback ctx is now used directly (already in network coordinate space), removing the double-applied transform and incorrect `canvas.width/2` DPR anchor (#334) +- Fix: AGENTS.md and GEMINI.md rebuild rule now uses `graphify update .` instead of hardcoded `python3 -c "..."` — correct Python is resolved through the graphify binary, no more interpreter mismatches in Nix/pipx/uv environments (#324) +- Fix: `graphify query` and `graphify explain` no longer crash with `AttributeError` when a node has `label: null` — all `.get("label", "")` calls guarded with `or ""` to handle explicit null values (#323) + ## 0.4.12 (2026-04-13) - Add: Kiro IDE/CLI support — `graphify kiro install` writes `.kiro/skills/graphify/SKILL.md` (invoked via `/graphify`) and `.kiro/steering/graphify.md` (`inclusion: always` — always-on context before every conversation) (#319, #321) diff --git a/graphify/__main__.py b/graphify/__main__.py index 2066531c..5813d5cf 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -186,7 +186,7 @@ This project has a graphify knowledge graph at graphify-out/. Rules: - Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure - If graphify-out/wiki/index.md exists, navigate it instead of reading raw files -- After modifying code files in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current +- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) """ _AGENTS_MD_MARKER = "## graphify" @@ -199,7 +199,7 @@ This project has a graphify knowledge graph at graphify-out/. Rules: - Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure - If graphify-out/wiki/index.md exists, navigate it instead of reading raw files -- After modifying code files in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current +- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) """ _GEMINI_MD_MARKER = "## graphify" diff --git a/graphify/detect.py b/graphify/detect.py index fb65923b..0e51d93d 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -18,7 +18,7 @@ class FileType(str, Enum): _MANIFEST_PATH = "graphify-out/manifest.json" -CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.dart'} +CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.dart', '.v', '.sv'} DOC_EXTENSIONS = {'.md', '.txt', '.rst'} PAPER_EXTENSIONS = {'.pdf'} IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'} diff --git a/graphify/export.py b/graphify/export.py index f0ee66ba..033ec66d 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -62,32 +62,24 @@ def _hyperedge_script(hyperedges_json: str) -> str: return f"""""" diff --git a/graphify/extract.py b/graphify/extract.py index 52183c4e..f420256c 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -1464,6 +1464,110 @@ def extract_dart(path: Path) -> dict: return {"nodes": nodes, "edges": edges} +def extract_verilog(path: Path) -> dict: + """Extract modules, functions, tasks, package imports, and instantiations from .v/.sv files.""" + try: + import tree_sitter_verilog as tsverilog + from tree_sitter import Language, Parser + except ImportError: + return {"nodes": [], "edges": [], "error": "tree_sitter_verilog not installed"} + + try: + language = Language(tsverilog.language()) + parser = Parser(language) + source = path.read_bytes() + tree = parser.parse(source) + root = tree.root_node + except Exception as e: + return {"nodes": [], "edges": [], "error": str(e)} + + stem = path.stem + str_path = str(path) + nodes: list[dict] = [] + edges: list[dict] = [] + seen_ids: set[str] = set() + + def add_node(nid: str, label: str, line: int) -> None: + if nid not in seen_ids: + seen_ids.add(nid) + nodes.append({"id": nid, "label": label, "file_type": "code", + "source_file": str_path, "source_location": f"L{line}", + "confidence_score": 1.0}) + + def add_edge(src: str, tgt: str, relation: str, line: int, + confidence: str = "EXTRACTED", score: float = 1.0) -> None: + edges.append({"source": src, "target": tgt, "relation": relation, + "confidence": confidence, "confidence_score": score, + "source_file": str_path, "source_location": f"L{line}", "weight": 1.0}) + + file_nid = _make_id(str(path)) + add_node(file_nid, path.name, 1) + + def walk(node, module_nid: str | None = None) -> None: + t = node.type + + if t == "module_declaration": + name_node = node.child_by_field_name("name") + if name_node: + mod_name = _read_text(name_node, source) + line = node.start_point[0] + 1 + nid = _make_id(stem, mod_name) + add_node(nid, mod_name, line) + add_edge(file_nid, nid, "defines", line) + for child in node.children: + walk(child, nid) + return + + elif t in ("function_declaration", "function_prototype"): + name_node = node.child_by_field_name("name") + if name_node: + func_name = _read_text(name_node, source) + line = node.start_point[0] + 1 + parent = module_nid or file_nid + nid = _make_id(parent, func_name) + add_node(nid, f"{func_name}()", line) + add_edge(parent, nid, "contains", line) + + elif t == "task_declaration": + name_node = node.child_by_field_name("name") + if name_node: + task_name = _read_text(name_node, source) + line = node.start_point[0] + 1 + parent = module_nid or file_nid + nid = _make_id(parent, task_name) + add_node(nid, task_name, line) + add_edge(parent, nid, "contains", line) + + elif t == "package_import_declaration": + for child in node.children: + if child.type == "package_import_item": + pkg_text = _read_text(child, source) + pkg_name = pkg_text.split("::")[0].strip() + if pkg_name: + line = node.start_point[0] + 1 + tgt_nid = _make_id(pkg_name) + add_node(tgt_nid, pkg_name, line) + src = module_nid or file_nid + add_edge(src, tgt_nid, "imports_from", line) + + elif t == "module_instantiation": + # module_type instantiates another module + type_node = node.child_by_field_name("module_type") + if type_node and module_nid: + inst_type = _read_text(type_node, source).strip() + if inst_type: + line = node.start_point[0] + 1 + tgt_nid = _make_id(inst_type) + add_node(tgt_nid, inst_type, line) + add_edge(module_nid, tgt_nid, "instantiates", line) + + for child in node.children: + walk(child, module_nid) + + walk(root) + return {"nodes": nodes, "edges": edges} + + def extract_lua(path: Path) -> dict: """Extract functions, methods, require() imports, and calls from a .lua file.""" return _extract_generic(path, _LUA_CONFIG) @@ -2948,6 +3052,8 @@ def extract(paths: list[Path]) -> dict: ".vue": extract_js, ".svelte": extract_js, ".dart": extract_dart, + ".v": extract_verilog, + ".sv": extract_verilog, } total = len(paths) diff --git a/graphify/serve.py b/graphify/serve.py index bd1a9484..d1f1960d 100644 --- a/graphify/serve.py +++ b/graphify/serve.py @@ -49,7 +49,7 @@ def _score_nodes(G: nx.Graph, terms: list[str]) -> list[tuple[float, str]]: scored = [] norm_terms = [_strip_diacritics(t).lower() for t in terms] for nid, data in G.nodes(data=True): - norm_label = data.get("norm_label") or _strip_diacritics(data.get("label", "")).lower() + norm_label = data.get("norm_label") or _strip_diacritics(data.get("label") or "").lower() source = (data.get("source_file") or "").lower() score = sum(1 for t in norm_terms if t in norm_label) + sum(0.5 for t in norm_terms if t in source) if score > 0: @@ -113,7 +113,7 @@ def _find_node(G: nx.Graph, label: str) -> list[str]: """Return node IDs whose label or ID matches the search term (diacritic-insensitive).""" term = _strip_diacritics(label).lower() return [nid for nid, d in G.nodes(data=True) - if term in (d.get("norm_label") or _strip_diacritics(d.get("label", "")).lower()) + if term in (d.get("norm_label") or _strip_diacritics(d.get("label") or "").lower()) or term == nid.lower()] @@ -251,7 +251,7 @@ def serve(graph_path: str = "graphify-out/graph.json") -> None: def _tool_get_node(arguments: dict) -> str: label = arguments["label"].lower() matches = [(nid, d) for nid, d in G.nodes(data=True) - if label in d.get("label", "").lower() or label == nid.lower()] + if label in (d.get("label") or "").lower() or label == nid.lower()] if not matches: return f"No node matching '{label}' found." nid, d = matches[0] diff --git a/pyproject.toml b/pyproject.toml index 2ef96702..9f6b5e94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.4.12" +version = "0.4.13" description = "AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = { file = "LICENSE" } @@ -33,6 +33,7 @@ dependencies = [ "tree-sitter-elixir", "tree-sitter-objc", "tree-sitter-julia", + "tree-sitter-verilog", ] [project.urls]