feat: add visual skill overview reports
This commit is contained in:
@@ -5,4 +5,6 @@ __pycache__/
|
||||
tests/tmp/
|
||||
tests/tmp_snapshot/
|
||||
tests/tmp_cli/
|
||||
tests/tmp_skill_overview/
|
||||
tests/tmp_skill_preview/
|
||||
reports/release_snapshots/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+85
-4
@@ -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__":
|
||||
|
||||
@@ -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"<li>{html.escape(str(item))}</li>" for item in body)
|
||||
body_html = f"<ol>{items}</ol>"
|
||||
else:
|
||||
body_html = f"<p>{html.escape(str(body))}</p>"
|
||||
meta = "".join(f"<span>{html.escape(str(item))}</span>" for item in card.get("meta", []))
|
||||
meta_html = f"<div class='meta'>{meta}</div>" if meta else ""
|
||||
return f"<div class='card'><h3>{html.escape(card['title'])}</h3>{body_html}{meta_html}</div>"
|
||||
|
||||
|
||||
def render_html(summary: dict) -> str:
|
||||
target_badges = "".join(f"<span>{html.escape(str(target))}</span>" for target in summary["metadata"]["targets"])
|
||||
cards_html = "".join(render_card_body(card) for card in summary["cards"])
|
||||
strengths_html = "".join(f"<li>{html.escape(item)}</li>" for item in summary["strengths"])
|
||||
package_rows = "".join(
|
||||
f"<tr><td>{html.escape(item['path'])}</td><td>{html.escape(item['label'])}</td><td>{html.escape(item['kind'])}</td></tr>"
|
||||
for item in summary["package_map"]
|
||||
)
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{html.escape(summary['display_name'])} Overview</title>
|
||||
<style>
|
||||
:root {{
|
||||
--text: #101010;
|
||||
--muted: #666666;
|
||||
--line: #e8e6e1;
|
||||
--soft: #f7f6f3;
|
||||
--white: #ffffff;
|
||||
}}
|
||||
* {{ box-sizing: border-box; }}
|
||||
body {{
|
||||
margin: 0;
|
||||
background: var(--white);
|
||||
color: var(--text);
|
||||
font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.wrap {{
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px 80px;
|
||||
}}
|
||||
.eyebrow {{
|
||||
margin: 0 0 12px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}}
|
||||
h1, h2, h3 {{
|
||||
margin: 0;
|
||||
font-family: "Iowan Old Style", "Palatino Linotype", Georgia, serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
}}
|
||||
h1 {{
|
||||
font-size: clamp(2.4rem, 5vw, 4.4rem);
|
||||
line-height: 1.02;
|
||||
max-width: 10ch;
|
||||
}}
|
||||
.lead {{
|
||||
max-width: 760px;
|
||||
margin: 18px 0 0;
|
||||
font-size: 1.05rem;
|
||||
color: #222;
|
||||
}}
|
||||
.hero-meta, .badges {{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 22px;
|
||||
}}
|
||||
.hero-meta span, .badges span {{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
background: var(--white);
|
||||
font-size: 13px;
|
||||
}}
|
||||
section {{
|
||||
padding-top: 28px;
|
||||
margin-top: 28px;
|
||||
border-top: 1px solid var(--line);
|
||||
}}
|
||||
.section-head {{
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 240px) minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}}
|
||||
.section-head p {{
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
max-width: 40ch;
|
||||
}}
|
||||
.cards {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}}
|
||||
.card {{
|
||||
min-height: 220px;
|
||||
padding: 22px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 20px;
|
||||
background: var(--white);
|
||||
}}
|
||||
.card h3 {{
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 14px;
|
||||
}}
|
||||
.card p, .card li {{
|
||||
color: #222;
|
||||
margin: 0;
|
||||
}}
|
||||
.card ol, .card ul {{
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}}
|
||||
.meta {{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}}
|
||||
.meta span {{
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--soft);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}}
|
||||
.strengths {{
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}}
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}}
|
||||
th, td {{
|
||||
padding: 14px 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--line);
|
||||
vertical-align: top;
|
||||
}}
|
||||
th {{
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}}
|
||||
@media (max-width: 900px) {{
|
||||
.section-head {{ grid-template-columns: 1fr; }}
|
||||
.cards {{ grid-template-columns: 1fr; }}
|
||||
.card {{ min-height: auto; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<p class="eyebrow">Skill Overview</p>
|
||||
<h1>{html.escape(summary["display_name"])}</h1>
|
||||
<p class="lead">{html.escape(summary["description"])}</p>
|
||||
<div class="hero-meta">
|
||||
<span>Skill name: {html.escape(summary["name"])}</span>
|
||||
<span>Maturity: {html.escape(summary["metadata"]["maturity_tier"])}</span>
|
||||
<span>Format: {html.escape(summary["metadata"]["canonical_format"])}</span>
|
||||
<span>Updated: {html.escape(summary["metadata"]["updated_at"])}</span>
|
||||
</div>
|
||||
<div class="badges">{target_badges}</div>
|
||||
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>Architecture</h2>
|
||||
<p>One clear flow: define the boundary, choose the right structure, run the checks, then ship a reusable package.</p>
|
||||
</div>
|
||||
<div class="cards">{cards_html}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>Why It Works</h2>
|
||||
<p>These are the strengths the package already makes explicit instead of leaving hidden in prompt text.</p>
|
||||
</div>
|
||||
<ul class="strengths">{strengths_html}</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>Package Map</h2>
|
||||
<p>Use this map to understand what lives where before you read the whole package.</p>
|
||||
</div>
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Path</th><th>Role</th><th>Type</th></tr>
|
||||
</thead>
|
||||
<tbody>{package_rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
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()
|
||||
+19
-1
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user