Files
our-claude-skills/custom-skills/97-ourdigital-okf/docs/IMPLEMENTATION-PLAN.md
2026-06-16 20:35:28 +09:00

1059 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ourdigital-okf Skill — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build the `/ourdigital-okf` Claude skill that produces, visualizes, and validates Google Open Knowledge Format (OKF) v0.1 bundles, following OurDigital skill conventions.
**Architecture:** A single OurDigital-family skill. Claude natively drafts conformant OKF concept docs (produce); two zero-dependency Python utilities handle deterministic work — `okf_validate.py` (conformance + broken-link lint) and `okf_viz.py` (minimal Cytoscape graph HTML). A shared `okf_common.py` holds the frontmatter/link parser. Tests use stdlib `unittest` and are verified against Google's three mirrored sample bundles.
**Tech Stack:** Python 3 (stdlib only — no pip deps), `unittest` for tests, Cytoscape.js + marked via CDN for the viewer, OurDigital skill structure (`SKILL.md` + `code/` + `desktop/` + `docs/`).
**Repo:** `/Users/ourdigital/Project/our-claude-skills` (git, branch `main`). Skill dir: `custom-skills/97-ourdigital-okf/`.
**Reference fixtures (already on disk):** `~/Documents/reference-library/open-knowledge-format/okf/bundles/{crypto_bitcoin,ga4,stackoverflow}` and the spec at `.../okf/SPEC.md`.
---
## File Structure
```
custom-skills/97-ourdigital-okf/
├── SKILL.md # top-level canonical (frontmatter + mode dispatch)
├── README.md # overview
├── DESIGN.md # (exists) approved spec
├── install.sh # symlink SKILL.md → ~/.claude/skills/ourdigital-okf
├── code/
│ ├── SKILL.md # Claude Code variant (detailed flows)
│ ├── CLAUDE.md # code-pattern pointer
│ ├── references/
│ │ ├── okf-spec-v0.1.md # distilled actionable spec + conformance checklist
│ │ └── frontmatter-fields.md
│ ├── assets/
│ │ ├── concept.md # template
│ │ ├── index.md # template
│ │ └── log.md # template
│ └── scripts/
│ ├── okf_common.py # shared parser (stdlib)
│ ├── okf_validate.py # validator CLI
│ ├── okf_viz.py # visualizer CLI
│ ├── requirements.txt # (documents: no runtime deps)
│ └── tests/
│ ├── test_okf_common.py
│ ├── test_okf_validate.py
│ ├── test_okf_viz.py
│ └── fixtures/mini_bundle/ # tiny conformant bundle (from SPEC Appendix A)
├── desktop/
│ ├── SKILL.md # Claude Desktop variant (leaner)
│ └── skill.yaml
└── docs/
├── CHANGELOG.md
└── IMPLEMENTATION-PLAN.md # this file
```
Responsibilities: `okf_common.py` = parsing only; `okf_validate.py` = conformance rules; `okf_viz.py` = graph build + HTML render. Each file is independently testable.
---
## Task 1: Branch + directory skeleton
**Files:**
- Create branch `feat/ourdigital-okf`
- Create: `code/scripts/`, `code/references/`, `code/assets/`, `code/scripts/tests/fixtures/mini_bundle/`, `desktop/`, `docs/`
- [ ] **Step 1: Create a feature branch**
```bash
cd /Users/ourdigital/Project/our-claude-skills
git checkout -b feat/ourdigital-okf
```
- [ ] **Step 2: Create the directory skeleton**
```bash
cd custom-skills/97-ourdigital-okf
mkdir -p code/references code/assets code/scripts/tests/fixtures/mini_bundle desktop docs
```
- [ ] **Step 3: Create `code/scripts/requirements.txt`**
```text
# ourdigital-okf scripts use the Python standard library only.
# No third-party runtime dependencies. Tests use stdlib `unittest`.
```
- [ ] **Step 4: Commit**
```bash
git add custom-skills/97-ourdigital-okf
git commit -m "feat(okf): scaffold ourdigital-okf skill skeleton"
```
---
## Task 2: Mini fixture bundle (test data, from SPEC Appendix A)
**Files:**
- Create: `code/scripts/tests/fixtures/mini_bundle/{index.md, datasets/sales.md, tables/orders.md, tables/customers.md}`
- [ ] **Step 1: Create `tests/fixtures/mini_bundle/index.md`**
```markdown
# Datasets
* [Sales](datasets/sales.md) - All sales-related tables.
# Tables
* [Orders](tables/orders.md) - One row per completed order.
* [Customers](tables/customers.md) - One row per customer.
```
- [ ] **Step 2: Create `tests/fixtures/mini_bundle/datasets/sales.md`**
```markdown
---
type: BigQuery Dataset
title: Sales
description: All sales-related tables for the retail business.
resource: https://console.cloud.google.com/bigquery?p=acme&d=sales
tags: [sales]
timestamp: 2026-05-28T00:00:00Z
---
The sales dataset contains [orders](/tables/orders.md) and [customers](/tables/customers.md).
```
- [ ] **Step 3: Create `tests/fixtures/mini_bundle/tables/orders.md`**
```markdown
---
type: BigQuery Table
title: Orders
description: One row per completed customer order.
resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=orders
tags: [sales, orders]
timestamp: 2026-05-28T00:00:00Z
---
# Schema
| Column | Type | Description |
|---------------|---------|------------------------------------------|
| `order_id` | STRING | Unique order identifier. |
| `customer_id` | STRING | FK to [customers](/tables/customers.md). |
Part of the [sales dataset](/datasets/sales.md).
```
- [ ] **Step 4: Create `tests/fixtures/mini_bundle/tables/customers.md`**
```markdown
---
type: BigQuery Table
title: Customers
description: One row per customer.
resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=customers
tags: [sales, customers]
timestamp: 2026-05-28T00:00:00Z
---
# Schema
| Column | Type | Description |
|---------------|--------|-----------------------|
| `customer_id` | STRING | Unique customer id. |
Referenced by [orders](/tables/orders.md).
```
- [ ] **Step 5: Commit**
```bash
git add custom-skills/97-ourdigital-okf/code/scripts/tests/fixtures
git commit -m "test(okf): add mini conformant fixture bundle"
```
---
## Task 3: `okf_common.py` — shared parser (TDD)
**Files:**
- Create: `code/scripts/okf_common.py`
- Test: `code/scripts/tests/test_okf_common.py`
- [ ] **Step 1: Write the failing test**`tests/test_okf_common.py`
```python
import sys, unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from okf_common import (parse_frontmatter, parse_yaml_subset,
extract_links, concept_id, FrontmatterError)
class TestFrontmatter(unittest.TestCase):
def test_parse_basic(self):
text = "---\ntype: BigQuery Table\ntitle: Orders\ntags: [sales, revenue]\n---\n\n# Body\n"
meta, body = parse_frontmatter(text)
self.assertEqual(meta["type"], "BigQuery Table")
self.assertEqual(meta["title"], "Orders")
self.assertEqual(meta["tags"], ["sales", "revenue"])
self.assertIn("# Body", body)
def test_url_value_with_colons(self):
meta, _ = parse_frontmatter("---\ntype: X\nresource: https://e.com/a?b=c\n---\nbody")
self.assertEqual(meta["resource"], "https://e.com/a?b=c")
def test_quoted_value(self):
self.assertEqual(parse_yaml_subset('okf_version: "0.1"')["okf_version"], "0.1")
def test_empty_list(self):
self.assertEqual(parse_yaml_subset("tags: []")["tags"], [])
def test_no_frontmatter(self):
meta, body = parse_frontmatter("# Just markdown\n")
self.assertIsNone(meta)
self.assertEqual(body, "# Just markdown\n")
def test_unclosed_raises(self):
with self.assertRaises(FrontmatterError):
parse_frontmatter("---\ntype: X\n\nbody without close")
class TestLinksAndIds(unittest.TestCase):
def test_extract_links(self):
body = "See [a](/tables/a.md) and [b](./b.md) and [ext](https://x.com)."
self.assertEqual(extract_links(body), ["/tables/a.md", "./b.md", "https://x.com"])
def test_concept_id(self):
self.assertEqual(concept_id("/bundle", "/bundle/tables/users.md"), "tables/users")
if __name__ == "__main__":
unittest.main()
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd custom-skills/97-ourdigital-okf/code/scripts && python3 -m unittest tests.test_okf_common -v`
Expected: FAIL with `ModuleNotFoundError: No module named 'okf_common'`
- [ ] **Step 3: Write `code/scripts/okf_common.py`**
```python
"""Shared OKF v0.1 parsing utilities (Python standard library only)."""
from __future__ import annotations
import re
from pathlib import Path
RESERVED = {"index.md", "log.md"}
_LINK_RE = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
class FrontmatterError(ValueError):
"""Raised when a frontmatter block is present but cannot be parsed."""
def split_frontmatter(text):
"""Return (raw_frontmatter, body). raw is None if there is no leading '---' block."""
if not text.startswith("---"):
return None, text
lines = text.splitlines()
if lines[0].strip() != "---":
return None, text
for i in range(1, len(lines)):
if lines[i].strip() == "---":
return "\n".join(lines[1:i]), "\n".join(lines[i + 1:])
raise FrontmatterError("opening '---' without a closing '---'")
def _scalar(value):
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
return value[1:-1]
return value
def parse_yaml_subset(raw):
"""Parse the small YAML subset OKF uses: `key: value` and `key: [a, b]`."""
meta = {}
for line in raw.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if ":" not in line:
raise FrontmatterError("unparseable frontmatter line: %r" % line)
key, _, value = line.partition(":")
key, value = key.strip(), value.strip()
if value.startswith("[") and value.endswith("]"):
inner = value[1:-1].strip()
meta[key] = [s for s in (_scalar(x) for x in inner.split(",")) if s] if inner else []
else:
meta[key] = _scalar(value)
return meta
def parse_frontmatter(text):
"""Return (meta, body). meta is None when no frontmatter block is present."""
raw, body = split_frontmatter(text)
if raw is None:
return None, text
return parse_yaml_subset(raw), body
def iter_concepts(bundle_dir):
"""Yield Path for every non-reserved .md file under bundle_dir, sorted."""
root = Path(bundle_dir)
for path in sorted(root.rglob("*.md")):
if path.name not in RESERVED:
yield path
def concept_id(bundle_dir, path):
"""Concept ID = bundle-relative path with the .md suffix removed."""
rel = Path(path).relative_to(Path(bundle_dir)).as_posix()
return rel[:-3] if rel.endswith(".md") else rel
def extract_links(body):
"""Return the list of link targets from markdown links in body."""
return _LINK_RE.findall(body)
```
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 -m unittest tests.test_okf_common -v`
Expected: PASS (8 tests OK)
- [ ] **Step 5: Commit**
```bash
git add custom-skills/97-ourdigital-okf/code/scripts/okf_common.py custom-skills/97-ourdigital-okf/code/scripts/tests/test_okf_common.py
git commit -m "feat(okf): add okf_common frontmatter/link parser with tests"
```
---
## Task 4: `okf_validate.py` — conformance + broken-link linter (TDD)
**Files:**
- Create: `code/scripts/okf_validate.py`
- Test: `code/scripts/tests/test_okf_validate.py`
- [ ] **Step 1: Write the failing test**`tests/test_okf_validate.py`
```python
import sys, tempfile, unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from okf_validate import validate_bundle
FIX = Path(__file__).resolve().parent / "fixtures" / "mini_bundle"
def write_bundle(root, files):
for rel, content in files.items():
p = Path(root) / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content, encoding="utf-8")
class TestValidate(unittest.TestCase):
def test_mini_fixture_conformant(self):
report = validate_bundle(FIX)
self.assertTrue(report["conformant"], report["errors"])
self.assertGreaterEqual(report["concepts"], 3)
def test_missing_type_is_error(self):
with tempfile.TemporaryDirectory() as d:
write_bundle(d, {"tables/x.md": "---\ntitle: X\n---\nbody"})
report = validate_bundle(d)
self.assertFalse(report["conformant"])
self.assertTrue(any(e["rule"] == "type" for e in report["errors"]))
def test_missing_frontmatter_is_error(self):
with tempfile.TemporaryDirectory() as d:
write_bundle(d, {"tables/x.md": "# no frontmatter\n"})
self.assertFalse(validate_bundle(d)["conformant"])
def test_reserved_files_not_required_to_have_type(self):
with tempfile.TemporaryDirectory() as d:
write_bundle(d, {"index.md": "# Index\n* [x](/tables/x.md)\n",
"tables/x.md": "---\ntype: T\n---\nb"})
self.assertTrue(validate_bundle(d)["conformant"])
def test_broken_link_is_warning_not_error(self):
with tempfile.TemporaryDirectory() as d:
write_bundle(d, {"tables/x.md": "---\ntype: T\n---\nSee [y](/tables/y.md)."})
report = validate_bundle(d)
self.assertTrue(report["conformant"])
self.assertTrue(any(w["rule"] == "broken_link" for w in report["warnings"]))
if __name__ == "__main__":
unittest.main()
```
- [ ] **Step 2: Run test to verify it fails**
Run: `python3 -m unittest tests.test_okf_validate -v`
Expected: FAIL with `ModuleNotFoundError: No module named 'okf_validate'`
- [ ] **Step 3: Write `code/scripts/okf_validate.py`**
```python
#!/usr/bin/env python3
"""OKF v0.1 conformance + broken-link validator (Python standard library only).
Conformance (SPEC.md §9): every non-reserved .md has a parseable frontmatter
block with a non-empty `type`. Broken cross-links are reported as warnings,
never errors (consumers MUST tolerate them).
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from okf_common import (FrontmatterError, concept_id, extract_links,
iter_concepts, parse_frontmatter)
def _resolve_link(bundle, path, target):
"""Return a Path for an internal .md link, or None for external/anchor/non-md."""
t = target.split("#", 1)[0].strip()
if not t or "://" in t or t.startswith("mailto:") or not t.endswith(".md"):
return None
if t.startswith("/"):
return bundle / t.lstrip("/")
return path.parent / t
def validate_bundle(bundle_dir):
bundle = Path(bundle_dir)
errors, warnings = [], []
concept_files = list(iter_concepts(bundle))
existing = {p.resolve() for p in bundle.rglob("*.md")}
for path in concept_files:
cid = concept_id(bundle, path)
text = path.read_text(encoding="utf-8")
try:
meta, body = parse_frontmatter(text)
except FrontmatterError as exc:
errors.append({"concept": cid, "rule": "frontmatter", "message": str(exc)})
continue
if meta is None:
errors.append({"concept": cid, "rule": "frontmatter",
"message": "missing YAML frontmatter block"})
continue
if not str(meta.get("type", "")).strip():
errors.append({"concept": cid, "rule": "type",
"message": "missing or empty required 'type' field"})
for target in extract_links(body):
resolved = _resolve_link(bundle, path, target)
if resolved is not None and resolved.resolve() not in existing:
warnings.append({"concept": cid, "rule": "broken_link",
"message": "link target not found: %s" % target})
return {
"bundle": str(bundle),
"concepts": len(concept_files),
"conformant": len(errors) == 0,
"errors": errors,
"warnings": warnings,
}
def format_report(report):
status = "CONFORMANT" if report["conformant"] else "NON-CONFORMANT"
lines = [
"OKF v0.1 validation: %s" % report["bundle"],
" concepts: %d status: %s" % (report["concepts"], status),
" errors: %d warnings: %d" % (len(report["errors"]), len(report["warnings"])),
]
for e in report["errors"]:
lines.append(" ERROR [%s] %s: %s" % (e["concept"], e["rule"], e["message"]))
for w in report["warnings"]:
lines.append(" WARN [%s] %s: %s" % (w["concept"], w["rule"], w["message"]))
return "\n".join(lines)
def main(argv=None):
ap = argparse.ArgumentParser(description="Validate an OKF v0.1 bundle.")
ap.add_argument("bundle", help="Path to the bundle directory")
ap.add_argument("--json", action="store_true", help="Emit a JSON report")
args = ap.parse_args(argv)
report = validate_bundle(args.bundle)
print(json.dumps(report, indent=2) if args.json else format_report(report))
return 0 if report["conformant"] else 1
if __name__ == "__main__":
sys.exit(main())
```
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 -m unittest tests.test_okf_validate -v`
Expected: PASS (5 tests OK)
- [ ] **Step 5: Commit**
```bash
git add custom-skills/97-ourdigital-okf/code/scripts/okf_validate.py custom-skills/97-ourdigital-okf/code/scripts/tests/test_okf_validate.py
git commit -m "feat(okf): add okf_validate conformance linter with tests"
```
---
## Task 5: Validate against Google's 3 reference bundles (integration + parser hardening)
**Files:**
- Modify (if needed): `code/scripts/okf_common.py`
- Test: append to `code/scripts/tests/test_okf_validate.py`
- [ ] **Step 1: Add the integration test** — append to `tests/test_okf_validate.py` (before the `if __name__` block)
```python
GOOGLE = Path.home() / "Documents/reference-library/open-knowledge-format/okf/bundles"
class TestGoogleBundles(unittest.TestCase):
@unittest.skipUnless(GOOGLE.exists(), "Google reference bundles not present")
def test_google_sample_bundles_conformant(self):
for name in ("crypto_bitcoin", "ga4", "stackoverflow"):
bundle = GOOGLE / name
if not bundle.exists():
continue
report = validate_bundle(bundle)
self.assertTrue(
report["conformant"],
"%s non-conformant; first errors: %s" % (name, report["errors"][:3]),
)
```
- [ ] **Step 2: Run it**
Run: `python3 -m unittest tests.test_okf_validate.TestGoogleBundles -v`
Expected: PASS. **If it FAILS:** inspect the reported errors. If they are real conformance gaps in Google's bundles, that's a finding — record it in `docs/CHANGELOG.md` and relax that specific check to a warning. If they are *parser limitations* (e.g. a frontmatter value shape `parse_yaml_subset` doesn't handle), extend the parser in `okf_common.py` minimally and re-run Task 3 tests to confirm no regressions.
- [ ] **Step 3: Commit**
```bash
git add custom-skills/97-ourdigital-okf/code/scripts
git commit -m "test(okf): validate against Google reference bundles"
```
---
## Task 6: `okf_viz.py` — minimal graph visualizer (TDD)
**Files:**
- Create: `code/scripts/okf_viz.py`
- Test: `code/scripts/tests/test_okf_viz.py`
- [ ] **Step 1: Write the failing test**`tests/test_okf_viz.py`
```python
import sys, tempfile, unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from okf_viz import build_graph, render_html, main
FIX = Path(__file__).resolve().parent / "fixtures" / "mini_bundle"
class TestViz(unittest.TestCase):
def test_build_graph_nodes_and_edges(self):
g = build_graph(FIX)
ids = {n["id"] for n in g["nodes"]}
self.assertIn("tables/orders", ids)
self.assertTrue(any(e["source"] == "tables/orders" and e["target"] == "tables/customers"
for e in g["edges"]))
def test_render_html_embeds_data_and_cdn(self):
html = render_html(build_graph(FIX), "Mini")
self.assertIn("cytoscape", html)
self.assertIn('"nodes"', html)
def test_render_html_escapes_script_close(self):
g = {"nodes": [{"id": "x", "type": "T", "title": "x",
"description": "", "body": "</script><b>hi</b>"}], "edges": []}
html = render_html(g, "X")
self.assertNotIn("</script><b>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()
```
- [ ] **Step 2: Run test to verify it fails**
Run: `python3 -m unittest tests.test_okf_viz -v`
Expected: FAIL with `ModuleNotFoundError: No module named 'okf_viz'`
- [ ] **Step 3: Write `code/scripts/okf_viz.py`**
```python
#!/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)
try:
meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
except Exception:
meta, body = None, path.read_text(encoding="utf-8")
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 = """<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8">
<title>__NAME__ — OKF Viewer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.30.2/cytoscape.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
body{margin:0;font-family:system-ui,sans-serif;display:flex;height:100vh}
#cy{flex:1;background:#fafafa}
#panel{width:380px;overflow:auto;padding:16px;border-left:1px solid #ddd;box-sizing:border-box}
#panel h2{margin:.2em 0;font-size:16px} table{border-collapse:collapse} td,th{border:1px solid #ddd;padding:2px 6px}
.type{display:inline-block;font-size:11px;background:#eef;color:#225;padding:2px 6px;border-radius:4px}
header{position:absolute;top:8px;left:12px;font-weight:600;color:#333}
</style></head><body>
<header>__NAME__</header><div id="cy"></div>
<div id="panel"><p>Click a concept node to view it.</p></div>
<script>
const DATA = __DATA__;
const colors=["#4e79a7","#f28e2b","#e15759","#76b7b2","#59a14f","#edc948","#b07aa1","#ff9da7","#9c755f","#bab0ac"];
const types=[...new Set(DATA.nodes.map(n=>n.type))];
const colorOf={}; types.forEach((t,i)=>colorOf[t]=colors[i%colors.length]);
const byId={}; DATA.nodes.forEach(n=>byId[n.id]=n);
const elements=[];
DATA.nodes.forEach(n=>elements.push({data:{id:n.id,label:n.title,color:colorOf[n.type]}}));
DATA.edges.forEach(e=>elements.push({data:{source:e.source,target:e.target}}));
const cy=cytoscape({container:document.getElementById('cy'),elements,
style:[{selector:'node',style:{'label':'data(label)','font-size':'9px','background-color':'data(color)','width':14,'height':14,'color':'#333'}},
{selector:'edge',style:{'width':1,'line-color':'#bbb','target-arrow-color':'#bbb','target-arrow-shape':'triangle','curve-style':'bezier','arrow-scale':0.7}}],
layout:{name:'cose',animate:false}});
cy.on('tap','node',evt=>{const n=byId[evt.target.id()];
document.getElementById('panel').innerHTML='<span class="type">'+n.type+'</span><h2>'+n.title+'</h2>'+
(n.description?'<p><em>'+n.description+'</em></p>':'')+'<hr>'+marked.parse(n.body||'');});
</script></body></html>"""
def render_html(graph, name="OKF Bundle"):
data = json.dumps(graph, ensure_ascii=False).replace("</", "<\\/")
return _HTML.replace("__NAME__", html.escape(name)).replace("__DATA__", data)
def main(argv=None):
ap = argparse.ArgumentParser(description="Visualize an OKF bundle as self-contained HTML.")
ap.add_argument("--bundle", required=True)
ap.add_argument("--out")
ap.add_argument("--name")
args = ap.parse_args(argv)
bundle = Path(args.bundle)
graph = build_graph(bundle)
out = Path(args.out) if args.out else bundle / "viz.html"
out.write_text(render_html(graph, args.name or bundle.name), encoding="utf-8")
print("Wrote %s (%d nodes, %d edges)" % (out, len(graph["nodes"]), len(graph["edges"])))
return 0
if __name__ == "__main__":
sys.exit(main())
```
- [ ] **Step 4: Run test to verify it passes**
Run: `python3 -m unittest tests.test_okf_viz -v`
Expected: PASS (4 tests OK)
- [ ] **Step 5: Smoke-test against a real Google bundle**
Run: `python3 okf_viz.py --bundle ~/Documents/reference-library/open-knowledge-format/okf/bundles/crypto_bitcoin --out /tmp/okf_btc_viz.html && echo OK`
Expected: prints `Wrote /tmp/okf_btc_viz.html (N nodes, M edges)` with N>0. Open in a browser to confirm a graph renders. (This is a manual visual check.)
- [ ] **Step 6: Commit**
```bash
git add custom-skills/97-ourdigital-okf/code/scripts/okf_viz.py custom-skills/97-ourdigital-okf/code/scripts/tests/test_okf_viz.py
git commit -m "feat(okf): add minimal okf_viz Cytoscape generator with tests"
```
---
## Task 7: Run the full test suite
- [ ] **Step 1: Run every test**
Run: `cd custom-skills/97-ourdigital-okf/code/scripts && python3 -m unittest discover -s tests -v`
Expected: all tests PASS (Google-bundle test passes or skips if fixtures absent).
- [ ] **Step 2: Commit any fixes** (only if changes were needed)
```bash
git add -A custom-skills/97-ourdigital-okf
git commit -m "test(okf): green full suite"
```
---
## Task 8: Reference docs (`code/references/`)
**Files:**
- Create: `code/references/okf-spec-v0.1.md`, `code/references/frontmatter-fields.md`
- [ ] **Step 1: Create `code/references/okf-spec-v0.1.md`**
Write a distilled, *actionable* version of OKF v0.1 (source: `~/Documents/reference-library/open-knowledge-format/okf/SPEC.md`). It MUST contain these sections with this content:
- **Bundle/Concept/Concept-ID definitions** (concept ID = path minus `.md`).
- **Reserved filenames table:** `index.md` (directory listing / progressive disclosure, no frontmatter except optional root `okf_version`), `log.md` (date-grouped `YYYY-MM-DD`, newest-first).
- **Frontmatter table:** `type` REQUIRED; `title`/`description`/`resource`/`tags`/`timestamp` recommended; arbitrary extra keys allowed.
- **Conventional body headings:** `# Schema`, `# Examples`, `# Citations`.
- **Cross-linking:** bundle-relative (`/path/concept.md`, leading `/`) recommended; relative (`./x.md`) allowed; broken links tolerated.
- **Conformance checklist (§9):** parseable frontmatter + non-empty `type` per concept; never reject for missing optional fields / unknown types / broken links / missing index.
- **Authoring rules for the producer** (numbered): one concept per file; pick descriptive `type` values; prefer structural markdown; cross-link related concepts; add `# Citations` when sourcing external claims; generate `index.md` per directory.
(The "OKF v0.1 at a glance" cheat-sheet in `~/Documents/reference-library/open-knowledge-format/README.md` is a ready source for the tables — adapt it.)
- [ ] **Step 2: Create `code/references/frontmatter-fields.md`**
A one-page field reference: for each of `type, title, description, resource, tags, timestamp` give purpose, requiredness, example value, and example `type` values (`BigQuery Table`, `BigQuery Dataset`, `Metric`, `Playbook`, `Reference`, `API Endpoint`).
- [ ] **Step 3: Commit**
```bash
git add custom-skills/97-ourdigital-okf/code/references
git commit -m "docs(okf): add distilled spec + frontmatter reference"
```
---
## Task 9: Templates (`code/assets/`)
**Files:**
- Create: `code/assets/concept.md`, `code/assets/index.md`, `code/assets/log.md`
- [ ] **Step 1: Create `code/assets/concept.md`**
```markdown
---
type: <Concept type, e.g. BigQuery Table>
title: <Human-readable display name>
description: <One-sentence summary>
resource: <Canonical URI, omit for abstract concepts>
tags: [<tag>, <tag>]
timestamp: <ISO 8601, e.g. 2026-06-16T00:00:00Z>
---
# Schema
| Column | Type | Description |
|--------|------|-------------|
| `col` | TYPE | What it is. FK to [other](/tables/other.md). |
# Citations
[1] [Source title](https://example.com)
```
- [ ] **Step 2: Create `code/assets/index.md`**
```markdown
# Group Heading
* [Title](relative-or-bundle-relative-path) - short description from the concept's frontmatter
```
- [ ] **Step 3: Create `code/assets/log.md`**
```markdown
# Update Log
## 2026-06-16
* **Initialization**: Created the bundle structure.
```
- [ ] **Step 4: Commit**
```bash
git add custom-skills/97-ourdigital-okf/code/assets
git commit -m "docs(okf): add concept/index/log templates"
```
---
## Task 10: `code/SKILL.md` + `code/CLAUDE.md` (Claude Code variant)
**Files:**
- Create: `code/SKILL.md`, `code/CLAUDE.md`
- [ ] **Step 1: Create `code/SKILL.md`** with this frontmatter and a body (target 8001,200 words) covering the sections below.
```yaml
---
name: ourdigital-okf
description: |
Produce, visualize, and validate Google Open Knowledge Format (OKF) v0.1
knowledge bundles. Activated with the "ourdigital" keyword for OKF work.
Triggers (ourdigital or our prefix):
- "ourdigital okf", "our okf"
- "ourdigital open knowledge format", "our knowledge bundle"
- "ourdigital okf 만들기", "our okf 검증"
Features:
- Produce conformant OKF bundles from a pasted/exported schema, a docs folder, or a research topic
- Validate a bundle for OKF v0.1 conformance + broken-link report
- Visualize a bundle as a self-contained interactive graph (viz.html)
version: "1.0"
author: OurDigital
environment: Both
---
```
Body sections (write actual instructions, not placeholders):
1. **What is OKF** (34 sentences; point to `references/okf-spec-v0.1.md` as the authority — instruct: read it before producing).
2. **Mode dispatch** — decide produce / visualize / validate from the request.
3. **Produce flow** (the 6 steps from DESIGN.md §5). Emphasize: (a) **confirm the target bundle directory with the user before creating anything** (no-directory-without-consent rule); (b) input adapters: *schema* = ingest a pasted/exported schema (BigQuery DDL, GA4 export schema, CSV/JSON Schema, OpenAPI) — do **not** call a live MCP; *docs* = read the provided files/folder; *research* = invoke `/reference-curator`; (c) every concept gets a required `type`; (d) cross-link with bundle-relative paths; (e) generate `index.md` per directory; (f) run `python3 code/scripts/okf_validate.py <bundle>` and fix all errors before reporting done.
4. **Validate flow**`python3 code/scripts/okf_validate.py <bundle> [--json]`; explain exit code and that broken links are warnings.
5. **Visualize flow**`python3 code/scripts/okf_viz.py --bundle <bundle> [--out viz.html] [--name X]`.
6. **Resources** — list `references/`, `assets/`, `scripts/`.
- [ ] **Step 2: Create `code/CLAUDE.md`**
```markdown
# ourdigital-okf (Claude Code)
Use `SKILL.md` in this directory as the instruction set. Scripts live in `scripts/`
(stdlib-only Python). Read `references/okf-spec-v0.1.md` before producing a bundle.
Always confirm the output directory with the user before creating it.
```
- [ ] **Step 3: Verify word count**
Run: `wc -w custom-skills/97-ourdigital-okf/code/SKILL.md`
Expected: body in the 8001,200 word range (OurDigital convention).
- [ ] **Step 4: Commit**
```bash
git add custom-skills/97-ourdigital-okf/code/SKILL.md custom-skills/97-ourdigital-okf/code/CLAUDE.md
git commit -m "feat(okf): add code-variant SKILL.md and CLAUDE.md"
```
---
## Task 11: Top-level `SKILL.md`, `README.md`, and `desktop/` variant
**Files:**
- Create: `SKILL.md` (top-level), `README.md`, `desktop/SKILL.md`, `desktop/skill.yaml`
- [ ] **Step 1: Create top-level `SKILL.md`**
Same frontmatter as `code/SKILL.md` (Task 10 Step 1). Body: a concise canonical version (300500 words) summarizing the three modes and pointing to `code/SKILL.md` for the detailed Claude Code flows and `code/references/` for the spec.
- [ ] **Step 2: Create `README.md`**
Overview: what the skill does, the three modes, install instructions (`./install.sh`), the script commands, and a note that bundles are validated against Google's reference bundles. Link to `DESIGN.md`.
- [ ] **Step 3: Create `desktop/SKILL.md`**
Leaner Claude Desktop variant: same frontmatter with `environment: Desktop`; body explains produce + validate + visualize conceptually and notes the Python scripts run in the Code environment (Desktop users run them via a terminal).
- [ ] **Step 4: Create `desktop/skill.yaml`** (mirror the pattern in `04-ourdigital-research/desktop/skill.yaml`)
```yaml
# Skill metadata (extracted from SKILL.md frontmatter)
name: ourdigital-okf
description: |
Produce, visualize, and validate Google Open Knowledge Format (OKF) v0.1 bundles.
Triggers: "ourdigital okf", "our okf", "open knowledge format", "knowledge bundle".
```
- [ ] **Step 5: Commit**
```bash
git add custom-skills/97-ourdigital-okf/SKILL.md custom-skills/97-ourdigital-okf/README.md custom-skills/97-ourdigital-okf/desktop
git commit -m "feat(okf): add top-level + desktop skill variants and README"
```
---
## Task 12: `install.sh` + smoke test
**Files:**
- Create: `install.sh`
- [ ] **Step 1: Create `install.sh`**
```bash
#!/bin/bash
# ourdigital-okf installer — symlinks the skill into ~/.claude/skills/ourdigital-okf
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILLS_DIR="$HOME/.claude/skills"
TARGET="$SKILLS_DIR/ourdigital-okf"
mkdir -p "$SKILLS_DIR"
if [ -e "$TARGET" ] || [ -L "$TARGET" ]; then
echo "Removing existing $TARGET"
rm -rf "$TARGET"
fi
ln -s "$SCRIPT_DIR" "$TARGET"
echo "Linked $TARGET -> $SCRIPT_DIR"
echo "Verifying scripts (stdlib-only)…"
python3 "$SCRIPT_DIR/code/scripts/okf_validate.py" \
"$SCRIPT_DIR/code/scripts/tests/fixtures/mini_bundle" >/dev/null && echo "validate OK"
echo "Done. Invoke with /ourdigital-okf"
```
- [ ] **Step 2: Make it executable and run it**
Run:
```bash
chmod +x custom-skills/97-ourdigital-okf/install.sh
custom-skills/97-ourdigital-okf/install.sh
```
Expected: prints `Linked …`, `validate OK`, `Done.` and exit 0.
- [ ] **Step 3: Verify the symlink and that the skill resolves**
Run: `ls -l ~/.claude/skills/ourdigital-okf && test -f ~/.claude/skills/ourdigital-okf/SKILL.md && echo PRESENT`
Expected: symlink shown, `PRESENT`.
- [ ] **Step 4: Commit**
```bash
git add custom-skills/97-ourdigital-okf/install.sh
git commit -m "feat(okf): add install.sh symlink installer"
```
---
## Task 13: End-to-end produce test (manual, real flow)
- [ ] **Step 1: Produce a tiny bundle from a docs folder**
Create a scratch input and ask the skill (or follow the produce flow manually) to emit a 23 concept bundle into a **user-confirmed** temp dir, e.g. `/tmp/okf-e2e/`. Each concept must have a `type`.
- [ ] **Step 2: Validate it**
Run: `python3 custom-skills/97-ourdigital-okf/code/scripts/okf_validate.py /tmp/okf-e2e`
Expected: `status: CONFORMANT`, `errors: 0`.
- [ ] **Step 3: Visualize it**
Run: `python3 custom-skills/97-ourdigital-okf/code/scripts/okf_viz.py --bundle /tmp/okf-e2e`
Expected: writes `/tmp/okf-e2e/viz.html`; opens to a graph in a browser.
- [ ] **Step 4: Record the result in `docs/CHANGELOG.md`**
```markdown
# Changelog
## 1.0 — 2026-06-16
* Initial release: produce / validate / visualize OKF v0.1 bundles.
* Validated against Google reference bundles (crypto_bitcoin, ga4, stackoverflow).
```
- [ ] **Step 5: Commit**
```bash
git add custom-skills/97-ourdigital-okf/docs/CHANGELOG.md
git commit -m "docs(okf): add changelog; e2e produce→validate→visualize verified"
```
---
## Task 14: OurDigital consistency check + finalize
- [ ] **Step 1: Run the OurDigital skill-creator validation**
Invoke `ourdigital-skill-creator` in "validate existing skill" mode against `97-ourdigital-okf`. It checks: frontmatter has `ourdigital` triggers; description states activation; body 8001,200 words; `shared/` (here `code/references` + `code/assets`) referenced; no overlap with existing skills; numbering/structure conventions.
- [ ] **Step 2: Fix any reported violations**, re-running the relevant tests/word-count checks. Commit fixes:
```bash
git add -A custom-skills/97-ourdigital-okf
git commit -m "fix(okf): address ourdigital-skill-creator consistency findings"
```
- [ ] **Step 3: Update the Notion spec page** (`381581e5-8a1e-8112-8280-f43839902dc8`) Status → Done.
- [ ] **Step 4: Final full-suite run**
Run: `cd custom-skills/97-ourdigital-okf/code/scripts && python3 -m unittest discover -s tests -v`
Expected: all green.
- [ ] **Step 5: Offer to merge `feat/ourdigital-okf`** (use superpowers:finishing-a-development-branch).
---
## Self-Review (completed by plan author)
**Spec coverage:** produce (Tasks 810 author flow; validator self-check in Task 4) · visualize (Task 6) · validate (Tasks 45) · pasted/exported schema adapter (Task 10 §3b) · MCP-agnostic (no live MCP calls in scripts/flows) · minimal viz first (Task 6) · OurDigital structure (Tasks 1011) · DESIGN.md repo copy (exists) · ourdigital-skill-creator check (Task 14) · Google-bundle verification (Tasks 5, 6) · no-directory-without-consent (Task 10 §3a, Task 13). All spec sections map to a task.
**Placeholder scan:** No "TBD/TODO". Content tasks (811) specify exact sections + content to write, not vague "fill in". Code tasks carry complete implementations.
**Type consistency:** `parse_frontmatter`, `parse_yaml_subset`, `split_frontmatter`, `iter_concepts`, `concept_id`, `extract_links`, `FrontmatterError`, `RESERVED` defined in Task 3 and imported consistently in Tasks 4 & 6. `validate_bundle` report keys (`bundle/concepts/conformant/errors/warnings`) used consistently. `build_graph``{nodes,edges}` and `render_html(graph,name)` consistent across Task 6 code and tests.
**Known risk:** Task 5 may surface frontmatter shapes in Google's real bundles that the minimal parser doesn't handle; Task 5 Step 2 gives the explicit remediation (extend parser or downgrade to warning).