Skip to content

Commit 0c22041

Browse files
committed
feat: add human-friendly architecture dashboard with Mermaid.js
Add dashboard.py that generates a single-page HTML architecture overview from graph.json, designed for human team members and onboarding. - Merges fine-grained Leiden communities into 5-10 macro-modules - Auto-detects architectural layers (frontend/backend/engine/data/tools/tests) - Renders Mermaid.js flowchart with layered subgraphs and directional arrows - Module cards with key files, top nodes, and cross-module dependencies - PageRank-based recommended reading order for new team members - LLM-optional: auto-infers names, accepts override names and descriptions - CLI: graphify dashboard [--graph path] [--output path] - Skill: auto-generates in Step 6c of /graphify pipeline - No new dependencies (Mermaid.js via CDN)
1 parent 8bed332 commit 0c22041

3 files changed

Lines changed: 795 additions & 216 deletions

File tree

graphify/__main__.py

Lines changed: 36 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def _refresh_all_version_stamps() -> None:
117117
},
118118
"antigravity": {
119119
"skill_file": "skill.md",
120-
"skill_dst": Path(".agents") / "skills" / "graphify" / "SKILL.md",
120+
"skill_dst": Path(".agent") / "skills" / "graphify" / "SKILL.md",
121121
"claude_md": False,
122122
},
123123
"windows": {
@@ -148,12 +148,7 @@ def install(platform: str = "claude") -> None:
148148
print(f"error: {cfg['skill_file']} not found in package - reinstall graphify", file=sys.stderr)
149149
sys.exit(1)
150150

151-
import os as _os
152-
if platform in ("claude", "windows") and _os.environ.get("CLAUDE_CONFIG_DIR"):
153-
_claude_base = Path(_os.environ["CLAUDE_CONFIG_DIR"])
154-
skill_dst = _claude_base / "skills" / "graphify" / "SKILL.md"
155-
else:
156-
skill_dst = Path.home() / cfg["skill_dst"]
151+
skill_dst = Path.home() / cfg["skill_dst"]
157152
skill_dst.parent.mkdir(parents=True, exist_ok=True)
158153
shutil.copy(skill_src, skill_dst)
159154
(skill_dst.parent / ".graphify_version").write_text(__version__, encoding="utf-8")
@@ -196,7 +191,6 @@ def install(platform: str = "claude") -> None:
196191
Rules:
197192
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
198193
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
199-
- For cross-module "how does X relate to Y" questions, prefer `graphify query "<question>"`, `graphify path "<A>" "<B>"`, or `graphify explain "<concept>"` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files
200194
- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)
201195
"""
202196

@@ -212,7 +206,6 @@ def install(platform: str = "claude") -> None:
212206
Rules:
213207
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
214208
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
215-
- For cross-module "how does X relate to Y" questions, prefer `graphify query "<question>"`, `graphify path "<A>" "<B>"`, or `graphify explain "<concept>"` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files
216209
- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)
217210
"""
218211

@@ -226,7 +219,6 @@ def install(platform: str = "claude") -> None:
226219
Rules:
227220
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
228221
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
229-
- For cross-module "how does X relate to Y" questions, prefer `graphify query "<question>"`, `graphify path "<A>" "<B>"`, or `graphify explain "<concept>"` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files
230222
- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)
231223
"""
232224

@@ -417,8 +409,8 @@ def vscode_uninstall(project_dir: Path | None = None) -> None:
417409
print(f" {instructions} -> deleted (was empty after removal)")
418410

419411

420-
_ANTIGRAVITY_RULES_PATH = Path(".agents") / "rules" / "graphify.md"
421-
_ANTIGRAVITY_WORKFLOW_PATH = Path(".agents") / "workflows" / "graphify.md"
412+
_ANTIGRAVITY_RULES_PATH = Path(".agent") / "rules" / "graphify.md"
413+
_ANTIGRAVITY_WORKFLOW_PATH = Path(".agent") / "workflows" / "graphify.md"
422414

423415
_ANTIGRAVITY_RULES = """\
424416
## graphify
@@ -429,7 +421,6 @@ def vscode_uninstall(project_dir: Path | None = None) -> None:
429421
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
430422
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
431423
- If the graphify MCP server is active, utilize tools like `query_graph`, `get_node`, and `shortest_path` for precise architecture navigation instead of falling back to `grep`
432-
- If the MCP server is not active, the CLI equivalents are `graphify query "<question>"`, `graphify path "<A>" "<B>"`, and `graphify explain "<concept>"` — prefer these over grep for cross-module questions
433424
- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)
434425
"""
435426

@@ -439,7 +430,7 @@ def vscode_uninstall(project_dir: Path | None = None) -> None:
439430
**Description:** Turn any folder of files into a navigable knowledge graph
440431
441432
## Steps
442-
Follow the graphify skill installed at ~/.agents/skills/graphify/SKILL.md to run the full pipeline.
433+
Follow the graphify skill installed at ~/.agent/skills/graphify/SKILL.md to run the full pipeline.
443434
444435
If no path argument is given, use `.` (current directory).
445436
"""
@@ -509,8 +500,8 @@ def _kiro_uninstall(project_dir: Path) -> None:
509500

