diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce3fdb7e..eff4cbe3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) +## Unreleased + +- Feature: `--viz 3d` renders `graph.html` as a navigable WebGL view (three.js + d3-force-3d via a version-pinned, sha384-SRI `3d-force-graph` bundle, same supply-chain posture as the existing vis-network tag). Available on `/graphify`, `cluster-only`/`label` and `graphify export html`, or via `GRAPHIFY_VIZ_MODE`. The vis.js canvas renderer tops out around a few thousand nodes and offers pan/zoom as its only navigation; WebGL raises that ceiling by roughly an order of magnitude. The layout runs its simulation before the first frame and the camera is framed once at startup, so the graph opens already spread out and never moves on its own. +- Feature: 3D navigation — search, click-to-inspect with neighbour links, community filters, a single "Show" control that isolates 1-6 hops around the selection, an opt-in name overlay (hover names the node either way), and `/` `F` `L` `R` `Space` `Esc` shortcuts. Learning-overlay status is painted onto the node rather than ringing it, and hyperedges render as clickable hub nodes instead of shaded hulls. +- Refactor: `to_html()` now builds a renderer-agnostic view model (nodes, edges, legend, hyperedges) that both the 2D and 3D renderers consume, so the two views always describe the same graph. The default stays 2D and its output is byte-identical to before, guarded by a test. + ## 0.9.28 (2026-07-27) - Fix: incremental extraction no longer drops cross-file edges whose target file wasn't in the batch (#2211, #2213). Python relative imports and markdown reference links emitted absolute-path-derived target ids without the `target_file` stamp the incremental canonicalization needs, so a re-extracted file's imports/references dangled or vanished; both now stamp the resolved target and canonicalize to the root-relative node. diff --git a/README.md b/README.md index a4f188f46..50e3f3db2 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ That's it. You get **three files**: ``` graphify-out/ -├── graph.html open in any browser — click nodes, filter, search +├── graph.html open in any browser — click nodes, filter, search (`--viz 3d` for the WebGL 3D view) ├── GRAPH_REPORT.md the highlights: key concepts, surprising connections, suggested questions └── graph.json the full graph — query it anytime without re-reading your files ``` @@ -371,6 +371,7 @@ You can also set `GRAPHIFY_GOOGLE_WORKSPACE=1`. Graphify exports shortcuts into /graphify . --cluster-only --resolution 1.5 # more granular communities /graphify . --cluster-only --exclude-hubs 99 # suppress utility super-hubs from god-node rankings /graphify . --no-viz # skip the HTML, just the report + JSON +/graphify . --viz 3d # render graph.html as a navigable WebGL 3D view /graphify . --wiki # build a markdown wiki from the graph graphify export callflow-html # Mermaid architecture/call-flow HTML (auto-regenerates on every git commit if hook is installed) diff --git a/graphify/cli.py b/graphify/cli.py index bd8f12bca..afbfd04c3 100644 --- a/graphify/cli.py +++ b/graphify/cli.py @@ -14,6 +14,11 @@ from graphify.paths import GRAPHIFY_OUT as _GRAPHIFY_OUT from pathlib import Path +# Accepted `--viz` values. Kept as a literal so `graphify --help` and the arg +# validation below don't drag the exporter (and networkx) into every startup; +# the tuple is asserted against exporters.html.VIZ_MODES in the test suite. +_VIZ_MODES = ("2d", "3d") + _SEARCH_NUDGE = json.dumps({ "hookSpecificOutput": { @@ -1559,6 +1564,7 @@ def dispatch_command(cmd: str) -> None: label_model = _model_arg.split("=", 1)[1] if _model_arg else None _min_cs_arg = next((a for a in sys.argv if a.startswith("--min-community-size=")), None) min_community_size = int(_min_cs_arg.split("=")[1]) if _min_cs_arg else 3 + viz_mode: str | None = None args = sys.argv[2:] watch_path: Path | None = None graph_override: Path | None = None @@ -1595,6 +1601,13 @@ def dispatch_command(cmd: str) -> None: label_batch_size = int(args[i_arg + 1]); i_arg += 2 elif a.startswith("--batch-size="): label_batch_size = int(a.split("=", 1)[1]); i_arg += 1 + # Must precede the `--`-prefixed catch-all: without an explicit arm + # the flag is skipped and its value falls through to the positional + # path slot, so `--viz 3d` would try to cluster a directory "3d". + elif a == "--viz" and i_arg + 1 < len(args): + viz_mode = args[i_arg + 1]; i_arg += 2 + elif a.startswith("--viz="): + viz_mode = a.split("=", 1)[1]; i_arg += 1 elif a in ("--no-viz", "--missing-only") or a.startswith("--min-community-size="): i_arg += 1 elif a.startswith("--"): @@ -1605,6 +1618,10 @@ def dispatch_command(cmd: str) -> None: i_arg += 1 if watch_path is None: watch_path = Path(".") + if viz_mode is not None and viz_mode.strip().lower() not in _VIZ_MODES: + print(f"error: --viz must be one of {', '.join(_VIZ_MODES)} (got {viz_mode!r})", + file=sys.stderr) + sys.exit(1) graph_json = graph_override if graph_override is not None else watch_path / _GRAPHIFY_OUT / "graph.json" if not graph_json.exists(): print( @@ -1857,7 +1874,7 @@ def dispatch_command(cmd: str) -> None: # path so an oversized graph still renders a usable graph.html. _node_limit = 5000 if _over_cap else None to_html(G, communities, str(html_target), community_labels=labels or None, - node_limit=_node_limit) + node_limit=_node_limit, mode=viz_mode) stages.mark("export"); stages.total() print(f"Done - {len(communities)} communities. GRAPH_REPORT.md, graph.json and graph.html updated.") except ValueError as viz_err: @@ -2163,7 +2180,7 @@ def _to_simple(g: "_nx.Graph") -> "_nx.Graph": subcmd = sys.argv[2] if len(sys.argv) > 2 else "" if subcmd not in ("html", "callflow-html", "obsidian", "wiki", "svg", "graphml", "neo4j", "falkordb"): print("Usage: graphify export ", file=sys.stderr) - print(" html [--graph PATH] [--labels PATH] [--node-limit N] [--no-viz]", file=sys.stderr) + print(" html [--graph PATH] [--labels PATH] [--node-limit N] [--no-viz] [--viz 2d|3d]", file=sys.stderr) print(" callflow-html [GRAPH|DIR] [--graph PATH] [--labels PATH] [--report PATH] [--sections PATH] [--output HTML]", file=sys.stderr) print(" [--lang auto|zh-CN|en] [--max-sections N] [--diagram-scale N]", file=sys.stderr) print(" obsidian [--graph PATH] [--labels PATH] [--dir PATH]", file=sys.stderr) @@ -2194,6 +2211,7 @@ def _to_simple(g: "_nx.Graph") -> "_nx.Graph": analysis_path = Path(_GRAPHIFY_OUT) / ".graphify_analysis.json" node_limit = 5000 no_viz = False + export_viz_mode: str | None = None obsidian_dir = Path(_GRAPHIFY_OUT) / "obsidian" # Shared push-connection settings for the graph-database sinks (neo4j, # falkordb), parsed from the generic --push/--user/--password flags below. @@ -2254,6 +2272,10 @@ def _to_simple(g: "_nx.Graph") -> "_nx.Graph": node_limit = int(args[i + 1]); i += 2 elif a == "--no-viz": no_viz = True; i += 1 + elif a == "--viz" and i + 1 < len(args): + export_viz_mode = args[i + 1]; i += 2 + elif a.startswith("--viz="): + export_viz_mode = a.split("=", 1)[1]; i += 1 elif a == "--dir" and i + 1 < len(args): obsidian_dir = Path(args[i + 1]); i += 2 elif a == "--push" and i + 1 < len(args): @@ -2383,6 +2405,10 @@ def _to_simple(g: "_nx.Graph") -> "_nx.Graph": if subcmd == "html": from graphify.export import to_html as _to_html + if export_viz_mode is not None and export_viz_mode.strip().lower() not in _VIZ_MODES: + print(f"error: --viz must be one of {', '.join(_VIZ_MODES)} (got {export_viz_mode!r})", + file=sys.stderr) + sys.exit(1) if no_viz: html_target = out_dir / "graph.html" if html_target.exists(): @@ -2393,7 +2419,8 @@ def _to_simple(g: "_nx.Graph") -> "_nx.Graph": # path so the oversized graph still renders a usable artifact. _effective_node_limit = 5000 if _over_cap else node_limit _to_html(G, communities, str(out_dir / "graph.html"), - community_labels=labels or None, node_limit=_effective_node_limit) + community_labels=labels or None, node_limit=_effective_node_limit, + mode=export_viz_mode) if G.number_of_nodes() <= _effective_node_limit: print(f"graph.html written - open in any browser, no server needed") if _over_cap: diff --git a/graphify/exporters/html.py b/graphify/exporters/html.py index 59c0e52e3..43fae6643 100644 --- a/graphify/exporters/html.py +++ b/graphify/exporters/html.py @@ -12,6 +12,22 @@ MAX_NODES_FOR_VIZ = 5_000 +#: Renderers `to_html` can dispatch to. "2d" is the long-standing vis.js canvas +#: view; "3d" is the WebGL view in graphify.exporters.html3d. +VIZ_MODES = ("2d", "3d") + +def _resolve_viz_mode(mode: str | None) -> str: + """Pick the renderer: explicit argument > GRAPHIFY_VIZ_MODE > "2d". + + Defaults to 2d so existing callers keep their byte-identical output; an + unknown value falls back to 2d rather than raising, matching how + _viz_node_limit() treats a malformed env var. + """ + import os + raw = mode if mode is not None else os.environ.get("GRAPHIFY_VIZ_MODE", "") + candidate = (raw or "").strip().lower() + return candidate if candidate in VIZ_MODES else "2d" + def _viz_node_limit() -> int: """Return the effective viz node limit, honoring GRAPHIFY_VIZ_NODE_LIMIT env var. @@ -330,19 +346,25 @@ def to_html( member_counts: dict[int, int] | None = None, node_limit: int | None = None, learning_overlay: dict | None = None, + mode: str | None = None, ) -> None: - """Generate an interactive vis.js HTML visualization of the graph. + """Generate an interactive HTML visualization of the graph. Features: node size by degree, click-to-inspect panel, search box, community filter, physics clustering by community, confidence-styled edges. Raises ValueError if graph exceeds MAX_NODES_FOR_VIZ. + `mode` selects the renderer — "2d" (vis.js canvas, the default) or "3d" + (WebGL via 3d-force-graph). Both are fed the identical node/edge/legend + model built below, so the two views always describe the same graph. + If member_counts is provided (aggregated community view), node sizes are based on community member counts rather than graph degree. If node_limit is set and the graph exceeds it, automatically builds an aggregated community-level meta-graph instead of raising ValueError. """ + mode = _resolve_viz_mode(mode) limit = node_limit if node_limit is not None else _viz_node_limit() if G.number_of_nodes() > limit: if node_limit is not None: @@ -392,7 +414,7 @@ def to_html( }) meta.graph["hyperedges"] = remapped to_html(meta, meta_communities, output_path, - community_labels=community_labels, member_counts=mc) + community_labels=community_labels, member_counts=mc, mode=mode) print(f"graph.html written (aggregated: {meta.number_of_nodes()} community nodes, {meta.number_of_edges()} cross-community edges)") print("Tip: run with --obsidian for full node-level detail.") return @@ -511,18 +533,46 @@ def to_html( n = member_counts.get(cid, len(communities.get(cid, []))) if member_counts else len(communities.get(cid, [])) legend_data.append({"cid": cid, "color": color, "label": lbl, "count": n}) - # Escape sequences so embedded JSON cannot break out of the script tag - def _js_safe(obj) -> str: - return json.dumps(obj).replace(" +def js_safe(obj) -> str: + """Serialize `obj` for embedding in a ` from + closing the tag early and turning graph data into markup. + """ + return json.dumps(obj).replace(" str: + """Render the view model as the vis.js 2D canvas viewer.""" + nodes_json = js_safe(model["nodes"]) + edges_json = js_safe(model["edges"]) + legend_json = js_safe(model["legend"]) + hyperedges_json = js_safe(model["hyperedges"]) + title = model["title"] + stats = model["stats"] + + return f""" @@ -556,5 +606,3 @@ def _js_safe(obj) -> str: {_hyperedge_script(hyperedges_json)} """ - - Path(output_path).write_text(html, encoding="utf-8") # nosec diff --git a/graphify/exporters/html3d.py b/graphify/exporters/html3d.py new file mode 100644 index 000000000..ddd393756 --- /dev/null +++ b/graphify/exporters/html3d.py @@ -0,0 +1,730 @@ +"""html3d — WebGL renderer for the graph viewer (`--viz 3d`). + +Consumes the same view model as the vis.js renderer in graphify.exporters.html +(nodes, edges, legend, hyperedges) and draws it with 3d-force-graph, a UMD +bundle of three.js + d3-force-3d. The canvas renderer tops out around a few +thousand nodes; WebGL pushes that an order of magnitude further, and the extra +dimension gives dense communities somewhere to spread out. + +Two deliberate differences from the 2D view, both downstream of one constraint: +the bundle keeps its three.js instance private, so we cannot build custom +geometry without shipping a second copy of three. + + * Learning-overlay status is painted onto the node itself (green/amber/grey) + instead of ringing it. A sphere has no border to tint, and the community + colour stays legible in the legend, the search list and the info panel. + * Hyperedges become a synthetic hub node wired to every member (the standard + star expansion) rather than a shaded hull. Same information, and the hub is + clickable, which the hull never was. + +Labels are DOM elements projected onto the canvas each frame rather than +sprites, which keeps text crisp, keeps it escapable, and costs no extra +dependency. Only the highest-degree visible nodes plus the current selection +get one — a label per node is unreadable in 3D long before it is slow. +""" +from __future__ import annotations + +from graphify.exporters.html import _html_styles, js_safe + +# Pinned + SRI-hashed exactly like the vis.js tag in the 2D renderer: an +# unpinned CDN URL would let a compromised registry ship arbitrary JavaScript +# into every rendered graph. Verified against the upstream file at +# https://unpkg.com/3d-force-graph@1.80.0/dist/3d-force-graph.min.js +# Bumping the version MUST recompute the hash: +# curl -sL | openssl dgst -sha384 -binary | openssl base64 -A +FORCE_GRAPH_VERSION = "1.80.0" +FORCE_GRAPH_URL = ( + f"https://unpkg.com/3d-force-graph@{FORCE_GRAPH_VERSION}/dist/3d-force-graph.min.js" +) +FORCE_GRAPH_SRI = "sha384-Y7bC2PBKu8ujxtvo5+Z61OeGdSVRzFsYWBK4i5dnL/U6aFDTodk61qOUkTfInaxS" + + +def _styles_3d() -> str: + """Styles layered on top of the shared sidebar CSS from the 2D renderer.""" + return """""" + + +# The viewer script. Kept as a plain (non-f) string so the JS can use `${...}` +# template literals and object literals without brace-doubling; the four data +# placeholders are substituted by render() below. +_SCRIPT_3D = r"""""" + + +def render(model: dict) -> str: + """Render the shared view model as the 3d-force-graph WebGL viewer.""" + script = ( + _SCRIPT_3D + .replace("/*__NODES__*/null", js_safe(model["nodes"])) + .replace("/*__EDGES__*/null", js_safe(model["edges"])) + .replace("/*__LEGEND__*/null", js_safe(model["legend"])) + .replace("/*__HYPEREDGES__*/null", js_safe(model["hyperedges"])) + ) + hyper_btn = ( + '' + if model["hyperedges"] else "" + ) + return f""" + + + +graphify 3D - {model["title"]} + +{_html_styles()} +{_styles_3d()} + + +
+ +{script} + +""" diff --git a/graphify/skill-agents.md b/graphify/skill-agents.md index afb4ecc12..35e6079fb 100644 --- a/graphify/skill-agents.md +++ b/graphify/skill-agents.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -532,6 +533,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-amp.md b/graphify/skill-amp.md index afb4ecc12..35e6079fb 100644 --- a/graphify/skill-amp.md +++ b/graphify/skill-amp.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -532,6 +533,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-claw.md b/graphify/skill-claw.md index d98865cc8..149f68207 100644 --- a/graphify/skill-claw.md +++ b/graphify/skill-claw.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-codex.md b/graphify/skill-codex.md index 0c821a278..49c3bcc01 100644 --- a/graphify/skill-codex.md +++ b/graphify/skill-codex.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -532,6 +533,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-copilot.md b/graphify/skill-copilot.md index d98865cc8..149f68207 100644 --- a/graphify/skill-copilot.md +++ b/graphify/skill-copilot.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-droid.md b/graphify/skill-droid.md index c3815d556..1f464bb8a 100644 --- a/graphify/skill-droid.md +++ b/graphify/skill-droid.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -532,6 +533,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-kilo.md b/graphify/skill-kilo.md index dbb4658ca..ebc336896 100644 --- a/graphify/skill-kilo.md +++ b/graphify/skill-kilo.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-kiro.md b/graphify/skill-kiro.md index d98865cc8..149f68207 100644 --- a/graphify/skill-kiro.md +++ b/graphify/skill-kiro.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-opencode.md b/graphify/skill-opencode.md index cf5dae440..fc4055b00 100644 --- a/graphify/skill-opencode.md +++ b/graphify/skill-opencode.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -527,6 +528,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-pi.md b/graphify/skill-pi.md index d98865cc8..149f68207 100644 --- a/graphify/skill-pi.md +++ b/graphify/skill-pi.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-trae.md b/graphify/skill-trae.md index b0cbeb122..c3746b44b 100644 --- a/graphify/skill-trae.md +++ b/graphify/skill-trae.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -533,6 +534,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-vscode.md b/graphify/skill-vscode.md index 3e6bc6b7b..cebe01559 100644 --- a/graphify/skill-vscode.md +++ b/graphify/skill-vscode.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -531,6 +532,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill-windows.md b/graphify/skill-windows.md index 574384576..452bb1275 100644 --- a/graphify/skill-windows.md +++ b/graphify/skill-windows.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -557,6 +558,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/graphify/skill.md b/graphify/skill.md index d98865cc8..149f68207 100644 --- a/graphify/skill.md +++ b/graphify/skill.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify --cluster-only # rerun clustering on existing graph /graphify --no-viz # skip visualization, just report + JSON +/graphify --viz 3d # render graph.html as an interactive 3D WebGL view /graphify --html # (HTML is generated by default - this flag is a no-op) /graphify --svg # also export graph.svg (embeds in Notion, GitHub) /graphify --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tests/test_export_html3d.py b/tests/test_export_html3d.py new file mode 100644 index 000000000..a43040c7a --- /dev/null +++ b/tests/test_export_html3d.py @@ -0,0 +1,258 @@ +"""Tests for the 3D (WebGL) graph renderer behind `--viz 3d`.""" +import json +import re +import tempfile +from pathlib import Path + +import networkx as nx +import pytest + +from graphify.build import build_from_json +from graphify.cluster import cluster +from graphify.export import to_html +from graphify.exporters.html import VIZ_MODES, _resolve_viz_mode +from graphify.exporters.html3d import FORCE_GRAPH_SRI, FORCE_GRAPH_VERSION + +FIXTURES = Path(__file__).parent / "fixtures" + + +def make_graph(): + return build_from_json(json.loads((FIXTURES / "extraction.json").read_text())) + + +def render(mode=None, **kwargs): + G = kwargs.pop("G", None) or make_graph() + communities = kwargs.pop("communities", None) or cluster(G) + with tempfile.TemporaryDirectory() as tmp: + out = Path(tmp) / "graph.html" + to_html(G, communities, str(out), mode=mode, **kwargs) + return out.read_text(encoding="utf-8") + + +def test_viz_3d_loads_force_graph_pinned_with_sri(): + """The 3D bundle must be pinned to a version and carry a sha384 SRI hash + plus crossorigin=anonymous, exactly like the vis.js tag in the 2D view. + Without it a compromised CDN could ship arbitrary JavaScript into every + rendered graph. Bumping FORCE_GRAPH_VERSION MUST recompute the hash.""" + content = render("3d") + assert f"3d-force-graph@{FORCE_GRAPH_VERSION}/dist/3d-force-graph.min.js" in content + assert "https://unpkg.com/3d-force-graph/dist" not in content + assert f'integrity="{FORCE_GRAPH_SRI}"' in content + assert FORCE_GRAPH_SRI.startswith("sha384-") + assert 'crossorigin="anonymous"' in content + + +def test_viz_3d_does_not_pull_in_visjs(): + content = render("3d") + assert "vis-network" not in content + assert "ForceGraph3D(" in content + + +def test_default_mode_is_unchanged_2d(): + """Adding the 3D renderer must not change what existing callers get.""" + G = make_graph() + communities = cluster(G) + with tempfile.TemporaryDirectory() as tmp: + default_out, explicit_out = Path(tmp) / "a.html", Path(tmp) / "b.html" + to_html(G, communities, str(default_out)) + to_html(G, communities, str(explicit_out), mode="2d") + # The output path is echoed in the , so normalize it out. + default = default_out.read_text(encoding="utf-8").replace("a.html", "X.html") + explicit = explicit_out.read_text(encoding="utf-8").replace("b.html", "X.html") + assert "vis-network" in default + assert "3d-force-graph" not in default + assert default == explicit + + +def test_viz_3d_carries_the_same_node_and_edge_data(): + content = render("3d") + assert "RAW_NODES" in content + assert "RAW_EDGES" in content + assert "LEGEND" in content + # The placeholders in the script template must all have been substituted. + assert "/*__NODES__*/null" not in content + assert "/*__EDGES__*/null" not in content + assert "/*__LEGEND__*/null" not in content + assert "/*__HYPEREDGES__*/null" not in content + + +def test_viz_3d_neighbor_links_have_no_inline_onclick_xss(): + """#1838 applies to the 3D panel too: a node id containing a double quote + must not be able to break out of an inline handler. Same escaped data + attribute + single delegated listener as the 2D renderer.""" + content = render("3d") + assert 'onclick="selectNode(' not in content + assert 'onclick="focusNode(' not in content + assert 'data-nid="${esc(nid)}"' in content + assert "closest('.neighbor-link')" in content + + +def test_viz_3d_exposes_navigation_controls(): + content = render("3d") + for control in ("btn-reset", "btn-freeze", "focus-depth", "cb-labels"): + assert f'id="{control}"' in content, control + # k-hop focus and the keyboard shortcuts are what make it navigable. + assert "recomputeFocus" in content + assert "graph2ScreenCoords" in content + + +def test_viz_3d_focus_is_a_single_control(): + """A Focus on/off button plus a separate depth dropdown read as two settings + with an unexplained relationship. The depth select *is* the switch: 0 shows + the whole graph, anything higher isolates that many hops.""" + content = render("3d") + assert 'id="btn-focus"' not in content + assert '<option value="0" selected>whole graph</option>' in content + for depth in range(1, 7): + assert f'<option value="{depth}">selection + {depth} hop' in content, depth + assert "let focusDepth = 0" in content + assert "if (focusDepth < 1 || selectedId === null) return;" in content + + +def test_viz_3d_legend_shortcuts_survive_the_removed_focus_button(): + """F and L act on the community under the cursor: F isolates it, L isolates + it and names it. A focused <select> swallows letter keys as native option + typeahead, which silently ate the shortcut and stepped the depth dropdown + instead, so it is suppressed and the select blurs after use.""" + content = render("3d") + assert "hoveredCommunity = c.cid" in content + assert "if (hoveredCommunity !== null) isolateCommunity(hoveredCommunity);" in content + assert "if (hoveredCommunity !== null) setLabels(isolateCommunity(hoveredCommunity));" in content + # Typeahead suppression + blur, the two halves of the swallowed-key fix. + assert "e.target instanceof HTMLSelectElement) e.preventDefault();" in content + assert "depthSel.blur();" in content + # Isolating is a toggle, and it drives the checkbox UI rather than only the set. + assert "function isCommunityIsolated(cid)" in content + assert "function setCommunityHidden(cid, hidden)" in content + + +def test_viz_3d_names_are_off_by_default(): + """Names are opt-in — 1500 sprites in a rotating volume is noise. The hover + tooltip must still name the node whether or not the overlay is on.""" + content = render("3d") + assert "let showLabels = false;" in content + assert '<input type="checkbox" class="view-cb" id="cb-labels">' in content + assert 'id="cb-labels" checked' not in content + # nodeLabel drives the hover tooltip and is never gated on showLabels. + assert ".nodeLabel(n => '<div class=\"gtip\">' + n.tip + '</div>')" in content + + +def test_viz_3d_starts_pre_expanded_and_bounded(): + """The stock d3-force-3d setup erupts from a point over ~10s and lets + disconnected components coast outward forever. Warmup ticks move the + simulation before the first frame; the gravity force and the capped + repulsion range keep the result compact.""" + content = render("3d") + assert ".warmupTicks(WARMUP_TICKS)" in content + assert "gravityForce" in content + assert "charge.distanceMax(CHARGE_MAX_DISTANCE)" in content + + +def test_viz_3d_layout_constants_stay_in_their_working_range(): + """These four numbers were tuned against a 1.6k-node graph in a browser, and + each has a failure mode outside its range: gravity above ~0.08 packs the + graph into a featureless ball, and a repulsion cap far past the graph radius + makes the warmup ticks expensive enough to freeze the tab on load.""" + content = render("3d") + values = { + name: float(re.search(rf"const {name} = (-?[\d.]+);", content).group(1)) + for name in ("GRAVITY_STRENGTH", "CHARGE_MAX_DISTANCE", + "CHARGE_STRENGTH", "WARMUP_TICKS") + } + assert 0 < values["GRAVITY_STRENGTH"] <= 0.08 + assert 200 <= values["CHARGE_MAX_DISTANCE"] <= 450 + assert values["CHARGE_STRENGTH"] < 0 + assert 0 < values["WARMUP_TICKS"] <= 200 + + +def test_viz_3d_frames_once_at_startup_without_zoom_to_fit(): + """The camera must be placed once, early, and then left alone. zoomToFit() + animates even when asked for a zero-duration transition, so using it here + reads as the view drifting or lurching out from under the user; and framing + off a timer runs before the warmup ticks land, measuring a radius of zero + and parking the camera inside the graph.""" + content = render("3d") + calls = [ln for ln in content.splitlines() + if "zoomToFit" in ln and not ln.strip().startswith("//")] + assert not calls, calls + # Framed from the first tick that has real positions, exactly once. + assert "if (framed || layoutRadius() < 1) return;" in content + assert "graph.onEngineTick(" in content + # Placement is direct, with no transition. + assert "graph.cameraPosition({ x: 0, y: 0, z: dist }, { x: 0, y: 0, z: 0 }, 0);" in content + + +def test_viz_3d_never_calls_refresh(): + """graph.refresh() snaps the camera back to its default, silently undoing + every fly-to. Accessor re-assignment (restyle) is the supported way to make + the renderer re-evaluate visibility and colour.""" + content = render("3d") + calls = [ln for ln in content.splitlines() + if "graph.refresh()" in ln and not ln.strip().startswith("//")] + assert not calls, calls + assert "function restyle()" in content + + +def test_viz_3d_hyperedges_become_a_star_expansion(): + """3D has no hull shading, so each hyperedge is rendered as a hub node + linked to its members. The toggle only appears when there are any.""" + G = make_graph() + members = list(G.nodes())[:3] + G.graph["hyperedges"] = [{"id": "h1", "label": "shared pipeline", "nodes": members}] + content = render("3d", G=G) + assert '"shared pipeline"' in content + assert "__hyperedge_" in content + assert 'id="btn-hyper"' in content + + G.graph["hyperedges"] = [] + assert 'id="btn-hyper"' not in render("3d", G=G) + + +def test_viz_3d_survives_a_node_id_with_quotes_and_script_tags(): + G = nx.Graph() + G.add_node('a"b', label='</script><img src=x onerror=alert(1)>', file_type="py") + G.add_node("c", label="plain", file_type="py") + G.add_edge('a"b', "c", relation="calls", confidence="EXTRACTED") + content = render("3d", G=G, communities={0: ['a"b', "c"]}) + # The embedded JSON must not be able to close the script tag early. + assert "</script><img" not in content + assert "<\\/script>" in content + + +def test_over_limit_aggregation_keeps_the_requested_mode(): + """The community-aggregation fallback re-enters to_html; it must not drop + back to the 2D renderer when the caller asked for 3D.""" + G = make_graph() + communities = cluster(G) + if len(communities) < 2: + pytest.skip("fixture collapses to a single community") + with tempfile.TemporaryDirectory() as tmp: + out = Path(tmp) / "graph.html" + to_html(G, communities, str(out), node_limit=1, mode="3d") + content = out.read_text(encoding="utf-8") + assert "3d-force-graph" in content + assert "vis-network" not in content + + +@pytest.mark.parametrize("raw,expected", [ + (None, "2d"), ("", "2d"), ("2d", "2d"), ("3d", "3d"), + ("3D", "3d"), (" 3d ", "3d"), ("webgl", "2d"), ("nonsense", "2d"), +]) +def test_resolve_viz_mode(raw, expected): + assert _resolve_viz_mode(raw) == expected + + +def test_viz_mode_env_var(monkeypatch): + monkeypatch.setenv("GRAPHIFY_VIZ_MODE", "3d") + assert "3d-force-graph" in render() + # An explicit argument still wins over the environment. + assert "vis-network" in render("2d") + monkeypatch.setenv("GRAPHIFY_VIZ_MODE", "bogus") + assert "vis-network" in render() + + +def test_cli_viz_modes_match_the_exporter(): + """cli.py hardcodes the accepted values to keep networkx off the startup + path; the two lists must not drift.""" + from graphify.cli import _VIZ_MODES + assert tuple(_VIZ_MODES) == tuple(VIZ_MODES) diff --git a/tools/skillgen/expected/graphify__skill-agents.md b/tools/skillgen/expected/graphify__skill-agents.md index afb4ecc12..35e6079fb 100644 --- a/tools/skillgen/expected/graphify__skill-agents.md +++ b/tools/skillgen/expected/graphify__skill-agents.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -532,6 +533,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-amp.md b/tools/skillgen/expected/graphify__skill-amp.md index afb4ecc12..35e6079fb 100644 --- a/tools/skillgen/expected/graphify__skill-amp.md +++ b/tools/skillgen/expected/graphify__skill-amp.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -532,6 +533,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-claw.md b/tools/skillgen/expected/graphify__skill-claw.md index d98865cc8..149f68207 100644 --- a/tools/skillgen/expected/graphify__skill-claw.md +++ b/tools/skillgen/expected/graphify__skill-claw.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-codex.md b/tools/skillgen/expected/graphify__skill-codex.md index 0c821a278..49c3bcc01 100644 --- a/tools/skillgen/expected/graphify__skill-codex.md +++ b/tools/skillgen/expected/graphify__skill-codex.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -532,6 +533,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-copilot.md b/tools/skillgen/expected/graphify__skill-copilot.md index d98865cc8..149f68207 100644 --- a/tools/skillgen/expected/graphify__skill-copilot.md +++ b/tools/skillgen/expected/graphify__skill-copilot.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-droid.md b/tools/skillgen/expected/graphify__skill-droid.md index c3815d556..1f464bb8a 100644 --- a/tools/skillgen/expected/graphify__skill-droid.md +++ b/tools/skillgen/expected/graphify__skill-droid.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -532,6 +533,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-kilo.md b/tools/skillgen/expected/graphify__skill-kilo.md index dbb4658ca..ebc336896 100644 --- a/tools/skillgen/expected/graphify__skill-kilo.md +++ b/tools/skillgen/expected/graphify__skill-kilo.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-kiro.md b/tools/skillgen/expected/graphify__skill-kiro.md index d98865cc8..149f68207 100644 --- a/tools/skillgen/expected/graphify__skill-kiro.md +++ b/tools/skillgen/expected/graphify__skill-kiro.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-opencode.md b/tools/skillgen/expected/graphify__skill-opencode.md index cf5dae440..fc4055b00 100644 --- a/tools/skillgen/expected/graphify__skill-opencode.md +++ b/tools/skillgen/expected/graphify__skill-opencode.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -527,6 +528,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-pi.md b/tools/skillgen/expected/graphify__skill-pi.md index d98865cc8..149f68207 100644 --- a/tools/skillgen/expected/graphify__skill-pi.md +++ b/tools/skillgen/expected/graphify__skill-pi.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-trae.md b/tools/skillgen/expected/graphify__skill-trae.md index b0cbeb122..c3746b44b 100644 --- a/tools/skillgen/expected/graphify__skill-trae.md +++ b/tools/skillgen/expected/graphify__skill-trae.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -533,6 +534,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-vscode.md b/tools/skillgen/expected/graphify__skill-vscode.md index 3e6bc6b7b..cebe01559 100644 --- a/tools/skillgen/expected/graphify__skill-vscode.md +++ b/tools/skillgen/expected/graphify__skill-vscode.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -531,6 +532,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill-windows.md b/tools/skillgen/expected/graphify__skill-windows.md index 574384576..452bb1275 100644 --- a/tools/skillgen/expected/graphify__skill-windows.md +++ b/tools/skillgen/expected/graphify__skill-windows.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -557,6 +558,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/expected/graphify__skill.md b/tools/skillgen/expected/graphify__skill.md index d98865cc8..149f68207 100644 --- a/tools/skillgen/expected/graphify__skill.md +++ b/tools/skillgen/expected/graphify__skill.md @@ -21,6 +21,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -535,6 +536,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags) diff --git a/tools/skillgen/fragments/core/core.md b/tools/skillgen/fragments/core/core.md index e28910728..06fdc04dc 100644 --- a/tools/skillgen/fragments/core/core.md +++ b/tools/skillgen/fragments/core/core.md @@ -18,6 +18,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti /graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy /graphify <path> --cluster-only # rerun clustering on existing graph /graphify <path> --no-viz # skip visualization, just report + JSON +/graphify <path> --viz 3d # render graph.html as an interactive 3D WebGL view /graphify <path> --html # (HTML is generated by default - this flag is a no-op) /graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub) /graphify <path> --graphml # export graph.graphml (Gephi, yEd) @@ -470,6 +471,7 @@ Generate the HTML graph (always, unless `--no-viz`): ```bash graphify export html # auto-aggregates to community view if graph > 5000 nodes # or: graphify export html --no-viz +# or: graphify export html --viz 3d # only when the user asked for --viz 3d ``` ### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags)