ui-ux-pro-max-skill/scripts/validate-agent-guide.py
Viet Tran a38d04c3d5
feat(search): overhaul relevance and curated design data
Overhaul BM25 relevance, reasoning and data-quality contracts; refresh UI styles and framework guidance; add resilient text, chip, badge and micro-interaction guidance; strengthen release, provenance and catalog refresh gates; update bilingual documentation.
2026-08-14 00:08:23 +07:00

195 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""Validate agent guide claims and run its locked semantic examples."""
import csv
import json
import re
import shlex
import subprocess
import sys
from functools import lru_cache
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / "src/ui-ux-pro-max/data"
TEMPLATES = ROOT / "src/ui-ux-pro-max/templates"
PLATFORMS = TEMPLATES / "platforms"
GUIDE = TEMPLATES / "base/skill-content.md"
CLAUDE_GUIDE = ROOT / ".claude/skills/ui-ux-pro-max/SKILL.md"
README = ROOT / "README.md"
README_ZH = ROOT / "README.zh.md"
DOMAINS = {
"product", "style", "typography", "color", "landing", "chart", "ux",
"gsap", "react", "web", "icons", "google-fonts",
}
FLAGS = {
"--domain", "-d", "--stack", "-s", "--max-results", "-n", "--json",
"--full", "--design-system", "-ds", "--project-name", "-p", "--format",
"-f", "--persist", "--page", "--output-dir", "-o", "--force",
"--variance", "--motion", "--density",
}
PLATFORM_COUNT = 20
GUIDE_REQUIREMENTS = (
"## Query Contract", "one dominant intent", "Retry once",
"Do not persist unverified output", "explicit accessibility outcome terms",
)
SEARCH_COMMAND = re.compile(r"search\.py|\{\{SCRIPT_PATH\}\}")
QUOTED_QUERY = re.compile(r'search\.py"?\s+"[^"]+"')
DOMAIN_FLAG = re.compile(r"--domain\s+(\S+)")
STACK_FLAG = re.compile(r"--stack\s+(\S+)")
@lru_cache(maxsize=None)
def row_count(name):
with (DATA / name).open(encoding="utf-8", newline="") as handle:
return sum(1 for _ in csv.DictReader(handle))
@lru_cache(maxsize=1)
def style_counts():
with (DATA / "styles.csv").open(encoding="utf-8", newline="") as handle:
rows = list(csv.DictReader(handle))
return {
"searchable": sum(row.get("Status") != "deprecated" for row in rows),
"active": sum(row.get("Status") == "active" for row in rows),
}
@lru_cache(maxsize=1)
def stack_names():
return frozenset(path.stem for path in (DATA / "stacks").glob("*.csv"))
def expected_description():
styles = style_counts()
return (
"UI/UX design intelligence for web, mobile, and desktop. This skill should be used when designing, "
"building, reviewing, or fixing interfaces, including pages, components, design "
"systems, accessibility, interaction, responsive layout, typography, color, "
"charts, and stack-specific UI implementation. Searchable local data: "
f"{styles['searchable']} searchable styles ({styles['active']} active), "
f"{row_count('colors.csv')} product palettes "
f"and reasoning profiles, {row_count('typography.csv')} font pairings, "
f"{row_count('ux-guidelines.csv')} UX guidelines, {row_count('icons.csv')} icons, "
f"{row_count('motion.csv')} GSAP presets, {row_count('charts.csv')} chart types, "
f"and {len(stack_names())} stacks."
)
def run_json(*args):
command = [
sys.executable, str(ROOT / "src/ui-ux-pro-max/scripts/search.py"),
*args, "--json",
]
completed = subprocess.run(command, check=True, capture_output=True, text=True)
return json.loads(completed.stdout)
def validate_commands(label, text):
errors = []
commands = []
for line in text.splitlines():
command = line.strip()
if command.startswith(("python ", "python3 ")) and (
SEARCH_COMMAND.search(command)):
commands.append(command.replace("{{SCRIPT_PATH}}", "search.py"))
if not commands:
errors.append(f"{label}: no documented search commands found")
for command in commands:
if not QUOTED_QUERY.search(command):
errors.append(f"{label}: query must immediately follow search.py: {command}")
try:
tokens = shlex.split(command.replace("[", "").replace("]", ""))
except ValueError as exc:
errors.append(f"{label}: invalid shell command ({exc}): {command}")
continue
unknown_flags = sorted({token for token in tokens if token.startswith("-") and token not in FLAGS})
if unknown_flags:
errors.append(f"{label}: unknown flags {unknown_flags}: {command}")
domain = DOMAIN_FLAG.search(command)
if domain and domain.group(1) not in DOMAINS | {"<domain>"}:
errors.append(f"{label}: unknown domain {domain.group(1)!r}")
stack = STACK_FLAG.search(command)
if stack and stack.group(1) not in stack_names() | {"<stack>"}:
errors.append(f"{label}: unknown stack {stack.group(1)!r}")
if "--persist" in command and "--output-dir" not in command:
errors.append(f"{label}: persisted command lacks --output-dir")
return errors
def validate():
errors = []
product_counts = {
"products": row_count("products.csv"),
"palettes": row_count("colors.csv"),
"reasoning profiles": row_count("ui-reasoning.csv"),
}
if len(set(product_counts.values())) != 1:
errors.append(f"product/palette/reasoning counts differ: {product_counts}")
description = expected_description()
claude_text = CLAUDE_GUIDE.read_text(encoding="utf-8")
if description not in claude_text:
errors.append("claude: frontmatter description/counts differ from canonical metadata")
styles = style_counts()
guide_claims = (
f"{styles['searchable']} searchable styles ({styles['active']} active)",
f"{row_count('typography.csv')} font pairings",
f"{row_count('ux-guidelines.csv')} UX guidelines",
f"{row_count('icons.csv')} curated icons",
f"{row_count('motion.csv')} GSAP presets",
f"{row_count('charts.csv')} chart types",
f"{len(stack_names())} technology stacks",
)
for claim in guide_claims:
if claim not in claude_text:
errors.append(f"claude: missing live count claim {claim!r}")
reasoning_count = row_count("ui-reasoning.csv")
public_claims = (
(README, (f"{reasoning_count} Reasoning Rules",
f"{row_count('ux-guidelines.csv')} UX Guidelines")),
(README_ZH, (f"{reasoning_count} 条推理规则",
f"{row_count('ux-guidelines.csv')} 条 UX 指南")),
)
for path, claims in public_claims:
text = path.read_text(encoding="utf-8")
for claim in claims:
if claim not in text:
errors.append(f"{path.name}: missing live count claim {claim!r}")
configs = sorted(PLATFORMS.glob("*.json"))
if len(configs) != PLATFORM_COUNT:
errors.append(f"expected {PLATFORM_COUNT} platform configs, got {len(configs)}")
for path in configs:
config = json.loads(path.read_text(encoding="utf-8"))
if config.get("description") != description:
errors.append(f"{path.name}: stale description")
frontmatter = config.get("frontmatter") or {}
if frontmatter.get("description") and frontmatter["description"] != description:
errors.append(f"{path.name}: stale frontmatter description")
for label, path in (("canonical", GUIDE), ("claude", CLAUDE_GUIDE)):
text = path.read_text(encoding="utf-8")
for phrase in GUIDE_REQUIREMENTS:
if phrase not in text:
errors.append(f"{label}: missing {phrase!r}")
errors.extend(validate_commands(label, text))
design = run_json("beauty spa wellness service", "--design-system")
if design["design_system"]["category"] != "Beauty/Spa/Wellness Service":
errors.append("design-system example resolved wrong product category")
ux = run_json("keyboard focus modal", "--domain", "ux")
if ux.get("domain") != "ux" or not ux.get("results"):
errors.append("focused UX example did not return an explicit UX match")
stack = run_json("virtualized list", "--stack", "react-native")
if stack.get("stack") != "react-native" or not stack.get("results"):
errors.append("React Native stack example did not return a stack match")
return errors
if __name__ == "__main__":
problems = validate()
if problems:
print("Agent guide validation failed:\n- " + "\n- ".join(problems), file=sys.stderr)
raise SystemExit(1)
print(
f"Agent guide validation passed: {PLATFORM_COUNT} platforms "
"and 3 locked examples checked."
)