mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-28 09:17:02 +00:00
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:
co-authored by
Claude Sonnet 4.6
parent
11dff7e9b3
commit
29c639d97d
+5
-8
@@ -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"
|
||||
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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}"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user