mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-22 21:45:58 +00:00
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:
co-authored by
Claude Sonnet 4.6
parent
41544c7076
commit
79a9200f09
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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>"""
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user