#!/usr/bin/env python3 """Verify semantic-pattern routing and the animated example contract. Uses only the Python standard library and never executes example JavaScript. """ from __future__ import annotations import argparse import re import sys from collections import Counter from html.parser import HTMLParser from pathlib import Path from urllib.parse import parse_qs, urlparse ROOT = Path(__file__).resolve().parent.parent SKILL = ROOT / "skills/diagram-design/SKILL.md" PATTERNS = ROOT / "skills/diagram-design/references/semantic-patterns.md" ANIMATION = ROOT / "skills/diagram-design/references/animation.md" EXAMPLE = ROOT / "skills/diagram-design/assets/example-policy-trace-animated.html" MAX_SKILL_BYTES = 35_000 PATTERN_NAMES = ( "Fan-in queue / bottleneck", "Stage framework with semantic slots", "Unstructured input → structured artifact", "Paired policy-evaluation traces", "Secure paved road", "Governance / control catalog", "Compensating security layers", ) PATTERN_FIELDS = ( "Selection triggers:", "Required primitives:", "Complexity budget:", "Anti-patterns:", "Static fallback:", "Nearest visual type:", ) MOTION_MODES = ("none", "reveal", "step", "loop") MOTION_PRIMITIVES = ( "Path draw", "Staggered reveal", "Queue counter", "Typing / field population", "Policy evaluation", "Flow token", "Containment", "Audit append", ) CONTROL_ACTIONS = {"play", "pause", "replay", "prev", "next"} def is_approved_google_fonts_stylesheet(value: str) -> bool: try: parsed = urlparse(value) port = parsed.port except ValueError: return False return ( parsed.scheme.casefold() == "https" and parsed.hostname is not None and parsed.hostname.casefold() == "fonts.googleapis.com" and port in (None, 443) and parsed.path == "/css2" and not parsed.fragment and any( family.strip() for family in parse_qs(parsed.query, keep_blank_values=True).get("family", []) ) ) class ContractParser(HTMLParser): def __init__(self) -> None: super().__init__(convert_charrefs=True) self.ids: list[str] = [] self.controls: set[str] = set() self.motion_roots: list[dict[str, str]] = [] self.motion_items: list[dict[str, str]] = [] self.statuses: list[dict[str, str]] = [] self.scripts: list[dict[str, str]] = [] self.remote_assets: list[tuple[str, str]] = [] self.trace_connectors: list[dict[str, str]] = [] self.trace_b_terminals: list[dict[str, str]] = [] self.trace_b_not_reached: list[dict[str, str]] = [] self.trace_b_outcomes: list[dict[str, str]] = [] self.svg_lines: list[dict[str, str]] = [] self.noscript_count = 0 self.svgs: list[dict[str, object]] = [] self._svg_depth = 0 self._current_svg: dict[str, object] | None = None self._capture: str | None = None def handle_starttag( self, tag: str, attrs: list[tuple[str, str | None]] ) -> None: data = {name.casefold(): value or "" for name, value in attrs} if data.get("id"): self.ids.append(data["id"]) if "data-motion-root" in data: self.motion_roots.append(data) if "data-motion-item" in data: self.motion_items.append(data) if data.get("data-motion-action"): self.controls.add(data["data-motion-action"]) if data.get("role") == "status": self.statuses.append(data) if tag == "script": self.scripts.append(data) if tag == "noscript": self.noscript_count += 1 if data.get("data-trace-connector") == "trace-b": self.trace_connectors.append(data) if data.get("data-trace-b-terminal") == "fail": self.trace_b_terminals.append(data) if data.get("data-trace-b-state") == "not-reached": self.trace_b_not_reached.append(data) if data.get("data-trace-b-outcome") == "denied": self.trace_b_outcomes.append(data) if tag == "line": self.svg_lines.append(data) for attr in ("src", "href"): value = data.get(attr, "").strip() try: parsed_value = urlparse(value) remote = parsed_value.scheme.casefold() in {"http", "https"} or bool(parsed_value.netloc) except ValueError: remote = True if value and remote: allowed_font = tag == "link" and attr == "href" and is_approved_google_fonts_stylesheet(value) if not allowed_font: self.remote_assets.append((tag, value)) if tag == "svg": self._svg_depth = 1 self._current_svg = { "attrs": data, "first_child": None, "title": {"attrs": {}, "text": ""}, "desc": {"attrs": {}, "text": ""}, } self.svgs.append(self._current_svg) return if self._svg_depth: self._svg_depth += 1 assert self._current_svg is not None if self._svg_depth == 2 and self._current_svg["first_child"] is None: self._current_svg["first_child"] = tag if tag in {"title", "desc"} and self._svg_depth == 2: self._current_svg[tag] = {"attrs": data, "text": ""} self._capture = tag def handle_endtag(self, tag: str) -> None: if self._svg_depth: if tag in {"title", "desc"}: self._capture = None self._svg_depth -= 1 if self._svg_depth == 0: self._current_svg = None def handle_data(self, data: str) -> None: if self._capture and self._current_svg: node = self._current_svg[self._capture] assert isinstance(node, dict) node["text"] = str(node.get("text", "")) + data def section(markdown: str, heading: str, next_heading: str | None) -> str: start = markdown.find(heading) if start < 0: return "" end = markdown.find(next_heading, start + len(heading)) if next_heading else -1 return markdown[start:] if end < 0 else markdown[start:end] def verify_markdown() -> list[str]: errors: list[str] = [] skill_bytes = SKILL.read_bytes() skill = skill_bytes.decode("utf-8") patterns = PATTERNS.read_text(encoding="utf-8") animation = ANIMATION.read_text(encoding="utf-8") if len(skill_bytes) > MAX_SKILL_BYTES: errors.append( f"SKILL.md exceeds {MAX_SKILL_BYTES} bytes: {len(skill_bytes)} bytes" ) if "Selection: semantic pattern, then visual type" not in skill: errors.append("SKILL.md must choose semantic pattern before visual type") if skill.find("semantic-patterns.md") > skill.find("### Visual-type guide (27)"): errors.append("semantic-pattern router must precede the visual-type guide") visual_guide = section(skill, "### Visual-type guide (27)", "Rules of thumb:") visual_rows = re.findall(r"^\|.*\[type-[^)]+\.md\]", visual_guide, re.MULTILINE) if len(visual_rows) != 27: errors.append(f"visual-type guide must preserve 27 rows; found {len(visual_rows)}") for index, name in enumerate(PATTERN_NAMES, 1): if name not in skill: errors.append(f"SKILL.md does not route semantic pattern: {name}") next_heading = f"## {index + 1}." if index < len(PATTERN_NAMES) else "## Composition rules" block = section(patterns, f"## {index}. {name}", next_heading) if not block: errors.append(f"semantic-patterns.md is missing section {index}: {name}") continue for field in PATTERN_FIELDS: if f"**{field}**" not in block: errors.append(f"{name} is missing required field {field}") for mode in MOTION_MODES: if f"`{mode}`" not in animation: errors.append(f"animation.md is missing mode {mode}") for primitive in MOTION_PRIMITIVES: if f"**{primitive}**" not in animation: errors.append(f"animation.md is missing primitive {primitive}") required_animation_terms = ( "prefers-reduced-motion", "CSS-only", "inline JavaScript", "Play, Pause, Replay, Previous, and Next", "deterministic", "static", "color-only", "screenshot", "export", ) for term in required_animation_terms: if term not in animation: errors.append(f"animation.md is missing contract term {term!r}") return errors def verify_example(path: Path = EXAMPLE) -> list[str]: errors: list[str] = [] source = path.read_text(encoding="utf-8") parser = ContractParser() parser.feed(source) parser.close() duplicates = sorted(name for name, count in Counter(parser.ids).items() if count > 1) if duplicates: errors.append("duplicate HTML/SVG IDs: " + ", ".join(duplicates)) if parser.remote_assets: errors.append(f"remote dependencies other than allowed fonts: {parser.remote_assets}") if len(parser.scripts) != 1: errors.append(f"expected one minimal inline control script; found {len(parser.scripts)}") else: script = parser.scripts[0] if script.get("src"): errors.append("remote or file-based scripts are forbidden") if "data-diagram-controls" not in script: errors.append("inline script must be scoped with data-diagram-controls") if len(parser.motion_roots) != 1: errors.append(f"expected one motion root; found {len(parser.motion_roots)}") else: root = parser.motion_roots[0] if root.get("data-motion-mode") != "step": errors.append("working example must use step mode") if root.get("data-static-frame") != "complete": errors.append("motion root must declare a complete static fallback") if root.get("data-frame") != "static": errors.append("source first frame must be static") missing_controls = CONTROL_ACTIONS - parser.controls if missing_controls: errors.append("missing controls: " + ", ".join(sorted(missing_controls))) if not parser.statuses: errors.append("interactive example needs accessible status text") else: status = parser.statuses[0] if status.get("aria-live") != "polite" or status.get("aria-atomic") != "true": errors.append("status needs aria-live=polite and aria-atomic=true") if parser.noscript_count != 1: errors.append("example needs one explicit no-JavaScript fallback") if ( len(parser.trace_connectors) != 1 or len(parser.trace_b_terminals) != 1 or len(parser.trace_b_not_reached) != 2 or len(parser.trace_b_outcomes) != 1 ): errors.append( "Trace B needs one persistent connector, one terminal FAIL, " "two NOT REACHED rows, and one denied outcome" ) else: connector = parser.trace_connectors[0] terminal = parser.trace_b_terminals[0] not_reached = parser.trace_b_not_reached outcome = parser.trace_b_outcomes[0] try: connector_end = float(connector["y2"]) fail_end = float(terminal["y"]) + float(terminal["height"]) later_starts = [float(node["y"]) for node in not_reached] + [float(outcome["y"])] connector_x = float(connector["x1"]) vertical = connector_x == float(connector["x2"]) continuations = [ line for line in parser.svg_lines if line is not connector and float(line.get("x1", "nan")) == connector_x and float(line.get("x2", "nan")) == connector_x and max(float(line.get("y1", "nan")), float(line.get("y2", "nan"))) > fail_end ] except (KeyError, ValueError): errors.append("Trace B structural coordinates must be numeric") else: if ( not vertical or connector_end != fail_end or any(connector_end >= start for start in later_starts) or continuations ): errors.append( "Trace B persistent connector must terminate at the first FAIL " "before NOT REACHED rows and the denied outcome" ) steps: list[int] = [] for item in parser.motion_items: try: steps.append(int(item.get("data-step", ""))) except ValueError: errors.append("every motion item needs an integer data-step") if not item.get("aria-label", "").strip(): errors.append("every semantic motion item needs an aria-label") if steps != [1, 2, 3, 4, 5]: errors.append(f"policy steps must be exactly [1, 2, 3, 4, 5]; found {steps}") if len(parser.svgs) != 1: errors.append(f"expected one SVG; found {len(parser.svgs)}") else: svg = parser.svgs[0] attrs = svg["attrs"] title = svg["title"] desc = svg["desc"] assert isinstance(attrs, dict) and isinstance(title, dict) and isinstance(desc, dict) title_attrs = title.get("attrs", {}) desc_attrs = desc.get("attrs", {}) labelled = attrs.get("aria-labelledby", "").split() if attrs.get("role") != "img" or svg["first_child"] != "title": errors.append("SVG needs role=img and title as first child") if not str(title.get("text", "")).strip() or not str(desc.get("text", "")).strip(): errors.append("SVG title and description must be non-empty") if not isinstance(title_attrs, dict) or not isinstance(desc_attrs, dict): errors.append("SVG title/description attributes could not be parsed") elif labelled != [title_attrs.get("id"), desc_attrs.get("id")]: errors.append("SVG aria-labelledby must resolve to title then description") required_source = { "@media (prefers-reduced-motion: reduce)": "reduced-motion fallback", "@media print": "print/static fallback", 'data-static-frame="complete"': "complete source fallback", "?motion=static": "static query documentation", "params.get('motion') === 'static'": "static screenshot path", "document.fonts.ready": "font-stable screenshot hook", "visibilitychange": "background pause", "ArrowRight": "right-arrow keyboard path", "ArrowLeft": "left-arrow keyboard path", "event.code === 'Space'": "space play/pause path", "Home": "home/reset keyboard path", "toLowerCase() === 'r'": "replay keyboard path", "window.setTimeout": "deterministic timer", "requestedStep !== null": "non-null test-step validation", "Number.isSafeInteger(parsedStep)": "integer test-step validation", "parsedStep <= count": "bounded test-step validation", "playback controls unavailable": "static/reduced-motion browser status", "PASS": "pass text state", "FAIL": "fail text state", "SKIPPED": "skipped text state", "NOT REACHED": "not-reached text state", "FIRST DIVERGENCE": "first-divergence marker", } for needle, label in required_source.items(): if needle not in source: errors.append(f"missing {label}: {needle}") if "setInterval" in source or "Math.random" in source: errors.append("motion timing must not use setInterval or randomness") if re.search(r"]*\bsrc\s*=", source, re.IGNORECASE): errors.append("example must not load any remote script") return errors def main() -> int: argument_parser = argparse.ArgumentParser(description=__doc__) group = argument_parser.add_mutually_exclusive_group() group.add_argument("--markdown-only", action="store_true") group.add_argument("--example-only", action="store_true") args = argument_parser.parse_args() errors = ( verify_markdown() if args.markdown_only else verify_example() if args.example_only else verify_markdown() + verify_example() ) if errors: print("Semantic/motion verification failed:") for error in errors: print(f"- {error}") return 1 if not args.example_only: print("OK: 7 semantic patterns route independently to the preserved 27 visual types") print("OK: animation modes, primitives, static fallback, and accessibility contract") if not args.markdown_only: print("OK: policy-trace example controls, structure, reduced motion, and unique IDs") return 0 if __name__ == "__main__": sys.exit(main())