Apply PRs #82 #93 #102 #109: extension drift, click detection, skill coverage, .graphify_python persistence

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Safi
2026-04-09 08:22:01 +01:00
co-authored by Claude Sonnet 4.6
parent 11dff7e9b3
commit 29c639d97d
9 changed files with 87 additions and 44 deletions
+5 -8
View File
@@ -109,19 +109,16 @@ def _is_concept_node(G: nx.Graph, node_id: str) -> bool:
return False
_CODE_EXTENSIONS = {"py", "ts", "tsx", "js", "go", "rs", "java", "rb", "cpp", "c", "h", "cs", "kt", "scala", "php"}
_DOC_EXTENSIONS = {"md", "txt", "rst"}
_PAPER_EXTENSIONS = {"pdf"}
_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "webp", "gif", "svg"}
from graphify.detect import CODE_EXTENSIONS, DOC_EXTENSIONS, PAPER_EXTENSIONS, IMAGE_EXTENSIONS
def _file_category(path: str) -> str:
ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
if ext in _CODE_EXTENSIONS:
ext = ("." + path.rsplit(".", 1)[-1].lower()) if "." in path else ""
if ext in CODE_EXTENSIONS:
return "code"
if ext in _PAPER_EXTENSIONS:
if ext in PAPER_EXTENSIONS:
return "paper"
if ext in _IMAGE_EXTENSIONS:
if ext in IMAGE_EXTENSIONS:
return "image"
return "doc"
-17
View File
@@ -52,23 +52,6 @@ def _partition(G: nx.Graph) -> dict[str, int]:
return {node: cid for cid, nodes in enumerate(communities) for node in nodes}
def build_graph(nodes: list[dict], edges: list[dict]) -> nx.Graph:
"""Build a NetworkX graph from graphify node/edge dicts.
Preserves original edge direction as _src/_tgt attributes so that
display functions can show relationships in the correct direction,
even though the graph is undirected for structural analysis.
"""
G = nx.Graph()
for n in nodes:
G.add_node(n["id"], **{k: v for k, v in n.items() if k != "id"})
for e in edges:
attrs = {k: v for k, v in e.items() if k not in ("source", "target")}
attrs["_src"] = e["source"]
attrs["_tgt"] = e["target"]
G.add_edge(e["source"], e["target"], **attrs)
return G
_MAX_COMMUNITY_FRACTION = 0.25 # communities larger than 25% of graph get split
_MIN_SPLIT_SIZE = 10 # only split if community has at least this many nodes
+21 -2
View File
@@ -183,9 +183,28 @@ function focusNode(nodeId) {{
showInfo(nodeId);
}}
// Track hovered node — hover detection is more reliable than click params
let hoveredNodeId = null;
network.on('hoverNode', params => {{
hoveredNodeId = params.node;
container.style.cursor = 'pointer';
}});
network.on('blurNode', () => {{
hoveredNodeId = null;
container.style.cursor = 'default';
}});
container.addEventListener('click', () => {{
if (hoveredNodeId !== null) {{
showInfo(hoveredNodeId);
network.selectNodes([hoveredNodeId]);
}}
}});
network.on('click', params => {{
if (params.nodes.length > 0) showInfo(params.nodes[0]);
else document.getElementById('info-content').innerHTML = '<span class="empty">Click a node to inspect it</span>';
if (params.nodes.length > 0) {{
showInfo(params.nodes[0]);
}} else if (hoveredNodeId === null) {{
document.getElementById('info-content').innerHTML = '<span class="empty">Click a node to inspect it</span>';
}}
}});
const searchInput = document.getElementById('search');
+1 -1
View File
@@ -637,7 +637,7 @@ cost_path.write_text(json.dumps(cost, indent=2))
print(f'This run: {input_tok:,} input tokens, {output_tok:,} output tokens')
print(f'All time: {cost[\"total_input_tokens\"]:,} input, {cost[\"total_output_tokens\"]:,} output ({len(cost[\"runs\"])} runs)')
"
Remove-Item -ErrorAction SilentlyContinue .graphify_detect.json, .graphify_extract.json, .graphify_ast.json, .graphify_semantic.json, .graphify_analysis.json, .graphify_labels.json, .graphify_python
Remove-Item -ErrorAction SilentlyContinue .graphify_detect.json, .graphify_extract.json, .graphify_ast.json, .graphify_semantic.json, .graphify_analysis.json, .graphify_labels.json
Remove-Item -ErrorAction SilentlyContinue graphify-out/.needs_update
```
+20 -2
View File
@@ -71,9 +71,9 @@ else
PYTHON="python3"
fi
"$PYTHON" -c "import graphify" 2>/dev/null || pip install graphifyy -q --break-system-packages 2>&1 | tail -3
# Write interpreter path for all subsequent steps
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w').write(sys.executable)"
# Write interpreter path for all subsequent steps (persists across invocations)
mkdir -p graphify-out
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w').write(sys.executable)"
```
If the import succeeds, print nothing and move straight to Step 2.
@@ -683,6 +683,24 @@ The graph is the map. Your job after the pipeline is to be the guide.
---
## Interpreter guard for subcommands
Before running any subcommand below (`--update`, `--cluster-only`, `query`, `path`, `explain`, `add`), check that `.graphify_python` exists. If it's missing (e.g. user deleted `graphify-out/`), re-resolve the interpreter first:
```bash
if [ ! -f graphify-out/.graphify_python ]; then
GRAPHIFY_BIN=$(which graphify 2>/dev/null)
if [ -n "$GRAPHIFY_BIN" ]; then
PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!')
case "$PYTHON" in *[!a-zA-Z0-9/_.-]*) PYTHON="python3" ;; esac
else
PYTHON="python3"
fi
mkdir -p graphify-out
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w').write(sys.executable)"
fi
```
## For --update (incremental re-extraction)
Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time.
+3 -10
View File
@@ -5,17 +5,10 @@ import time
from pathlib import Path
_WATCHED_EXTENSIONS = {
".py", ".ts", ".js", ".go", ".rs", ".java", ".cpp", ".c", ".rb", ".swift", ".kt",
".cs", ".scala", ".php", ".cc", ".cxx", ".hpp", ".h", ".kts",
".md", ".txt", ".rst", ".pdf",
".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg",
}
from graphify.detect import CODE_EXTENSIONS, DOC_EXTENSIONS, PAPER_EXTENSIONS, IMAGE_EXTENSIONS
_CODE_EXTENSIONS = {
".py", ".ts", ".js", ".go", ".rs", ".java", ".cpp", ".c", ".rb", ".swift", ".kt",
".cs", ".scala", ".php", ".cc", ".cxx", ".hpp", ".h", ".kts",
}
_WATCHED_EXTENSIONS = CODE_EXTENSIONS | DOC_EXTENSIONS | PAPER_EXTENSIONS | IMAGE_EXTENSIONS
_CODE_EXTENSIONS = CODE_EXTENSIONS
def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False) -> bool:
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "graphifyy"
version = "0.3.17"
version = "0.3.18"
description = "AI coding assistant skill (Claude Code, Codex, OpenCode, OpenClaw) - turn any folder of code, docs, papers, or images into a queryable knowledge graph"
readme = "README.md"
license = { file = "LICENSE" }
@@ -57,4 +57,4 @@ where = ["."]
include = ["graphify*"]
[tool.setuptools.package-data]
graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-claw.md", "skill-windows.md", "skill-droid.md"]
graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-claw.md", "skill-windows.md", "skill-droid.md", "skill-trae.md"]
+9
View File
@@ -137,6 +137,15 @@ def test_file_category():
assert _file_category("flash.pdf") == "paper"
assert _file_category("diagram.png") == "image"
assert _file_category("notes.md") == "doc"
# Languages added in later releases — would misclassify as "doc" without detect.py import
assert _file_category("app.swift") == "code"
assert _file_category("plugin.lua") == "code"
assert _file_category("build.zig") == "code"
assert _file_category("deploy.ps1") == "code"
assert _file_category("server.ex") == "code"
assert _file_category("component.jsx") == "code"
assert _file_category("analysis.jl") == "code"
assert _file_category("view.m") == "code"
def test_is_concept_node_empty_source():
+26 -2
View File
@@ -9,6 +9,10 @@ PLATFORMS = {
"codex": (".agents/skills/graphify/SKILL.md",),
"opencode": (".config/opencode/skills/graphify/SKILL.md",),
"claw": (".claw/skills/graphify/SKILL.md",),
"droid": (".factory/skills/graphify/SKILL.md",),
"trae": (".trae/skills/graphify/SKILL.md",),
"trae-cn": (".trae-cn/skills/graphify/SKILL.md",),
"windows": (".claude/skills/graphify/SKILL.md",),
}
@@ -38,6 +42,26 @@ def test_install_claw(tmp_path):
assert (tmp_path / ".claw" / "skills" / "graphify" / "SKILL.md").exists()
def test_install_droid(tmp_path):
_install(tmp_path, "droid")
assert (tmp_path / ".factory" / "skills" / "graphify" / "SKILL.md").exists()
def test_install_trae(tmp_path):
_install(tmp_path, "trae")
assert (tmp_path / ".trae" / "skills" / "graphify" / "SKILL.md").exists()
def test_install_trae_cn(tmp_path):
_install(tmp_path, "trae-cn")
assert (tmp_path / ".trae-cn" / "skills" / "graphify" / "SKILL.md").exists()
def test_install_windows(tmp_path):
_install(tmp_path, "windows")
assert (tmp_path / ".claude" / "skills" / "graphify" / "SKILL.md").exists()
def test_install_unknown_platform_exits(tmp_path):
with pytest.raises(SystemExit):
_install(tmp_path, "unknown")
@@ -67,10 +91,10 @@ def test_claw_skill_is_sequential():
def test_all_skill_files_exist_in_package():
"""All four platform skill files must be present in the installed package."""
"""All installable platform skill files must be present in the installed package."""
import graphify
pkg = Path(graphify.__file__).parent
for name in ("skill.md", "skill-codex.md", "skill-opencode.md", "skill-claw.md"):
for name in ("skill.md", "skill-codex.md", "skill-opencode.md", "skill-claw.md", "skill-windows.md", "skill-droid.md", "skill-trae.md"):
assert (pkg / name).exists(), f"Missing: {name}"