From cdcee4a748dbf067eb20b1b528a7b1e251e93fcb Mon Sep 17 00:00:00 2001 From: yaojingang Date: Mon, 6 Apr 2026 11:07:10 +0800 Subject: [PATCH] feat: add visual skill overview reports --- .gitignore | 2 + Makefile | 7 +- README.md | 5 +- docs/README.fr-FR.md | 3 +- docs/README.ja-JP.md | 3 +- docs/README.ru-RU.md | 3 +- docs/README.zh-CN.md | 3 +- scripts/init_skill.py | 89 +++++- scripts/render_skill_overview.py | 484 +++++++++++++++++++++++++++++++ scripts/yao.py | 20 +- tests/verify_skill_overview.py | 63 ++++ tests/verify_yao_cli.py | 10 +- 12 files changed, 676 insertions(+), 16 deletions(-) create mode 100644 scripts/render_skill_overview.py create mode 100644 tests/verify_skill_overview.py diff --git a/.gitignore b/.gitignore index 16cf22f1..0f853810 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ __pycache__/ tests/tmp/ tests/tmp_snapshot/ tests/tmp_cli/ +tests/tmp_skill_overview/ +tests/tmp_skill_preview/ reports/release_snapshots/ diff --git a/Makefile b/Makefile index 15b4342b..a605e593 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PYTHON ?= python3 -.PHONY: eval eval-suite route-scorecard route-confusion-check description-optimization judge-blind-eval description-optimization-check promotion-check yao-cli-check description-drift-history iteration-ledger results-panel regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check snapshot-check validate lint governance-check resource-boundary-check quality-check test clean +.PHONY: eval eval-suite route-scorecard route-confusion-check description-optimization judge-blind-eval description-optimization-check promotion-check yao-cli-check skill-overview-check description-drift-history iteration-ledger results-panel regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check snapshot-check validate lint governance-check resource-boundary-check quality-check test clean eval: $(PYTHON) scripts/trigger_eval.py --description-file evals/improved_description.txt --cases evals/trigger_cases.json --baseline-description-file evals/baseline_description.txt @@ -29,6 +29,9 @@ promotion-check: yao-cli-check: $(PYTHON) tests/verify_yao_cli.py +skill-overview-check: + $(PYTHON) tests/verify_skill_overview.py + description-drift-history: $(PYTHON) scripts/render_description_drift_history.py @@ -77,7 +80,7 @@ resource-boundary-check: quality-check: $(PYTHON) tests/verify_quality_checks.py -test: eval eval-suite route-scorecard route-confusion-check description-optimization description-optimization-check promotion-check yao-cli-check description-drift-history iteration-ledger regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check snapshot-check validate lint governance-check resource-boundary-check quality-check +test: eval eval-suite route-scorecard route-confusion-check description-optimization description-optimization-check promotion-check yao-cli-check skill-overview-check description-drift-history iteration-ledger regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check snapshot-check validate lint governance-check resource-boundary-check quality-check clean: rm -rf dist tests/tmp tests/tmp_snapshot diff --git a/README.md b/README.md index 04714c1d..837e3263 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ It turns rough workflows, transcripts, prompts, notes, and runbooks into reusabl - a clear trigger surface - a lean `SKILL.md` - optional references, scripts, and evals +- a generated visual HTML overview for each newly initialized skill - neutral source metadata plus client-specific adapters - governance, promotion, and portability checks built into the default flow @@ -51,8 +52,8 @@ Read it in 10 seconds: Or use the unified authoring CLI: ```bash -python3 scripts/yao.py validate . -python3 scripts/yao.py workspace-flow --target root --label first-pass +python3 scripts/yao.py init my-skill --description "Describe what the skill does." +python3 scripts/yao.py skill-report my-skill python3 scripts/yao.py package . --platform generic --output-dir dist ``` diff --git a/docs/README.fr-FR.md b/docs/README.fr-FR.md index 53d09920..654d2ab7 100644 --- a/docs/README.fr-FR.md +++ b/docs/README.fr-FR.md @@ -9,6 +9,7 @@ Il transforme des workflows bruts, des transcripts, des prompts, des notes et de - une surface de déclenchement claire - un `SKILL.md` léger - des references, scripts et evals optionnels +- un rapport HTML minimaliste en fond blanc généré automatiquement pour chaque nouveau skill - des métadonnées sources neutres et des adaptateurs spécifiques au client - des contrôles de gouvernance, de promotion et de portabilité intégrés au flux standard @@ -36,7 +37,7 @@ Lecture en 10 secondes : 1. Décrivez le workflow, l'ensemble de prompts ou la tâche répétée que vous voulez transformer en skill. 2. Utilisez `yao-meta-skill` pour générer ou améliorer le paquet en mode scaffold, production ou library. -3. Exécutez `context_sizer.py`, `trigger_eval.py` et `cross_packager.py` selon le besoin pour valider et exporter le résultat. +3. Chaque nouveau skill reçoit aussi `reports/skill-overview.html`, pour comprendre rapidement son architecture, sa logique, son usage et ses points forts. ## Results diff --git a/docs/README.ja-JP.md b/docs/README.ja-JP.md index 4ad42185..b7e6faf3 100644 --- a/docs/README.ja-JP.md +++ b/docs/README.ja-JP.md @@ -9,6 +9,7 @@ - 明確なトリガー面 - 軽量な `SKILL.md` - 必要に応じた references、scripts、evals +- 新しい skill ごとに、白背景の簡潔な HTML overview を自動生成 - 中立的なソースメタデータとクライアント別アダプタ - ガバナンス、昇格判定、portability チェックを標準フローに内蔵 @@ -36,7 +37,7 @@ flowchart LR 1. skill 化したい workflow、prompt 集合、または反復タスクを説明します。 2. `yao-meta-skill` を使って scaffold、production、library のいずれかのモードでパッケージを生成または改善します。 -3. 必要に応じて `context_sizer.py`、`trigger_eval.py`、`cross_packager.py` を実行し、検証と出力を行います。 +3. 新しく作成した skill には `reports/skill-overview.html` が付き、構造、ロジック、使い方、強みをすぐ確認できます。 ## Results diff --git a/docs/README.ru-RU.md b/docs/README.ru-RU.md index bc47d094..36e67ab9 100644 --- a/docs/README.ru-RU.md +++ b/docs/README.ru-RU.md @@ -9,6 +9,7 @@ - понятной поверхностью срабатывания - компактным `SKILL.md` - необязательными references, scripts и evals +- автоматически создаваемым минималистичным HTML-обзором на белом фоне для каждого нового skill - нейтральными исходными метаданными и клиентскими адаптерами - встроенными проверками governance, promotion и portability в стандартном потоке @@ -36,7 +37,7 @@ flowchart LR 1. Опишите workflow, набор prompts или повторяющуюся задачу, которую хотите превратить в skill. 2. Используйте `yao-meta-skill`, чтобы сгенерировать или улучшить пакет в режиме scaffold, production или library. -3. При необходимости запустите `context_sizer.py`, `trigger_eval.py` и `cross_packager.py`, чтобы проверить и экспортировать результат. +3. Каждый новый skill также получает `reports/skill-overview.html`, чтобы быстро понять архитектуру, логику, способ использования и сильные стороны. ## Results diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 6eec92a1..33f2ee69 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -9,6 +9,7 @@ - 清晰的触发面 - 精简的 `SKILL.md` - 可选的 references、scripts 和 evals +- 新建 skill 时自动生成一份极简白底 HTML 可视化说明 - 中性的源元数据以及面向不同客户端的适配层 - 内建的治理、晋升和 portability 检查 @@ -36,7 +37,7 @@ flowchart LR 1. 先描述你想沉淀成 skill 的 workflow、prompt 集合或重复任务。 2. 使用 `yao-meta-skill` 以 scaffold、production 或 library 模式生成或改进 skill 包。 -3. 按需要运行 `context_sizer.py`、`trigger_eval.py` 和 `cross_packager.py` 来检查并导出结果。 +3. 新建 skill 后,会默认附带 `reports/skill-overview.html`,方便快速理解架构、逻辑、使用方法和优势。 ## Results diff --git a/scripts/init_skill.py b/scripts/init_skill.py index dc8fcbbe..b4df9816 100644 --- a/scripts/init_skill.py +++ b/scripts/init_skill.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 import argparse +import json +from datetime import date from pathlib import Path +from render_skill_overview import render_skill_overview + SKILL_TEMPLATE = """--- name: {name} @@ -18,10 +22,33 @@ description: {description} """ +README_TEMPLATE = """# {title} + +## What It Does + +`{name}` is a reusable skill package for this job: + +> {description} + +## How To Use + +1. Load the skill through `SKILL.md`. +2. Follow the workflow steps in `SKILL.md`. +3. Check `reports/skill-overview.html` if you want a fast visual explanation of the package. + +## Package Map + +- `SKILL.md`: trigger and workflow entrypoint +- `agents/interface.yaml`: portable interface metadata +- `manifest.json`: lifecycle and packaging metadata +- `reports/skill-overview.html`: visual overview report +""" + + INTERFACE_TEMPLATE = """interface: display_name: "{title}" short_description: "{short_description}" - default_prompt: "Use ${name} to ..." + default_prompt: "Use ${name} when you need to {default_prompt}." compatibility: canonical_format: "agent-skills" adapter_targets: @@ -45,6 +72,31 @@ compatibility: """ +def build_manifest(name: str) -> dict: + return { + "name": name, + "version": "0.1.0", + "owner": "Yao Team", + "updated_at": str(date.today()), + "status": "active", + "maturity_tier": "scaffold", + "lifecycle_stage": "scaffold", + "context_budget_tier": "scaffold", + "review_cadence": "ad-hoc", + "target_platforms": [ + "openai", + "claude", + "generic", + "agent-skills-compatible", + ], + "factory_components": [ + "references", + "scripts", + "reports", + ], + } + + def main() -> None: parser = argparse.ArgumentParser(description="Initialize a minimal skill package.") parser.add_argument("name", help="skill folder and frontmatter name") @@ -58,12 +110,41 @@ def main() -> None: (root / "agents").mkdir(parents=True, exist_ok=True) (root / "references").mkdir(exist_ok=True) (root / "scripts").mkdir(exist_ok=True) + (root / "reports").mkdir(exist_ok=True) (root / "SKILL.md").write_text(SKILL_TEMPLATE.format(name=args.name, description=args.description, title=title), encoding="utf-8") - (root / "agents" / "interface.yaml").write_text( - INTERFACE_TEMPLATE.format(name=args.name, title=title, short_description=args.description[:80]), + (root / "README.md").write_text( + README_TEMPLATE.format(name=args.name, description=args.description, title=title), encoding="utf-8", ) - print(str(root)) + (root / "agents" / "interface.yaml").write_text( + INTERFACE_TEMPLATE.format( + name=args.name, + title=title, + short_description=args.description[:80], + default_prompt=args.description.rstrip("."), + ), + encoding="utf-8", + ) + (root / "manifest.json").write_text( + json.dumps(build_manifest(args.name), ensure_ascii=False, indent=2), + encoding="utf-8", + ) + overview = render_skill_overview(root) + print( + json.dumps( + { + "ok": True, + "root": str(root), + "artifacts": { + "readme": str(root / "README.md"), + "manifest": str(root / "manifest.json"), + **overview["artifacts"], + }, + }, + ensure_ascii=False, + indent=2, + ) + ) if __name__ == "__main__": diff --git a/scripts/render_skill_overview.py b/scripts/render_skill_overview.py new file mode 100644 index 00000000..776784f9 --- /dev/null +++ b/scripts/render_skill_overview.py @@ -0,0 +1,484 @@ +#!/usr/bin/env python3 +import argparse +import html +import json +import re +from datetime import date +from pathlib import Path + +try: + import yaml +except ImportError: # pragma: no cover + yaml = None + + +KNOWN_ENTRIES = [ + ("SKILL.md", "Skill entrypoint"), + ("README.md", "Human-readable usage guide"), + ("agents/interface.yaml", "Neutral interface metadata"), + ("manifest.json", "Lifecycle and portability metadata"), + ("references", "Extended guidance and reusable notes"), + ("scripts", "Deterministic helpers or local tooling"), + ("evals", "Trigger and quality checks"), + ("reports", "Generated evidence and overview artifacts"), +] + + +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: + data = yaml.safe_load(frontmatter_text) or {} + return data if isinstance(data, 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_yaml(path: Path) -> dict: + if not path.exists(): + return {} + text = path.read_text(encoding="utf-8") + if yaml is not None: + payload = yaml.safe_load(text) or {} + return payload if isinstance(payload, dict) else {} + return {} + + +def load_json(path: Path) -> dict: + if not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + +def extract_title(body: str, fallback: str) -> str: + for line in body.splitlines(): + if line.startswith("# "): + return line[2:].strip() + return fallback + + +def parse_sections(body: str) -> dict[str, str]: + sections: dict[str, list[str]] = {} + current = "_preamble" + sections[current] = [] + for line in body.splitlines(): + if line.startswith("## "): + current = line[3:].strip() + sections[current] = [] + continue + sections[current].append(line) + return {name: "\n".join(lines).strip() for name, lines in sections.items()} + + +def extract_list_items(text: str) -> list[str]: + items = [] + for line in text.splitlines(): + stripped = line.strip() + if not stripped: + continue + ordered = re.match(r"^\d+\.\s+(.*)$", stripped) + bullet = re.match(r"^[-*]\s+(.*)$", stripped) + match = ordered or bullet + if match: + items.append(match.group(1).strip()) + return items + + +def summarize_logic(sections: dict[str, str]) -> list[str]: + for key in ("Compact Workflow", "Workflow", "How It Works", "Logic", "Quick Start"): + if key in sections: + items = extract_list_items(sections[key]) + if items: + return items[:5] + preamble_items = extract_list_items(sections.get("_preamble", "")) + return preamble_items[:5] + + +def summarize_usage(sections: dict[str, str], default_prompt: str, description: str) -> list[str]: + usage = [] + for key in ("How To Use", "Quick Start", "Usage", "Runbook"): + if key in sections: + usage = extract_list_items(sections[key]) + if usage: + return usage[:5] + if default_prompt: + usage.append(default_prompt) + usage.append(f"Use this skill when the request matches: {description}") + return usage[:4] + + +def package_entries(skill_dir: Path) -> list[dict]: + items = [] + for rel_path, label in KNOWN_ENTRIES: + target = skill_dir / rel_path + if target.exists(): + kind = "folder" if target.is_dir() else "file" + items.append({"path": rel_path, "label": label, "kind": kind}) + return items + + +def derive_strengths(skill_dir: Path, metadata: dict) -> list[str]: + strengths = ["Lean trigger surface anchored in frontmatter description."] + if (skill_dir / "agents" / "interface.yaml").exists(): + strengths.append("Portable interface metadata is already packaged for adapter-based export.") + if (skill_dir / "references").exists() and any((skill_dir / "references").iterdir()): + strengths.append("Long guidance is separated into references so the entrypoint can stay compact.") + if (skill_dir / "scripts").exists() and any((skill_dir / "scripts").iterdir()): + strengths.append("Deterministic helpers are packaged with the skill instead of hidden in prompt text.") + if (skill_dir / "evals").exists() and any((skill_dir / "evals").iterdir()): + strengths.append("The package includes quality gates or trigger checks that can travel with the skill.") + if metadata.get("maturity_tier"): + strengths.append(f"Lifecycle metadata is explicit, with maturity tier set to `{metadata['maturity_tier']}`.") + return strengths[:5] + + +def card_items(interface_data: dict, logic_steps: list[str], package_map: list[dict], usage_steps: list[str], description: str) -> list[dict]: + compatibility = interface_data.get("compatibility", {}) + execution = compatibility.get("execution", {}) + activation = compatibility.get("activation", {}) + return [ + { + "title": "Trigger", + "body": description, + "meta": [ + f"Activation: {activation.get('mode', 'manual')}", + f"Context: {execution.get('context', 'inline')}", + ], + }, + { + "title": "Logic", + "body": logic_steps or ["Understand the request", "Execute the task", "Validate the result"], + "meta": [], + }, + { + "title": "Usage", + "body": usage_steps, + "meta": [], + }, + { + "title": "Package", + "body": [entry["path"] for entry in package_map[:6]], + "meta": [f"{len(package_map)} structured entries detected"], + }, + ] + + +def build_summary(skill_dir: Path) -> dict: + skill_text = (skill_dir / "SKILL.md").read_text(encoding="utf-8") + frontmatter, body = parse_frontmatter(skill_text) + interface_data = load_yaml(skill_dir / "agents" / "interface.yaml") + manifest = load_json(skill_dir / "manifest.json") + sections = parse_sections(body) + + name = frontmatter.get("name", skill_dir.name) + description = frontmatter.get("description", "No description found.") + title = extract_title(body, name.replace("-", " ").title()) + display_name = interface_data.get("interface", {}).get("display_name", title) + default_prompt = interface_data.get("interface", {}).get("default_prompt", "") + logic_steps = summarize_logic(sections) + usage_steps = summarize_usage(sections, default_prompt, description) + package_map = package_entries(skill_dir) + strengths = derive_strengths(skill_dir, manifest) + + return { + "name": name, + "title": title, + "display_name": display_name, + "description": description, + "logic_steps": logic_steps, + "usage_steps": usage_steps, + "package_map": package_map, + "strengths": strengths, + "cards": card_items(interface_data, logic_steps, package_map, usage_steps, description), + "metadata": { + "canonical_format": interface_data.get("compatibility", {}).get("canonical_format", "agent-skills"), + "targets": interface_data.get("compatibility", {}).get("adapter_targets", []), + "maturity_tier": manifest.get("maturity_tier", "scaffold"), + "updated_at": manifest.get("updated_at", str(date.today())), + }, + } + + +def render_card_body(card: dict) -> str: + body = card["body"] + if isinstance(body, list): + items = "".join(f"
  • {html.escape(str(item))}
  • " for item in body) + body_html = f"
      {items}
    " + else: + body_html = f"

    {html.escape(str(body))}

    " + meta = "".join(f"{html.escape(str(item))}" for item in card.get("meta", [])) + meta_html = f"
    {meta}
    " if meta else "" + return f"

    {html.escape(card['title'])}

    {body_html}{meta_html}
    " + + +def render_html(summary: dict) -> str: + target_badges = "".join(f"{html.escape(str(target))}" for target in summary["metadata"]["targets"]) + cards_html = "".join(render_card_body(card) for card in summary["cards"]) + strengths_html = "".join(f"
  • {html.escape(item)}
  • " for item in summary["strengths"]) + package_rows = "".join( + f"{html.escape(item['path'])}{html.escape(item['label'])}{html.escape(item['kind'])}" + for item in summary["package_map"] + ) + + return f""" + + + + + {html.escape(summary['display_name'])} Overview + + + +
    +

    Skill Overview

    +

    {html.escape(summary["display_name"])}

    +

    {html.escape(summary["description"])}

    +
    + Skill name: {html.escape(summary["name"])} + Maturity: {html.escape(summary["metadata"]["maturity_tier"])} + Format: {html.escape(summary["metadata"]["canonical_format"])} + Updated: {html.escape(summary["metadata"]["updated_at"])} +
    +
    {target_badges}
    + +
    +
    +
    +

    Architecture

    +

    One clear flow: define the boundary, choose the right structure, run the checks, then ship a reusable package.

    +
    +
    {cards_html}
    +
    +
    + +
    +
    +
    +

    Why It Works

    +

    These are the strengths the package already makes explicit instead of leaving hidden in prompt text.

    +
    +
      {strengths_html}
    +
    +
    + +
    +
    +
    +

    Package Map

    +

    Use this map to understand what lives where before you read the whole package.

    +
    +
    + + + + + {package_rows} +
    PathRoleType
    +
    +
    +
    +
    + + +""" + + +def render_skill_overview(skill_dir: Path, output_html: 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_html = output_html or reports_dir / "skill-overview.html" + output_json = output_json or reports_dir / "skill-overview.json" + + summary = build_summary(skill_dir) + output_html.write_text(render_html(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": { + "html": str(output_html), + "json": str(output_json), + }, + "summary": summary, + } + + +def main() -> None: + parser = argparse.ArgumentParser(description="Render a visual overview report for a skill package.") + parser.add_argument("skill_dir", nargs="?", default=".") + parser.add_argument("--output-html") + parser.add_argument("--output-json") + args = parser.parse_args() + + result = render_skill_overview( + Path(args.skill_dir), + Path(args.output_html).resolve() if args.output_html 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() diff --git a/scripts/yao.py b/scripts/yao.py index ea36ec74..8bbbe5a1 100644 --- a/scripts/yao.py +++ b/scripts/yao.py @@ -111,7 +111,7 @@ def command_init(args: argparse.Namespace) -> int: *(["--title", args.title] if args.title else []), ], ) - print(json.dumps(result, ensure_ascii=False, indent=2)) + print(json.dumps(result["payload"] if result["payload"] is not None else result, ensure_ascii=False, indent=2)) return 0 if result["ok"] else 2 @@ -213,6 +213,18 @@ def command_report(args: argparse.Namespace) -> int: return 0 if report["ok"] else 2 +def command_skill_report(args: argparse.Namespace) -> int: + skill_dir = str(Path(args.skill_dir).resolve()) + cmd = [skill_dir] + if args.output_html: + cmd.extend(["--output-html", args.output_html]) + if args.output_json: + cmd.extend(["--output-json", args.output_json]) + result = run_script("render_skill_overview.py", cmd) + print(json.dumps(result["payload"] if result["payload"] is not None else result, ensure_ascii=False, indent=2)) + return 0 if result["ok"] else 2 + + def command_review(args: argparse.Namespace) -> int: target_name = resolve_promotion_target(args.target) bundle_dir = ROOT / "reports" / "iteration_bundles" / target_name @@ -416,6 +428,12 @@ def build_parser() -> argparse.ArgumentParser: report_cmd.add_argument("--refresh-optimization", action="store_true") report_cmd.set_defaults(func=command_report) + skill_report_cmd = subparsers.add_parser("skill-report", help="Render a visual overview report for a skill package.") + skill_report_cmd.add_argument("skill_dir", nargs="?", default=".") + skill_report_cmd.add_argument("--output-html") + skill_report_cmd.add_argument("--output-json") + skill_report_cmd.set_defaults(func=command_skill_report) + package_cmd = subparsers.add_parser("package", help="Export compatibility artifacts for selected targets.") package_cmd.add_argument("skill_dir", nargs="?", default=".") package_cmd.add_argument("--platform", action="append") diff --git a/tests/verify_skill_overview.py b/tests/verify_skill_overview.py new file mode 100644 index 00000000..2b66a80e --- /dev/null +++ b/tests/verify_skill_overview.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +import json +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +CLI = ROOT / "scripts" / "yao.py" + + +def run(*args: str) -> dict: + proc = subprocess.run( + [sys.executable, str(CLI), *args], + cwd=ROOT, + capture_output=True, + text=True, + ) + payload = json.loads(proc.stdout) + return { + "ok": proc.returncode == 0, + "returncode": proc.returncode, + "payload": payload, + "stderr": proc.stderr, + } + + +def main() -> None: + tmp_root = ROOT / "tests" / "tmp_skill_overview" + if tmp_root.exists(): + subprocess.run(["rm", "-rf", str(tmp_root)], check=True) + tmp_root.mkdir(parents=True, exist_ok=True) + + init_result = run( + "init", + "skill-overview-demo", + "--description", + "Turn rough requests into a compact reusable demo skill.", + "--output-dir", + str(tmp_root), + ) + assert init_result["ok"], init_result + + created = tmp_root / "skill-overview-demo" + assert (created / "README.md").exists(), created + assert (created / "manifest.json").exists(), created + assert (created / "reports" / "skill-overview.html").exists(), created + assert (created / "reports" / "skill-overview.json").exists(), created + + rerender_result = run("skill-report", str(created)) + assert rerender_result["ok"], rerender_result + assert rerender_result["payload"]["artifacts"]["html"].endswith("reports/skill-overview.html"), rerender_result + + report_html = (created / "reports" / "skill-overview.html").read_text(encoding="utf-8") + assert "Skill Overview" in report_html, report_html[:200] + assert "Architecture" in report_html, report_html[:400] + assert "Why It Works" in report_html, report_html[:600] + + print(json.dumps({"ok": True}, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tests/verify_yao_cli.py b/tests/verify_yao_cli.py index 3b0c4e98..d39c84d2 100644 --- a/tests/verify_yao_cli.py +++ b/tests/verify_yao_cli.py @@ -33,15 +33,19 @@ def main() -> None: init_result = run("init", "cli-demo-skill", "--description", "CLI demo skill.", "--output-dir", str(tmp_root)) assert init_result["ok"], init_result - created = Path(init_result["payload"]["stdout"].strip()) if "stdout" in init_result["payload"] else tmp_root / "cli-demo-skill" - if not created.exists(): - created = tmp_root / "cli-demo-skill" + created = Path(init_result["payload"]["root"]) assert (created / "SKILL.md").exists(), created + assert (created / "README.md").exists(), created + assert (created / "reports" / "skill-overview.html").exists(), created validate_result = run("validate", str(created)) assert validate_result["ok"], validate_result assert len(validate_result["payload"]["steps"]) == 4, validate_result + skill_report_result = run("skill-report", str(created)) + assert skill_report_result["ok"], skill_report_result + assert skill_report_result["payload"]["artifacts"]["html"].endswith("reports/skill-overview.html"), skill_report_result + optimize_result = run("optimize-description", "--target", "root") assert optimize_result["ok"], optimize_result assert optimize_result["payload"]["winner"]["label"] == "Current", optimize_result