From da307c6d8a808708d61a23561405aca68a0061f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Furga=C5=82a?= <83299832+00200200@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:31:54 +0200 Subject: [PATCH] feat(wasm): add WebAssembly/Pyodide compilation engine and interactive playground (closes #29) --- docs/index.html | 519 +++++++++++++++++++++++++++++++++++++++++++++ src/cinch/wasm.py | 151 +++++++++++++ tests/test_wasm.py | 121 +++++++++++ 3 files changed, 791 insertions(+) create mode 100644 docs/index.html create mode 100644 src/cinch/wasm.py create mode 100644 tests/test_wasm.py diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..50ed6ba --- /dev/null +++ b/docs/index.html @@ -0,0 +1,519 @@ + + + + + + Cinch Playground — Interactive Cross-Harness Skill Compiler + + + + +
+
+

Cinch Playground

+ Universal skill compiler for 12 AI harnesses +
+
+ + + ⚡ < 2ms + + + +
+
+ +
+ +
+
+ Cinch Canonical Markdown (SKILL.md) + +
+
+ +
+
+
+ + +
+
+
+ +
+
+ .claude/skills/code-review/SKILL.md + +
+
+
+

+      
+
+
+ + + + diff --git a/src/cinch/wasm.py b/src/cinch/wasm.py new file mode 100644 index 0000000..ff031ee --- /dev/null +++ b/src/cinch/wasm.py @@ -0,0 +1,151 @@ +"""WebAssembly and Pyodide compilation engine for in-browser Cinch playground. + +Provides high-speed, zero-dependency translation of Cinch skills into all +supported AI harness dialects (Claude, Cursor, Codex, Copilot, Gemini, Windsurf, +Cline, OpenCode, Aider, Zed, Continue, Grok). +""" + +from __future__ import annotations + +import json +import time +from typing import Any + +from cinch.adapters import ADAPTERS, get_adapter +from cinch.doc import Doc, parse_frontmatter +from cinch.lsp import parse_diagnostics + + +def doc_from_markdown( + content: str, + name: str | None = None, + kind: str = "skill", +) -> Doc: + """Parse markdown skill content into a canonical Doc object.""" + meta, body = parse_frontmatter(content) + doc_name = meta.get("name") or name or "sample-skill" + description = meta.get("description", "") + + if not description: + for line in body.splitlines(): + line_str = line.strip() + if line_str and not line_str.startswith("#"): + description = line_str + break + if not description: + description = f"{doc_name} {kind}" + + paths_val = meta.get("paths") or meta.get("globs") or meta.get("applyTo") or () + if isinstance(paths_val, str): + paths: tuple[str, ...] = (paths_val,) + else: + paths = tuple(paths_val) + + requires_val = meta.get("requires") or () + if isinstance(requires_val, str): + requires: tuple[str, ...] = (requires_val.strip(),) if requires_val.strip() else () + elif isinstance(requires_val, (list, tuple)): + requires = tuple( + item.strip() for item in requires_val if isinstance(item, str) and item.strip() + ) + else: + requires = () + + clean_body = body.strip() + lines = clean_body.splitlines() + if lines and lines[0].startswith("# "): + h1 = lines[0][2:].strip().lower() + if h1 in (doc_name.lower(), doc_name.lower().replace("-", " "), "skill"): + clean_body = "\n".join(lines[1:]).strip() + + if not isinstance(description, str): + description = str(description) + if not isinstance(doc_name, str): + doc_name = str(doc_name) + + return Doc( + kind=kind, + name=doc_name, + description=description, + body=clean_body, + paths=paths, + requires=requires, + extra_meta=meta, + ) + + +def compile_skill(content: str, target: str) -> list[dict[str, str]]: + """Compile markdown skill to a target harness dialect.""" + doc = doc_from_markdown(content) + adapter = get_adapter(target) + files = adapter.render(doc) + return [ + { + "relpath": f.relpath, + "text": f.text, + "mode": f.mode, + } + for f in files + ] + + +def compile_all_dialects(content: str) -> dict[str, list[dict[str, str]]]: + """Compile markdown skill into all 12 supported AI harness dialects.""" + doc = doc_from_markdown(content) + results: dict[str, list[dict[str, str]]] = {} + for target, adapter in ADAPTERS.items(): + files = adapter.render(doc) + results[target] = [ + { + "relpath": f.relpath, + "text": f.text, + "mode": f.mode, + } + for f in files + ] + return results + + +def validate_skill(content: str) -> list[dict[str, Any]]: + """Validate skill frontmatter and return diagnostic warnings/errors.""" + return parse_diagnostics(content) + + +def compile_json(content: str, target: str | None = None) -> str: + """Entry point for WebAssembly / Pyodide / JS interop. + + Accepts raw markdown string, returns JSON string containing: + - valid: bool + - diagnostics: list of lint issues + - dialects: dict of rendered files per dialect (or single target) + - elapsed_ms: compilation time in milliseconds + """ + t0 = time.perf_counter() + diagnostics = validate_skill(content) + has_errors = any(d.get("severity") == 1 for d in diagnostics) + + if target: + dialects = {target: compile_skill(content, target)} + else: + dialects = compile_all_dialects(content) + + elapsed_ms = round((time.perf_counter() - t0) * 1000, 3) + + return json.dumps( + { + "valid": not has_errors, + "diagnostics": diagnostics, + "dialects": dialects, + "elapsed_ms": elapsed_ms, + }, + indent=2, + ) + + +__all__ = [ + "doc_from_markdown", + "compile_skill", + "compile_all_dialects", + "validate_skill", + "compile_json", +] diff --git a/tests/test_wasm.py b/tests/test_wasm.py new file mode 100644 index 0000000..f569a23 --- /dev/null +++ b/tests/test_wasm.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import json +import time +import unittest + +from cinch.adapters import ADAPTERS +from cinch.wasm import ( + compile_all_dialects, + compile_json, + compile_skill, + doc_from_markdown, + validate_skill, +) + +SAMPLE_SKILL = """--- +name: code-reviewer +description: Automated pull request code reviewer checking security and style. +paths: + - "src/**/*.py" +requires: + - git-tools +--- + +# Code Reviewer + +## Guidelines +1. Review all modified lines. +2. Ensure test coverage. +""" + + +class WasmCompilationTests(unittest.TestCase): + def test_doc_from_markdown(self) -> None: + doc = doc_from_markdown(SAMPLE_SKILL) + self.assertEqual(doc.name, "code-reviewer") + self.assertEqual( + doc.description, + "Automated pull request code reviewer checking security and style.", + ) + self.assertEqual(doc.paths, ("src/**/*.py",)) + self.assertEqual(doc.requires, ("git-tools",)) + # H1 matching doc name was stripped from body + self.assertNotIn("# Code Reviewer", doc.body) + self.assertIn("## Guidelines", doc.body) + + def test_doc_from_markdown_fallback_description(self) -> None: + raw = "---\nname: simple-tool\n---\n\nA helpful utility for tasks.\n" + doc = doc_from_markdown(raw) + self.assertEqual(doc.name, "simple-tool") + self.assertEqual(doc.description, "A helpful utility for tasks.") + + def test_compile_skill_single_dialect(self) -> None: + # 1. Claude Code + claude_files = compile_skill(SAMPLE_SKILL, "claude") + self.assertEqual(len(claude_files), 1) + self.assertEqual(claude_files[0]["relpath"], ".claude/skills/code-reviewer/SKILL.md") + self.assertIn('name: "code-reviewer"', claude_files[0]["text"]) + + # 2. Cursor + cursor_files = compile_skill(SAMPLE_SKILL, "cursor") + self.assertEqual(len(cursor_files), 1) + self.assertEqual(cursor_files[0]["relpath"], ".agents/skills/code-reviewer/SKILL.md") + self.assertIn('name: "code-reviewer"', cursor_files[0]["text"]) + self.assertIn('"src/**/*.py"', cursor_files[0]["text"]) + + # 3. Copilot + copilot_files = compile_skill(SAMPLE_SKILL, "copilot") + self.assertEqual(len(copilot_files), 1) + self.assertTrue(copilot_files[0]["relpath"].startswith(".github/")) + + def test_compile_all_dialects(self) -> None: + all_res = compile_all_dialects(SAMPLE_SKILL) + self.assertEqual(len(all_res), len(ADAPTERS)) + for key in ADAPTERS: + self.assertIn(key, all_res) + self.assertGreater(len(all_res[key]), 0) + self.assertIn("relpath", all_res[key][0]) + self.assertIn("text", all_res[key][0]) + + def test_compile_speed_under_10ms(self) -> None: + # Warmup + compile_all_dialects(SAMPLE_SKILL) + + # Benchmark 50 compilations across all 12 dialects + iterations = 50 + start = time.perf_counter() + for _ in range(iterations): + compile_all_dialects(SAMPLE_SKILL) + total_time = time.perf_counter() - start + avg_ms = (total_time / iterations) * 1000 + + self.assertLess(avg_ms, 10.0, f"Average compilation took {avg_ms:.2f}ms (expected <10ms)") + + def test_compile_json_interop(self) -> None: + raw_json = compile_json(SAMPLE_SKILL) + data = json.loads(raw_json) + self.assertTrue(data["valid"]) + self.assertIsInstance(data["dialects"], dict) + self.assertEqual(len(data["dialects"]), len(ADAPTERS)) + self.assertIn("claude", data["dialects"]) + self.assertIn("cursor", data["dialects"]) + self.assertIsInstance(data["elapsed_ms"], (int, float)) + + def test_compile_json_single_target(self) -> None: + raw_json = compile_json(SAMPLE_SKILL, target="cursor") + data = json.loads(raw_json) + self.assertTrue(data["valid"]) + self.assertEqual(list(data["dialects"].keys()), ["cursor"]) + + def test_validate_skill_diagnostics(self) -> None: + invalid_skill = "---\nname: Invalid_Name\n\tbad_tab: true\n" + diags = validate_skill(invalid_skill) + self.assertGreater(len(diags), 0) + messages = [d["message"] for d in diags] + # Should flag unclosed frontmatter and tabs + self.assertTrue(any("Unclosed YAML frontmatter" in m for m in messages)) + + +if __name__ == "__main__": + unittest.main()