feat(skill): pre-draw checkpoint, docs-sync gate, packaged self-check, ADRs
- SKILL.md: confirm type/pattern/size/cuts with the user before rendering; checklist hooks for the checkpoint and the packaged self-check - scripts/verify-docs-sync.py: CI now fails when the frontmatter description loses a type's lexical hook, the gallery can't reach a shipped example, or the README tree names a missing file - skills/diagram-design/scripts/self_check.py: distilled output checker that ships with the skill so installed agents can verify their own diagrams; adversarial suite in scripts/test-self-check.py - assets/index.html: six new tabs (OAuth sequence + five single-variant examples with a data-single variant lock) so every shipped example is browsable - docs/adr/0001-0004: settled policies recorded (pinned controller, taxonomy freeze, reveal-only autoplay, byte cap + trigger-rich description) - README: It's-working-if section, new gates documented; CONTRIBUTING: gate table, ADR guidance, type-addition invariants; CI: two new steps
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify that routing and browsing surfaces stay in sync with the skill.
|
||||
|
||||
Three drift classes, each of which has shipped before:
|
||||
|
||||
1. The SKILL.md frontmatter description is the only text an agent sees before
|
||||
deciding to load the skill — every visual type in the selection table must
|
||||
keep a lexical hook there.
|
||||
2. The gallery (assets/index.html) must reach every shipped example, and every
|
||||
gallery tab must point at a file that exists.
|
||||
3. Every concrete file named in README.md's architecture tree must exist.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SKILL = ROOT / "skills/diagram-design/SKILL.md"
|
||||
GALLERY = ROOT / "skills/diagram-design/assets/index.html"
|
||||
ASSET_DIR = ROOT / "skills/diagram-design/assets"
|
||||
README = ROOT / "README.md"
|
||||
VARIANTS = ("", "-dark", "-full")
|
||||
# Types whose selection-table name differs from its description vocabulary.
|
||||
DESCRIPTION_ALIASES = {
|
||||
"bar chart": "bar",
|
||||
"line chart": "line",
|
||||
"scatter plot": "scatter",
|
||||
}
|
||||
|
||||
|
||||
def normalized(text: str) -> str:
|
||||
text = text.casefold()
|
||||
text = re.sub(r"\s*/\s*", "/", text)
|
||||
return re.sub(r"\s+", " ", text)
|
||||
|
||||
|
||||
def frontmatter_description(markdown: str) -> str:
|
||||
parts = markdown.split("---")
|
||||
if len(parts) < 3:
|
||||
return ""
|
||||
match = re.search(r"^description:\s*(.+)$", parts[1], re.MULTILINE)
|
||||
return match.group(1).strip() if match else ""
|
||||
|
||||
|
||||
def selection_table_types(markdown: str) -> list[str]:
|
||||
start = markdown.find("### Visual-type guide")
|
||||
end = markdown.find("Rules of thumb", start)
|
||||
if start < 0 or end < 0:
|
||||
return []
|
||||
names = re.findall(r"^\|[^|]*\|\s*\*\*([^*]+)\*\*\s*\|", markdown[start:end], re.MULTILINE)
|
||||
return [name.strip() for name in names]
|
||||
|
||||
|
||||
def check_description(errors: list[str]) -> None:
|
||||
markdown = SKILL.read_text(encoding="utf-8")
|
||||
description = normalized(frontmatter_description(markdown))
|
||||
if not description:
|
||||
errors.append("SKILL.md frontmatter description is missing")
|
||||
return
|
||||
types = selection_table_types(markdown)
|
||||
if len(types) != 27:
|
||||
errors.append(f"expected 27 visual types in the selection table; found {len(types)}")
|
||||
for name in types:
|
||||
key = normalized(name)
|
||||
key = DESCRIPTION_ALIASES.get(key, key)
|
||||
if key not in description:
|
||||
errors.append(
|
||||
f"description lost the lexical hook for type {name!r} "
|
||||
f"(expected {key!r} in the SKILL.md frontmatter description)"
|
||||
)
|
||||
|
||||
|
||||
def gallery_types(source: str) -> list[str]:
|
||||
return re.findall(r'data-type="([^"]+)"', source)
|
||||
|
||||
|
||||
def check_gallery(errors: list[str]) -> None:
|
||||
source = GALLERY.read_text(encoding="utf-8")
|
||||
types = gallery_types(source)
|
||||
if not types:
|
||||
errors.append("gallery has no data-type tabs")
|
||||
return
|
||||
reachable = {f"example-{name}{variant}.html" for name in types for variant in VARIANTS}
|
||||
on_disk = {path.name for path in ASSET_DIR.glob("example-*.html")}
|
||||
for name in sorted(on_disk - reachable):
|
||||
errors.append(f"gallery cannot reach shipped example {name}; add a tab to assets/index.html")
|
||||
for name in sorted(types):
|
||||
if f"example-{name}.html" not in on_disk:
|
||||
errors.append(f"gallery tab {name!r} points at a missing example-{name}.html")
|
||||
|
||||
|
||||
def readme_tree_tokens(markdown: str) -> list[str]:
|
||||
blocks = re.findall(r"```\n(diagram-design/\n.*?)```", markdown, re.DOTALL)
|
||||
tokens: list[str] = []
|
||||
for block in blocks:
|
||||
tokens.extend(
|
||||
re.findall(r"([A-Za-z0-9][A-Za-z0-9_.*-]*\.(?:md|html|py|yml|yaml|json|txt|mmd|drawio|png))", block)
|
||||
)
|
||||
return tokens
|
||||
|
||||
|
||||
def check_readme_tree(errors: list[str]) -> None:
|
||||
markdown = README.read_text(encoding="utf-8")
|
||||
tokens = readme_tree_tokens(markdown)
|
||||
if not tokens:
|
||||
errors.append("README architecture tree not found or names no files")
|
||||
return
|
||||
for token in sorted(set(tokens)):
|
||||
matches = list(ROOT.rglob(token))
|
||||
if not matches:
|
||||
errors.append(f"README architecture tree names {token!r} but no such file exists")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
check_description(errors)
|
||||
check_gallery(errors)
|
||||
check_readme_tree(errors)
|
||||
if errors:
|
||||
print("FAIL docs sync")
|
||||
for error in errors:
|
||||
print(f" - {error}")
|
||||
return 1
|
||||
print("OK docs sync: description hooks, gallery reachability, README tree")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user