510501

511502
def _antigravity_install(project_dir: Path) -> None:
512-
"""Install graphify for Google Antigravity: skill + .agents/rules + .agents/workflows."""
513-
# 1. Copy skill file to ~/.agents/skills/graphify/SKILL.md
503+
"""Install graphify for Google Antigravity: skill + .agent/rules + .agent/workflows."""
504+
# 1. Copy skill file to ~/.agent/skills/graphify/SKILL.md
514505
install(platform="antigravity")
515506

516507
# 1.5. Inject YAML frontmatter for native Antigravity tool discovery
@@ -521,7 +512,7 @@ def _antigravity_install(project_dir: Path) -> None:
521512
frontmatter = "---\nname: graphify-manager\ndescription: Rebuild the code graph or perform manual CLI queries when MCP server is offline.\n---\n\n"
522513
skill_dst.write_text(frontmatter + content, encoding="utf-8")
523514

524-
# 2. Write .agents/rules/graphify.md
515+
# 2. Write .agent/rules/graphify.md
525516
rules_path = project_dir / _ANTIGRAVITY_RULES_PATH
526517
rules_path.parent.mkdir(parents=True, exist_ok=True)
527518
if rules_path.exists():
@@ -530,7 +521,7 @@ def _antigravity_install(project_dir: Path) -> None:
530521
rules_path.write_text(_ANTIGRAVITY_RULES, encoding="utf-8")
531522
print(f"graphify rule written to {rules_path.resolve()}")
532523

