Files
yao-meta-skill/scripts/render_artifact_design_profile.py
2026-05-01 14:27:51 +08:00

347 lines
14 KiB
Python

#!/usr/bin/env python3
import argparse
import json
import re
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
yaml = None
ARTIFACT_FAMILIES = [
{
"key": "tutorial",
"label": "Tutorial or guide",
"keywords": ["tutorial", "guide", "lesson", "course", "walkthrough", "how to", "教程", "指南", "课程"],
"direction": "Progressive instructional layout with domain-specific section names, short success checks, and examples close to the user's real input.",
"layout": ["opening promise", "task-specific sections", "worked example", "success checks", "common mistakes"],
"quality_gates": [
"Replace generic headings with learner- and domain-specific headings.",
"Pair every major step with a visible success check.",
"Do not add screenshots unless they are real, current, and action-relevant.",
],
},
{
"key": "report",
"label": "Report or brief",
"keywords": ["report", "brief", "analysis", "summary", "memo", "white paper", "报告", "简报", "分析", "总结"],
"direction": "High-trust editorial report with a clear first-screen thesis, compact evidence blocks, and decisions separated from supporting detail.",
"layout": ["thesis", "evidence blocks", "decision table", "risks", "next actions"],
"quality_gates": [
"Keep the first screen useful without requiring the reader to parse every detail.",
"Use tables only for comparisons; move explanations below the table.",
"Keep source notes readable without flooding the body with markers.",
],
},
{
"key": "review_viewer",
"label": "Review viewer",
"keywords": ["review", "viewer", "compare", "diff", "audit", "评审", "对比", "审查"],
"direction": "Side-by-side reviewer studio with explicit tradeoffs, evidence readiness, and fast paths for approving, blocking, or requesting one focused fix.",
"layout": ["summary", "variant comparison", "evidence", "risks", "review decision"],
"quality_gates": [
"Make differences visible instead of hiding them in prose.",
"Separate author-facing recommendations from reviewer-only evidence.",
"Surface conflicts clearly and keep routine benchmark synthesis quiet.",
],
},
{
"key": "dashboard",
"label": "Dashboard or metrics page",
"keywords": ["dashboard", "metric", "score", "kpi", "chart", "table", "scorecard", "仪表盘", "指标", "评分", "图表", "表格"],
"direction": "Metric-first dashboard with stable dimensions, short labels, visible deltas, and narrative callouts only where they change interpretation.",
"layout": ["metric board", "ranked signals", "comparison rows", "interpretation", "action queue"],
"quality_gates": [
"Avoid paragraph-heavy table cells.",
"Keep charts tied to one analytical question each.",
"Preserve stable color meaning across metrics and entities.",
],
},
{
"key": "visual_capture",
"label": "Screenshot or visual evidence",
"keywords": ["screenshot", "screen capture", "image evidence", "figma", "截图", "图片", "截屏", "视觉证据"],
"direction": "Evidence-led visual artifact that records source, viewport, crop intent, and the exact region the reader should inspect.",
"layout": ["source context", "visual frame", "inspection notes", "limitations", "next capture"],
"quality_gates": [
"Never invent missing screenshots or visual states.",
"Record source, viewport, and crop intent.",
"Describe the action-relevant region instead of only saying an image exists.",
],
},
{
"key": "slide_like",
"label": "Slide-like narrative",
"keywords": ["slide", "ppt", "deck", "presentation", "one-pager", "演示", "幻灯片", "一页纸"],
"direction": "Narrative page rhythm with strong section roles, controlled density, and a visual system chosen from the topic rather than a default template.",
"layout": ["hero claim", "supporting proof", "structured contrast", "implication", "closing action"],
"quality_gates": [
"Plan role, density, and rhythm before writing HTML.",
"Avoid three or more repeated surfaces in a row.",
"Use structure, spacing, and type before decoration.",
],
},
{
"key": "code_or_cli",
"label": "Code, CLI, or implementation guide",
"keywords": ["code", "cli", "script", "api", "command", "terminal", "代码", "脚本", "命令", "接口"],
"direction": "Execution-focused technical artifact with environment assumptions, copyable commands, expected outputs, and side effects made explicit.",
"layout": ["prerequisites", "commands", "expected output", "failure handling", "rollback or cleanup"],
"quality_gates": [
"Name the working directory and required inputs for commands.",
"Mark destructive, networked, or external side-effect operations.",
"Prefer the smallest runnable snippet over broad framework scaffolding.",
],
},
]
DESIGN_TOKENS = {
"type": [
"Use a distinctive display face or serif for major claims when the artifact is editorial.",
"Use a restrained sans for dense body text and technical details.",
"Use mono only for metadata, paths, commands, labels, and evidence tags.",
],
"color": [
"Choose colors from the artifact's domain, brand, or evidence mood.",
"Do not default to Kami parchment, purple gradients, or generic SaaS blue unless the content justifies it.",
"Keep accent color limited to decisions, active states, risk, or section anchors.",
],
"spacing": [
"Prefer clear grid rhythm over floating decorative cards.",
"Increase whitespace around decisions and shrink whitespace around supporting metadata.",
"Split dense content instead of shrinking type or adding scroll traps.",
],
"components": [
"Use cards for grouped evidence, tables for comparisons, callouts for decisions, and timelines for sequence.",
"Avoid cards inside cards.",
"Keep reviewer-only detail visible but visually quieter than user-facing guidance.",
],
}
def parse_frontmatter(text: str) -> tuple[dict, str]:
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return {}, text
try:
end_index = lines[1:].index("---") + 1
except ValueError:
return {}, text
frontmatter_text = "\n".join(lines[1:end_index])
body = "\n".join(lines[end_index + 1 :]).lstrip()
if yaml is not None:
payload = yaml.safe_load(frontmatter_text) or {}
return payload if isinstance(payload, dict) else {}, body
data = {}
for line in frontmatter_text.splitlines():
if ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip().strip('"')
return data, body
def load_json(path: Path) -> dict:
if not path.exists():
return {}
return json.loads(path.read_text(encoding="utf-8"))
def normalized_text(skill_dir: Path) -> str:
skill_text = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
frontmatter, body = parse_frontmatter(skill_text)
intent = load_json(skill_dir / "reports" / "intent-confidence.json")
output_risk = load_json(skill_dir / "reports" / "output-risk-profile.json")
context = intent.get("context", {}) if isinstance(intent, dict) else {}
risk_labels = " ".join(item.get("label", "") for item in output_risk.get("risk_families", []))
parts = [
skill_dir.name,
str(frontmatter.get("name", "")),
str(frontmatter.get("description", "")),
body,
str(context.get("job", "")),
str(context.get("primary_output", "")),
" ".join(context.get("real_inputs", []) or []),
" ".join(context.get("constraints", []) or []),
" ".join(context.get("standards", []) or []),
risk_labels,
]
return " ".join(parts).lower()
def match_family(text: str, family: dict) -> tuple[int, list[str]]:
hits = []
for keyword in family["keywords"]:
if keyword.lower() in text:
hits.append(keyword)
return len(hits), hits
def dedupe(items: list[str], limit: int) -> list[str]:
seen = set()
output = []
for item in items:
if not item or item in seen:
continue
seen.add(item)
output.append(item)
if len(output) == limit:
break
return output
def design_system_name(matched: list[dict]) -> str:
keys = {item["key"] for item in matched}
if "dashboard" in keys:
return "metric editorial"
if "review_viewer" in keys:
return "review studio"
if "slide_like" in keys:
return "narrative visual brief"
if "tutorial" in keys:
return "guided learning document"
if "visual_capture" in keys:
return "evidence frame"
return "content-led editorial"
def build_summary(skill_dir: Path) -> dict:
text = normalized_text(skill_dir)
matched = []
for family in ARTIFACT_FAMILIES:
score, hits = match_family(text, family)
if score:
matched.append({**family, "score": score, "matched_keywords": hits})
if not matched:
fallback = next(item for item in ARTIFACT_FAMILIES if item["key"] == "report")
matched = [{**fallback, "score": 0, "matched_keywords": ["general-artifact"]}]
matched = sorted(matched, key=lambda item: item["score"], reverse=True)[:5]
primary = matched[0]
layout_patterns = []
gates = []
for item in matched:
layout_patterns.extend(item["layout"])
gates.extend(item["quality_gates"])
anti_patterns = [
"Do not copy Kami's fixed parchment background as a default.",
"Do not use generic purple gradients, glass cards, or stock SaaS hero sections unless the content calls for them.",
"Do not let Markdown tables become the default shape for every comparison or explanation.",
"Do not turn reviewer evidence into user-facing clutter.",
"Do not invent screenshots, citations, charts, or UI states.",
]
return {
"skill_name": skill_dir.name,
"design_system": design_system_name(matched),
"primary_artifact": {
"key": primary["key"],
"label": primary["label"],
"direction": primary["direction"],
"matched_keywords": primary["matched_keywords"],
},
"artifact_families": [
{
"key": item["key"],
"label": item["label"],
"score": item["score"],
"matched_keywords": item["matched_keywords"],
"direction": item["direction"],
}
for item in matched
],
"layout_patterns": dedupe(layout_patterns, 8),
"design_tokens": DESIGN_TOKENS,
"quality_gates": dedupe(gates, 8),
"anti_patterns": anti_patterns,
"reviewer_note": "Use this profile to judge whether the generated artifacts feel designed for their job, not merely rendered.",
}
def render_markdown(summary: dict) -> str:
lines = [
"# Artifact Design Profile",
"",
f"Skill: `{summary['skill_name']}`",
f"Design system: `{summary['design_system']}`",
"",
"## Primary Artifact Direction",
"",
f"**{summary['primary_artifact']['label']}**",
"",
summary["primary_artifact"]["direction"],
"",
"## Matched Artifact Families",
"",
]
for family in summary["artifact_families"]:
lines.extend(
[
f"### {family['label']}",
f"- Matched keywords: {', '.join(family['matched_keywords'])}",
f"- Score: `{family['score']}`",
f"- Direction: {family['direction']}",
"",
]
)
lines.extend(["## Layout Patterns To Prefer", ""])
for item in summary["layout_patterns"]:
lines.append(f"- {item}")
lines.extend(["", "## Design Tokens", ""])
for key, values in summary["design_tokens"].items():
lines.append(f"### {key.title()}")
for value in values:
lines.append(f"- {value}")
lines.append("")
lines.extend(["## Quality Gates", ""])
for item in summary["quality_gates"]:
lines.append(f"- {item}")
lines.extend(["", "## Anti-Patterns", ""])
for item in summary["anti_patterns"]:
lines.append(f"- {item}")
lines.extend(["", "## Reviewer Note", "", summary["reviewer_note"], ""])
return "\n".join(lines).strip() + "\n"
def render_artifact_design_profile(
skill_dir: Path,
output_md: Path | None = None,
output_json: Path | None = None,
) -> dict:
skill_dir = skill_dir.resolve()
reports_dir = skill_dir / "reports"
reports_dir.mkdir(parents=True, exist_ok=True)
output_md = output_md or reports_dir / "artifact-design-profile.md"
output_json = output_json or reports_dir / "artifact-design-profile.json"
summary = build_summary(skill_dir)
output_md.write_text(render_markdown(summary), encoding="utf-8")
output_json.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
return {
"ok": True,
"skill_dir": str(skill_dir),
"artifacts": {
"markdown": str(output_md),
"json": str(output_json),
},
"summary": summary,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Render artifact design direction and visual quality gates for a skill package.")
parser.add_argument("skill_dir", nargs="?", default=".")
parser.add_argument("--output-md")
parser.add_argument("--output-json")
args = parser.parse_args()
result = render_artifact_design_profile(
Path(args.skill_dir),
Path(args.output_md).resolve() if args.output_md else None,
Path(args.output_json).resolve() if args.output_json else None,
)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()