v0.4.13: Verilog support, HiDPI hyperedge fix, null label guards, AGENTS.md python3 fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Safi
2026-04-14 09:28:01 +01:00
co-authored by Claude Sonnet 4.6
parent 41544c7076
commit 79a9200f09
7 changed files with 129 additions and 24 deletions
+7
View File
@@ -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)
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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'}
+8 -17
View File
@@ -62,32 +62,24 @@ def _hyperedge_script(hyperedges_json: str) -> str:
return f"""<script>
// Render hyperedges as shaded regions
const hyperedges = {hyperedges_json};
function drawHyperedges() {{
const canvas = network.canvas.frame.canvas;
const ctx = canvas.getContext('2d');
// afterDrawing passes ctx already transformed to network coordinate space.
// Draw node positions raw no manual pan/zoom/DPR math needed.
network.on('afterDrawing', function(ctx) {{
hyperedges.forEach(h => {{
const positions = h.nodes
.map(nid => network.getPositions([nid])[nid])
.filter(p => p !== undefined);
if (positions.length < 2) return;
// Draw convex hull as filled polygon
ctx.save();
ctx.globalAlpha = 0.12;
ctx.fillStyle = '#6366f1';
ctx.strokeStyle = '#6366f1';
ctx.lineWidth = 2;
ctx.beginPath();
const scale = network.getScale();
const offset = network.getViewPosition();
const toCanvas = (p) => ({{
x: (p.x - offset.x) * scale + canvas.width / 2,
y: (p.y - offset.y) * scale + canvas.height / 2
}});
const pts = positions.map(toCanvas);
// Expand hull slightly
const cx = pts.reduce((s, p) => s + p.x, 0) / pts.length;
const cy = pts.reduce((s, p) => s + p.y, 0) / pts.length;
const expanded = pts.map(p => ({{
// Centroid and expanded hull in network coordinates
const cx = positions.reduce((s, p) => s + p.x, 0) / positions.length;
const cy = positions.reduce((s, p) => s + p.y, 0) / positions.length;
const expanded = positions.map(p => ({{
x: cx + (p.x - cx) * 1.15,
y: cy + (p.y - cy) * 1.15
}}));
@@ -105,8 +97,7 @@ function drawHyperedges() {{
ctx.fillText(h.label, cx, cy - 5);
ctx.restore();
}});
}}
network.on('afterDrawing', drawHyperedges);
}});
</script>"""
+106
View File
@@ -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)
+3 -3
View File
@@ -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]
+2 -1
View File
@@ -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]