#!/usr/bin/env python3
"""Validate this portable skill's structure; no network or third-party packages."""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
from urllib.parse import unquote, urlsplit
ROOT = Path(__file__).resolve().parents[1]
REQUIRED = (
"SKILL.md", "README.md", "VERIFICATION.md", "evals/scenarios.json",
"references/design-and-interaction.md", "references/architecture-and-effects.md",
"references/layout-and-rendering.md", "references/versions-and-components.md",
"references/testing-and-release.md", "references/case-studies.md",
"references/sources.md", "examples/contracts/README.md",
"examples/contracts/go.mod", "examples/contracts/patterns.go",
"examples/contracts/patterns_test.go",
"examples/WORKED-EXAMPLES.md", "examples/worked/README.md",
"examples/worked/go.mod", "scripts/test_offline_examples.py",
"examples/worked/cmd/picker/main.go", "examples/worked/cmd/picker/model.go",
"examples/worked/cmd/search/main.go", "examples/worked/cmd/search/model.go",
"examples/worked/cmd/tail/main.go", "examples/worked/cmd/tail/model.go",
"examples/worked/internal/display/render.go",
)
def heading_ids(text: str) -> set[str]:
"""Handle the ordinary Markdown headings used by this bundle."""
result: set[str] = set()
for line in text.splitlines():
match = re.match(r"^#{1,6}\s+(.+)$", line)
if match:
value = re.sub(r"[^\w\s-]", "", match.group(1).strip().lower())
result.add(re.sub(r"\s", "-", value))
return result
def validate() -> tuple[list[str], int, int, int]:
errors: list[str] = []
for rel in REQUIRED:
if not (ROOT / rel).is_file():
errors.append(f"Missing required file: {rel}")
skill_path = ROOT / "SKILL.md"
if not skill_path.is_file():
return errors, 0, 0, 0
skill = skill_path.read_text(encoding="utf-8")
match = re.match(r"\A---\n(.*?)\n---\n", skill, re.S)
if not match:
errors.append("SKILL.md requires a leading YAML frontmatter block")
else:
fields = dict(re.findall(r"^([a-z-]+): (.+)$", match.group(1), re.M))
name, description = fields.get("name", ""), fields.get("description", "")
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name) or len(name) > 64:
errors.append("Invalid skill name")
if name != ROOT.name:
errors.append("Skill name and directory name differ")
if not description or len(description) > 1024:
errors.append("Description must have 1–1024 characters")
md_files = sorted(ROOT.rglob("*.md"))
checked_links = 0
for path in md_files:
text = path.read_text(encoding="utf-8")
if len(re.findall(r"^```", text, re.M)) % 2:
errors.append(f"Unbalanced code fences: {path.relative_to(ROOT)}")
for target in re.findall(r"\[[^\]\n]+\]\(([^)\s]+)\)", text):
url = urlsplit(target)
if url.scheme or url.netloc:
continue # Remote availability is intentionally not checked.
dest = (path.parent / unquote(url.path)).resolve() if url.path else path
checked_links += 1
if not dest.is_relative_to(ROOT):
errors.append(f"Link escapes skill root: {target}")
elif not dest.is_file():
errors.append(f"Broken local link in {path.name}: {target}")
elif url.fragment and dest.suffix == ".md":
if unquote(url.fragment) not in heading_ids(dest.read_text(encoding="utf-8")):
errors.append(f"Unknown Markdown anchor in {path.name}: {target}")
scenario_count = 0
try:
data = json.loads((ROOT / "evals/scenarios.json").read_text(encoding="utf-8"))
if data.get("skill") != "charm-tui" or data.get("format_version") != 1:
errors.append("Evaluation metadata mismatch")
ids: set[str] = set()
for case in data["scenarios"]:
if not isinstance(case.get("id"), str) or case["id"] in ids:
errors.append("Invalid or duplicate evaluation ID")
ids.add(case["id"])
for key in ("setup", "prompt"):
if not isinstance(case.get(key), str) or not case[key].strip():
errors.append(f"Missing evaluation {key}: {case['id']}")
for key in ("must_pass", "must_not"):
value = case.get(key)
if not isinstance(value, list) or not value or not all(
isinstance(item, str) and item.strip() for item in value
):
errors.append(f"Invalid evaluation {key}: {case['id']}")
scenario_count += 1
except (OSError, ValueError, KeyError, TypeError) as exc:
errors.append(f"Cannot validate evaluation scenarios: {exc}")
return errors, len(md_files), checked_links, scenario_count
if __name__ == "__main__":
problems, documents, links, scenarios = validate()
if problems:
for problem in problems:
print(f"FAIL: {problem}", file=sys.stderr)
raise SystemExit(1)
print(f"PASS: {documents} Markdown files; {links} local links; {scenarios} evaluation scenarios")
print("PASS: required files, frontmatter, code fences, and evaluation structure")
print("Not checked: remote URLs, agent performance, Charm compilation, or real-terminal behavior")