From 75acd3aa3ee44d291b01a81886047115d1f1b8d3 Mon Sep 17 00:00:00 2001 From: Andrew Yim Date: Tue, 16 Jun 2026 19:39:53 +0900 Subject: [PATCH] feat(okf): add minimal okf_viz Cytoscape generator with tests Full suite green (20 tests). Smoke-tested on Google crypto_bitcoin bundle. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../97-ourdigital-okf/code/scripts/okf_viz.py | 115 ++++++++++++++++++ .../code/scripts/tests/test_okf_viz.py | 36 ++++++ 2 files changed, 151 insertions(+) create mode 100644 custom-skills/97-ourdigital-okf/code/scripts/okf_viz.py create mode 100644 custom-skills/97-ourdigital-okf/code/scripts/tests/test_okf_viz.py diff --git a/custom-skills/97-ourdigital-okf/code/scripts/okf_viz.py b/custom-skills/97-ourdigital-okf/code/scripts/okf_viz.py new file mode 100644 index 0000000..414472d --- /dev/null +++ b/custom-skills/97-ourdigital-okf/code/scripts/okf_viz.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Generate a minimal, self-contained OKF bundle visualizer (stdlib only). + +Emits one HTML file: a Cytoscape.js force-directed graph of the bundle's +concepts (nodes colored by `type`), with a side panel that renders the +selected concept's markdown body via marked. Both libraries load from a CDN; +the bundle is embedded as JSON, so no backend and no data leaves the page. +""" +from __future__ import annotations + +import argparse +import html +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from okf_common import (concept_id, extract_links, iter_concepts, + parse_frontmatter) + + +def _target_id(bundle, path, target, ids): + t = target.split("#", 1)[0].strip() + if not t or "://" in t or not t.endswith(".md"): + return None + if t.startswith("/"): + cid = t.lstrip("/")[:-3] + else: + rel = (path.parent / t).resolve().relative_to(Path(bundle).resolve()) + cid = rel.as_posix()[:-3] + return cid if cid in ids else None + + +def build_graph(bundle_dir): + bundle = Path(bundle_dir) + concepts = list(iter_concepts(bundle)) + ids = {concept_id(bundle, p) for p in concepts} + nodes, edges = [], [] + for path in concepts: + cid = concept_id(bundle, path) + text = path.read_text(encoding="utf-8") + try: + meta, body = parse_frontmatter(text) + except Exception: + meta, body = None, text + meta = meta or {} + nodes.append({ + "id": cid, + "type": str(meta.get("type", "Concept")), + "title": str(meta.get("title", Path(cid).name)), + "description": str(meta.get("description", "")), + "body": body, + }) + for target in extract_links(body): + tid = _target_id(bundle, path, target, ids) + if tid and tid != cid: + edges.append({"source": cid, "target": tid}) + return {"nodes": nodes, "edges": edges} + + +_HTML = """ + +__NAME__ — OKF Viewer + + + +
__NAME__
+

Click a concept node to view it.

+""" + + +def render_html(graph, name="OKF Bundle"): + data = json.dumps(graph, ensure_ascii=False).replace("hi"}], "edges": []} + html = render_html(g, "X") + self.assertNotIn("hi", html) + + def test_main_writes_file(self): + with tempfile.TemporaryDirectory() as d: + out = Path(d) / "v.html" + self.assertEqual(main(["--bundle", str(FIX), "--out", str(out)]), 0) + self.assertTrue(out.exists() and out.stat().st_size > 500) + + +if __name__ == "__main__": + unittest.main()