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

39 KiB
Raw Blame History

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

cd /Users/ourdigital/Project/our-claude-skills
git checkout -b feat/ourdigital-okf
  • Step 2: Create the directory skeleton
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
# ourdigital-okf scripts use the Python standard library only.
# No third-party runtime dependencies. Tests use stdlib `unittest`.
  • Step 4: Commit
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

# 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
---
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
---
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
---
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
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 testtests/test_okf_common.py

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
"""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
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"

Files:

  • Create: code/scripts/okf_validate.py

  • Test: code/scripts/tests/test_okf_validate.py

  • Step 1: Write the failing testtests/test_okf_validate.py

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
#!/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
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)

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
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 testtests/test_okf_viz.py

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
#!/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
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)
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
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

---
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
# Group Heading

* [Title](relative-or-bundle-relative-path) - short description from the concept's frontmatter
  • Step 3: Create code/assets/log.md
# Update Log

## 2026-06-16
* **Initialization**: Created the bundle structure.
  • Step 4: Commit
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.

---
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 flowpython3 code/scripts/okf_validate.py <bundle> [--json]; explain exit code and that broken links are warnings.
  5. Visualize flowpython3 code/scripts/okf_viz.py --bundle <bundle> [--out viz.html] [--name X].
  6. Resources — list references/, assets/, scripts/.
  • Step 2: Create code/CLAUDE.md
# 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
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)
# 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
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

#!/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:

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
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
# 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
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:
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).