From 6c9b5ef63c85d2f5a522ac457d0e28be17c9593e Mon Sep 17 00:00:00 2001 From: market4drill Date: Sat, 2 May 2026 15:15:15 +0200 Subject: [PATCH] Wire --no-viz flag in cluster-only + GRAPHIFY_VIZ_NODE_LIMIT env var (closes #541) (#565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(export): GRAPHIFY_VIZ_NODE_LIMIT env var to override MAX_NODES_FOR_VIZ Adds opt-in env var so users with large graphs (>5000 nodes) can either raise the HTML viz limit or disable viz entirely (set to 0) without patching the package. - New _viz_node_limit() helper reads GRAPHIFY_VIZ_NODE_LIMIT, falls back to MAX_NODES_FOR_VIZ on unset/empty/non-integer values. - to_html() now uses the helper; ValueError message references both --no-viz and GRAPHIFY_VIZ_NODE_LIMIT for discoverability. - 7 new tests in test_export.py covering default, raise/lower, zero, invalid, empty, and end-to-end to_html behavior. Addresses option 2 in #541 (env var as default behavior, zero-config for most users, tunable for large repos. Co-Authored-By: Claude Opus 4.7 (1M context) EOF ) * feat(cluster-only): wire --no-viz flag and tolerate ValueError mid-write Before this change, `graphify cluster-only ` called to_html() unconditionally, so on graphs >5000 nodes the ValueError fired AFTER graph.json was written but BEFORE the "Done — ..." print, leaving a stale graph.html on disk and no clear signal in the CLI output. This mirrors the watch.py rebuild pattern (already merged): try/except around to_html, delete the stale graph.html when skipping, and emit a "Done" line that accurately reflects which artifacts landed. - Parse "--no-viz" from sys.argv (documented as such in skill.md but previously never wired into the Python CLI). - Wrap to_html() in try/except so core outputs (graph.json + GRAPH_REPORT.md) always land. - Remove stale graph.html when viz is skipped or fails — avoids the silent desync where graph.html lags behind graph.json by days. - Update help text under `cluster-only` to advertise --no-viz. Addresses option 1 in #541 (--no-viz flag, simplest opt-in). Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Tayfun Sert,MBA Co-authored-by: Claude Opus 4.7 (1M context) --- graphify/__main__.py | 23 +++++++++++++-- graphify/export.py | 27 +++++++++++++++--- tests/test_export.py | 68 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/graphify/__main__.py b/graphify/__main__.py index 74ffb8b..87f3d7b 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -1002,6 +1002,7 @@ def main() -> None: print(" watch watch a folder and rebuild the graph on code changes") print(" update re-extract code files and update the graph (no LLM needed)") print(" cluster-only rerun clustering on an existing graph.json and regenerate report") + print(" --no-viz skip graph.html generation (useful for >5000 node graphs / CI)") print(" query \"\" BFS traversal of graph.json for a question") print(" --dfs use depth-first instead of breadth-first") print(" --budget N cap output at N tokens (default 2000)") @@ -1379,6 +1380,7 @@ def main() -> None: elif cmd == "cluster-only": watch_path = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(".") + no_viz = "--no-viz" in sys.argv graph_json = watch_path / "graphify-out" / "graph.json" if not graph_json.exists(): print(f"error: no graph found at {graph_json} — run /graphify first", file=sys.stderr) @@ -1407,8 +1409,25 @@ def main() -> None: out = watch_path / "graphify-out" (out / "GRAPH_REPORT.md").write_text(report, encoding="utf-8") to_json(G, communities, str(out / "graph.json")) - to_html(G, communities, str(out / "graph.html"), community_labels=labels or None) - print(f"Done — {len(communities)} communities. GRAPH_REPORT.md, graph.json and graph.html updated.") + + # Mirror watch.py pattern: gate to_html so core outputs (graph.json + + # GRAPH_REPORT.md) always land. Honor --no-viz explicitly; otherwise + # fall back to ValueError handling so an oversized graph doesn't crash + # the CLI mid-write and leave a stale graph.html on disk. + html_target = out / "graph.html" + if no_viz: + if html_target.exists(): + html_target.unlink() + print(f"Done — {len(communities)} communities. GRAPH_REPORT.md and graph.json updated (--no-viz; graph.html removed).") + else: + try: + to_html(G, communities, str(html_target), community_labels=labels or None) + print(f"Done — {len(communities)} communities. GRAPH_REPORT.md, graph.json and graph.html updated.") + except ValueError as viz_err: + if html_target.exists(): + html_target.unlink() + print(f"Skipped graph.html: {viz_err}") + print(f"Done — {len(communities)} communities. GRAPH_REPORT.md and graph.json updated.") elif cmd == "update": if len(sys.argv) > 2: diff --git a/graphify/export.py b/graphify/export.py index 220a940..2dce21d 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -25,6 +25,22 @@ COMMUNITY_COLORS = [ MAX_NODES_FOR_VIZ = 5_000 +def _viz_node_limit() -> int: + """Return the effective viz node limit, honoring GRAPHIFY_VIZ_NODE_LIMIT env var. + + Falls back to MAX_NODES_FOR_VIZ when the env var is unset, empty, or non-integer. + Set to 0 to disable HTML viz unconditionally (useful for CI runners). + """ + import os + raw = os.environ.get("GRAPHIFY_VIZ_NODE_LIMIT") + if raw is None or not raw.strip(): + return MAX_NODES_FOR_VIZ + try: + return int(raw) + except ValueError: + return MAX_NODES_FOR_VIZ + + def _html_styles() -> str: return """