533-
# 3. Write .agents/workflows/graphify.md
524+
# 3. Write .agent/workflows/graphify.md
534525
wf_path = project_dir / _ANTIGRAVITY_WORKFLOW_PATH
535526
wf_path.parent.mkdir(parents=True, exist_ok=True)
536527
if wf_path.exists():
@@ -648,7 +639,7 @@ def _cursor_uninstall(project_dir: Path) -> None:
648639
"""
649640

650641
_OPENCODE_PLUGIN_PATH = Path(".opencode") / "plugins" / "graphify.js"
651-
_OPENCODE_CONFIG_PATH = Path(".opencode") / "opencode.json"
642+
_OPENCODE_CONFIG_PATH = Path("opencode.json")
652643

653644

654645
def _install_opencode_plugin(project_dir: Path) -> None:
@@ -911,59 +902,6 @@ def claude_uninstall(project_dir: Path | None = None) -> None:
911902
_uninstall_claude_hook(project_dir or Path("."))
912903

913904

914-
def _clone_repo(url: str, branch: str | None = None, out_dir: Path | None = None) -> Path:
915-
"""Clone a GitHub repo to a local cache dir and return the path.
916-
917-
Clones into ~/.graphify/repos/<owner>/<repo> by default so repeated
918-
runs on the same URL reuse the existing clone (git pull instead of clone).
919-
"""
920-
import subprocess as _sp
921-
import re as _re
922-
923-
# Normalise URL — strip trailing .git if present
924-
url = url.rstrip("/")
925-
if not url.endswith(".git"):
926-
git_url = url + ".git"
927-
else:
928-
git_url = url
929-
url = url[:-4]
930-
931-
# Extract owner/repo from URL
932-
m = _re.search(r"github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$", url)
933-
if not m:
934-
print(f"error: not a recognised GitHub URL: {url}", file=sys.stderr)
935-
sys.exit(1)
936-
owner, repo = m.group(1), m.group(2)
937-
938-
if out_dir:
939-
dest = out_dir
940-
else:
941-
dest = Path.home() / ".graphify" / "repos" / owner / repo
942-
943-
if dest.exists():
944-
print(f"Repo already cloned at {dest} — pulling latest...", flush=True)
945-
cmd = ["git", "-C", str(dest), "pull"]
946-
if branch:
947-
cmd += ["origin", branch]
948-
result = _sp.run(cmd, capture_output=True, text=True)
949-
if result.returncode != 0:
950-
print(f"warning: git pull failed:\n{result.stderr}", file=sys.stderr)
951-
else:
952-
dest.parent.mkdir(parents=True, exist_ok=True)
953-
print(f"Cloning {url}{dest} ...", flush=True)
954-
cmd = ["git", "clone", "--depth", "1"]
955-
if branch:
956-
cmd += ["--branch", branch]
957-
cmd += [git_url, str(dest)]
958-
result = _sp.run(cmd, capture_output=True, text=True)
959-
if result.returncode != 0:
960-
print(f"error: git clone failed:\n{result.stderr}", file=sys.stderr)
961-
sys.exit(1)
962-
963-
print(f"Ready at: {dest}", flush=True)
964-
return dest
965-
966-
967905
def main() -> None:
968906
# Check all known skill install locations for a stale version stamp.
969907
# Skip during install/uninstall (hook writes trigger a fresh check anyway).
@@ -981,11 +919,6 @@ def main() -> None:
981919
print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
982920
print(" explain \"X\" plain-language explanation of a node and its neighbors")
983921
print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
984-
print(" clone <github-url> clone a GitHub repo locally and print its path for /graphify")
985-
print(" merge-graphs <g1> <g2> merge two or more graph.json files into one cross-repo graph")
986-
print(" --out <path> output path (default: graphify-out/merged-graph.json)")
987-
print(" --branch <branch> checkout a specific branch (default: repo default)")
988-
print(" --out <dir> clone to a custom directory (default: ~/.graphify/repos/<owner>/<repo>)")
989922
print(" add <url> fetch a URL and save it to ./raw, then update the graph")
990923
print(" --author \"Name\" tag the author of the content")
991924
print(" --contributor \"Name\" tag who added it to the corpus")
@@ -1003,8 +936,10 @@ def main() -> None:
1003936
print(" --type T query type: query|path_query|explain (default: query)")
1004937
print(" --nodes N1 N2 ... source node labels cited in the answer")
1005938
print(" --memory-dir DIR memory directory (default: graphify-out/memory)")
1006-
print(" check-update <path> check needs_update flag and notify if semantic re-extraction is pending (cron-safe)")
1007939
print(" benchmark [graph.json] measure token reduction vs naive full-corpus approach")
940+
print(" dashboard generate human-friendly architecture dashboard")
941+
print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
942+
print(" --output <path> output path (default graphify-out/dashboard.html)")
1008943
print(" hook install install post-commit/post-checkout git hooks (all platforms)")
1009944
print(" hook uninstall remove git hooks")
1010945
print(" hook status check if git hooks are installed")
@@ -1032,8 +967,8 @@ def main() -> None:
1032967
print(" trae uninstall remove graphify section from AGENTS.md")
1033968
print(" trae-cn install write graphify section to AGENTS.md (Trae CN)")
1034969
print(" trae-cn uninstall remove graphify section from AGENTS.md")
1035-
print(" antigravity install write .agents/rules + .agents/workflows + skill (Google Antigravity)")
1036-
print(" antigravity uninstall remove .agents/rules, .agents/workflows, and skill")
970+
print(" antigravity install write .agent/rules + .agent/workflows + skill (Google Antigravity)")
971+
print(" antigravity uninstall remove .agent/rules, .agent/workflows, and skill")
1037972
print(" hermes install write skill to ~/.hermes/skills/graphify/ (Hermes)")
1038973
print(" hermes uninstall remove skill from ~/.hermes/skills/graphify/")
1039974
print(" kiro install write skill to .kiro/skills/graphify/ + steering file (Kiro IDE/CLI)")
@@ -1415,73 +1350,6 @@ def main() -> None:
14151350
print("Nothing to update or rebuild failed — check output above.", file=sys.stderr)
14161351
sys.exit(1)
14171352

1418-
elif cmd == "check-update":
1419-
if len(sys.argv) < 3:
1420-
print("Usage: graphify check-update <path>", file=sys.stderr)
1421-
sys.exit(1)
1422-
from graphify.watch import check_update
1423-
check_update(Path(sys.argv[2]).resolve())
1424-
sys.exit(0)
1425-
elif cmd == "merge-graphs":
1426-
# graphify merge-graphs graph1.json graph2.json ... --out merged.json
1427-
args = sys.argv[2:]
1428-
graph_paths: list[Path] = []
1429-
out_path = Path("graphify-out/merged-graph.json")
1430-
i = 0
1431-
while i < len(args):
1432-
if args[i] == "--out" and i + 1 < len(args):
1433-
out_path = Path(args[i + 1]); i += 2
1434-
else:
1435-
graph_paths.append(Path(args[i])); i += 1
1436-
if len(graph_paths) < 2:
1437-
print("Usage: graphify merge-graphs <graph1.json> <graph2.json> [...] [--out merged.json]", file=sys.stderr)
1438-
sys.exit(1)
1439-
import networkx as _nx
1440-
from networkx.readwrite import json_graph as _jg
1441-
graphs = []
1442-
for gp in graph_paths:
1443-
if not gp.exists():
1444-
print(f"error: not found: {gp}", file=sys.stderr)
1445-
sys.exit(1)
1446-
data = json.loads(gp.read_text(encoding="utf-8"))
1447-
try:
1448-
G = _jg.node_link_graph(data, edges="links")
1449-
except TypeError:
1450-
G = _jg.node_link_graph(data)
1451-
# Tag every node with which repo it came from
1452-
repo_tag = gp.parent.parent.name # graphify-out/../ → repo dir name
1453-
for node in G.nodes:
1454-
G.nodes[node].setdefault("repo", repo_tag)
1455-
graphs.append(G)
1456-
merged = _nx.compose_all(graphs)
1457-
try:
1458-
out_data = _jg.node_link_data(merged, edges="links")
1459-
except TypeError:
1460-
out_data = _jg.node_link_data(merged)
1461-
out_path.parent.mkdir(parents=True, exist_ok=True)
1462-
out_path.write_text(json.dumps(out_data, indent=2), encoding="utf-8")
1463-
print(f"Merged {len(graphs)} graphs → {merged.number_of_nodes()} nodes, {merged.number_of_edges()} edges")
1464-
print(f"Written to: {out_path}")
1465-
1466-
elif cmd == "clone":
1467-
if len(sys.argv) < 3:
1468-
print("Usage: graphify clone <github-url> [--branch <branch>] [--out <dir>]", file=sys.stderr)
1469-
sys.exit(1)
1470-
url = sys.argv[2]
1471-
branch: str | None = None
1472-
out_dir: Path | None = None
1473-
args = sys.argv[3:]
1474-
i = 0
1475-
while i < len(args):
1476-
if args[i] == "--branch" and i + 1 < len(args):
1477-
branch = args[i + 1]; i += 2
1478-
elif args[i] == "--out" and i + 1 < len(args):
1479-
out_dir = Path(args[i + 1]); i += 2
1480-
else:
1481-
i += 1
1482-
local_path = _clone_repo(url, branch=branch, out_dir=out_dir)
1483-
print(local_path)
1484-
14851353
elif cmd == "benchmark":
14861354
from graphify.benchmark import run_benchmark, print_benchmark
14871355
graph_path = sys.argv[2] if len(sys.argv) > 2 else "graphify-out/graph.json"
@@ -1496,6 +1364,27 @@ def main() -> None:
14961364
pass
14971365
result = run_benchmark(graph_path, corpus_words=corpus_words)
14981366
print_benchmark(result)
1367+
elif cmd == "dashboard":
1368+
graph_path = "graphify-out/graph.json"
1369+
output_path = "graphify-out/dashboard.html"
1370+
args = sys.argv[2:]
1371+
i = 0
1372+
while i < len(args):
1373+
if args[i] == "--graph" and i + 1 < len(args):
1374+
graph_path = args[i + 1]; i += 2
1375+
elif args[i] == "--output" and i + 1 < len(args):
1376+
output_path = args[i + 1]; i += 2
1377+
else:
1378+
i += 1
1379+
gp = Path(graph_path)
1380+
if not gp.exists():
1381+
print(f"error: graph not found at {gp} — run /graphify first", file=sys.stderr)
1382+
sys.exit(1)
1383+
from graphify.dashboard import generate_dashboard
1384+
result = generate_dashboard(str(gp), output_path)
1385+
n = len(result["modules"])
1386+
print(f"Dashboard: {n} modules → {output_path}")
1387+
print("Open in browser — no server needed.")
14991388
else:
15001389
print(f"error: unknown command '{cmd}'", file=sys.stderr)
15011390
print("Run 'graphify --help' for usage.", file=sys.stderr)

0 commit comments

Comments
 (0)