#!/usr/bin/env python3 import argparse import html import json from pathlib import Path from skill_report_charts import render_chart_set from skill_report_i18n import ( KIND_ZH, LABEL_EN, PACKAGE_LABEL_ZH, bi_item, bi_span, en_for, mode_zh, readable_description_zh, ) from skill_report_layout import render_language_switch, render_report_nav, skill_overview_css, skill_overview_script from skill_report_model import REPORT_NAV_V2, build_report_model def render_list(items: list[str], class_name: str = "list") -> str: if not items: return f'' return f'" def render_ordered_steps(items: list[str], class_name: str = "step-list") -> str: if not items: return f'
  1. {bi_span("暂无步骤。", "No steps yet.")}
' return f'
    ' + "".join(f"
  1. {bi_item(str(item))}
  2. " for item in items) + "
" def render_metric_cards(scorecard: dict) -> str: cards = [] for key, item in scorecard.items(): reasons = render_list(item.get("reasons", [])[:3], "compact-list") cards.append( "
" "
" f"{bi_item(str(item.get('label', key)))}" f"{html.escape(str(item.get('score', 'n/a')))}" "
" f"
{reasons}
" "
" ) return "".join(cards) def render_metric_summary(scorecard: dict) -> str: items = [] for key, item in scorecard.items(): score = int(item.get("score", 0)) label = str(item.get("label", key)) reason = str(item.get("reasons", [""])[0]) if score >= 85: verdict = "稳定" verdict_en = "Stable" elif score >= 70: verdict = "可用" verdict_en = "Usable" else: verdict = "关注" verdict_en = "Watch" items.append( "
  • " f"{bi_span(verdict, verdict_en)}" f"{bi_item(label)}" f"{score}" f"{bi_item(reason)}" "
  • " ) return "
      " + "".join(items) + "
    " def render_audit_rows(items: list[dict]) -> str: rows = [] for item in items: name = str(item.get("name", item.get("label", "项目"))) response = str(item.get("response", item.get("kind", ""))) rows.append( "" f"{bi_span(name, LABEL_EN.get(name, name))}" f"{bi_item(str(item.get('signal', item.get('body', ''))))}" f"{bi_span(response, LABEL_EN.get(response, response))}" "" ) return "".join(rows) WORLD_CLASS_CHECK_COPY = { "Provider model run": ("提供商实跑", "Provider model run"), "Token usage observed": ("Token 用量", "Token usage observed"), "No pending decisions": ("无待判定", "No pending decisions"), "Judgments complete": ("盲评完成", "Judgments complete"), "Native enforcement": ("原生执行", "Native enforcement"), "External events": ("外部事件", "External events"), "Adoption sample": ("采用样本", "Adoption sample"), } def render_world_class_check(check: object) -> str: if isinstance(check, dict): label_en = str(check.get("label_en") or check.get("label") or "") label_zh = str(check.get("label_zh") or "") else: label_en = str(check) label_zh = "" label_zh, label_en = WORLD_CLASS_CHECK_COPY.get(label_en, (label_zh or label_en, label_en)) return f"
  • {bi_span(label_zh, label_en)}
  • " def render_world_class_readiness(readiness: dict) -> str: if not readiness: return "" kpis = [ ( "待补证据", "Pending", readiness.get("pending_count", 0), "仍需外部或人工证据接受。", "External or human evidence still needs acceptance.", ), ( "已接受", "Accepted", readiness.get("accepted_count", 0), "已通过 source check 与提交契约。", "Passed source checks and submission contract.", ), ( "源检查", "Source Checks", f"{readiness.get('source_pass_count', 0)} / {readiness.get('source_check_count', 0)}", "通过数 / 总检查数。", "Passed checks / total checks.", ), ] kpi_html = "".join( "
    " f"{bi_span(zh, en)}" f"{html.escape(str(value))}" f"{bi_span(note_zh, note_en)}" "
    " for zh, en, value, note_zh, note_en in kpis ) entries = readiness.get("entries", []) if entries: blocks = [] for item in entries: blocked_checks = [ str(check).strip() for check in item.get("blocked_checks", []) if str(check).strip() ] if blocked_checks: checks_html = ( "" ) else: checks_html = ( "

    " f"{bi_span('当前没有阻塞检查。', 'No blocked checks right now.')}" "

    " ) blocks.append( "
    " "
    " f"{bi_span(str(item.get('category_zh', '外部证据')), str(item.get('category_en', 'External evidence')))}" f"

    {bi_span(str(item.get('label_zh', item.get('key', '证据项'))), str(item.get('label_en', item.get('key', 'Evidence item'))))}

    " "
    " f"

    {bi_span(str(item.get('summary_zh', '仍待补充证据。')), str(item.get('summary_en', 'Evidence is still pending.')))}

    " f"
    {bi_span('阻塞检查', 'Blocked Checks')}
    " f"{checks_html}" "
    " ) entry_html = "".join(blocks) else: entry_html = ( "
    " f"

    {bi_span('尚未生成 world-class ledger;这里只保留反过度承诺提示。', 'No world-class ledger has been generated; this panel keeps the anti-overclaim guard visible.')}

    " "
    " ) status = "可宣称" if readiness.get("ready") else "证据待补" status_en = "Claim-ready" if readiness.get("ready") else "Evidence pending" return ( "
    " "
    " "
    " f"

    {bi_span('世界证据', 'World Evidence')}

    " f"

    {bi_span(str(readiness.get('conclusion_zh', '世界级证据状态未知。')), str(readiness.get('conclusion_en', 'World-class evidence status is unknown.')))}

    " "
    " f"{bi_span(status, status_en)}" "
    " f"
    {kpi_html}
    " f"
    {entry_html}
    " "
    " ) def render_score_strip(scorecard: dict) -> str: keys = ["completeness_score", "trigger_score", "evidence_score", "context_cost"] cards = [] for key in keys: item = scorecard.get(key) if not item: continue score = int(item.get("score", 0)) reason = item.get("reasons", [""])[0] cards.append( "
    " f"{bi_item(str(item.get('label', key)))}" f"{score}" f"" f"{bi_item(str(reason))}" "
    " ) return "".join(cards) def render_roadmap(items: list[dict]) -> str: blocks = [] for index, item in enumerate(items[:3], start=1): actions = render_list([str(action) for action in item.get("actions", [])], "compact-list") blocks.append( "
    " f"{bi_span(f'下一步 {index}', f'Next {index}')}" f"

    {bi_item(str(item.get('title', '升级方向')))}

    " f"

    {bi_item(str(item.get('why', '提升复用稳定性。')))}

    " f"{actions}" f"

    {bi_item(str(item.get('unlocks', '')))}

    " "
    " ) return "".join(blocks) def render_html(summary: dict) -> str: charts = render_chart_set(summary) nav_html = render_report_nav(REPORT_NAV_V2) language_switch = render_language_switch() skill = summary.get("skill_summary", {}) metadata = summary.get("metadata", {}) scorecard = summary.get("scorecard", {}) profile = summary.get("capability_profile", {}) contract = summary.get("contract_boundary", {}) quality = summary.get("quality_review", {}) risk = summary.get("risk_governance", {}) world_readiness = summary.get("world_class_readiness", {}) assets = summary.get("package_assets", {}) roadmap = summary.get("iteration_roadmap", {}) output_execution = summary.get("output_execution", {}) output_execution_summary = output_execution.get("summary", {}) output_review = summary.get("output_review_adjudication", {}) output_review_summary = output_review.get("summary", {}) report_contract = summary.get("report_contract", {}) html_report_path = str(report_contract.get("html_report") or "reports/skill-overview.html") open_report_message = f"创建完成后建议先打开 {html_report_path},再继续扩展包体。" hero_meta = [ (f"技能名称:{summary['name']}", f"Skill name: {summary['name']}"), (f"成熟度:{mode_zh(metadata.get('maturity_tier', 'scaffold'))}", f"Maturity: {metadata.get('maturity_tier', 'scaffold')}"), (f"格式:{mode_zh(metadata.get('canonical_format', 'agent-skills'))}", f"Format: {metadata.get('canonical_format', 'agent-skills')}"), (f"更新时间:{metadata.get('updated_at', '')}", f"Updated: {metadata.get('updated_at', '')}"), ] hero_meta_html = "".join(f"{bi_span(zh, en)}" for zh, en in hero_meta) target_badges = "".join(f"{bi_span(str(target), str(target))}" for target in metadata.get("targets", [])) score_strip = render_score_strip(scorecard) package_rows = "".join( ( "" f"{html.escape(str(item.get('path', '')))}" f"{bi_span(PACKAGE_LABEL_ZH.get(str(item.get('path', '')), str(item.get('label', ''))), str(item.get('label', '')))}" f"{bi_span(KIND_ZH.get(str(item.get('kind', '')), str(item.get('kind', ''))), str(item.get('kind', '')))}" "" ) for item in assets.get("entries", []) ) quality_rows = render_audit_rows( [{"name": "强项", "signal": item, "response": "保留并复用"} for item in quality.get("strengths", [])[:3]] + [{"name": "缺口", "signal": item, "response": "纳入下一轮修复"} for item in quality.get("gaps", [])[:3]] ) if output_review_summary: agreement = output_review_summary.get("agreement_rate") review_items = [ f"评审进度:{output_review_summary.get('judgment_count', 0)} / {output_review_summary.get('pair_count', 0)}", f"待评审:{output_review_summary.get('pending_count', 0)}", f"一致率:{agreement if agreement is not None else '暂无'}", f"非法决策:{output_review_summary.get('invalid_decision_count', 0)}", ] else: review_items = ["尚未生成盲评审定报告。"] if output_execution_summary: execution_items = [ f"变体运行:{output_execution_summary.get('variant_run_count', 0)}", f"模型执行:{output_execution_summary.get('model_executed_count', 0)}", f"记录样本:{output_execution_summary.get('recorded_fixture_count', 0)}", f"Token 估算:{output_execution_summary.get('token_estimated_count', 0)}", ] else: execution_items = ["尚未生成输出执行证据报告。"] capability_items = [ f"能力类型:{profile.get('task_family', 'Skill workflow')}", f"成熟度:{profile.get('maturity', 'scaffold')}", f"触发强度:{profile.get('trigger_strength', 'manual')}", f"复用范围:{profile.get('reuse_scope', '本地复用')}", ] trigger = contract.get("trigger", {}) risk_rows = render_audit_rows(risk.get("risks", [])) return f""" {html.escape(summary['name'])} Skill 生成审计报告
    {language_switch}

    {bi_span("YAO Skill 生成审计报告", "YAO Skill Generation Audit")}

    {html.escape(summary['name'])}

    {bi_span("技能审计报告", f"{summary['display_name']} Audit Report")}

    {bi_span("这份报告默认使用中文简体,把新 Skill 的定位、指标、原理、契约、质量、风险、资产和迭代路线整理为一份可审计的 HTML 报告。", en_for(str(summary["description"])))}

    {hero_meta_html}
    {target_badges}
    {score_strip}

    {bi_span("技能概述", "Overview")}

    {bi_span("先用一屏说明这个 Skill 是什么、给谁用、交付什么。", "A first-screen explanation of what this skill is, who it serves, and what it delivers.")}

    {bi_span("作用定位", "Role")}

    {render_list([summary["description"], skill.get("core_value", ""), f"交付结果:{', '.join(skill.get('deliverables', []))}"])}
    {charts["flow"]}

    {bi_span("总览指标", "Metrics")}

    {bi_span("分数来自本地文件和 reports 证据,缺失时明确标为证据不足。", "Scores are derived from package files and reports; missing inputs are shown as evidence gaps.")}

    {bi_span("指标判读", "Reading")}

    {bi_span("先看雷达图判断能力短板,再看每项分数的证据原因。分数不是装饰数字,必须和本地文件、reports 证据或证据不足提示对应。", "Read the radar first for weak spots, then inspect each score with its evidence. Scores must map to local files, reports, or explicit evidence gaps.")}

    {charts["radar"]}

    {bi_span("成熟度条", "Maturity Bar")}

    {bi_span("这里把每个指标压缩为状态、分数和第一条证据,先给出整体判断,再进入下方明细。", "This compresses each metric into status, score, and the first evidence item before the detailed cards below.")}

    {render_metric_summary(scorecard)}
    {bi_span("指标明细", "Metric Details")}
    {render_metric_cards(scorecard)}

    {bi_span("能力画像", "Capability")}

    {bi_span("判断这个 Skill 在能力地图中的位置和复用范围。", "Places this skill on a capability map and clarifies reuse scope.")}

    {charts["matrix"]}

    {bi_span("画像摘要", "Profile")}

    {render_list(capability_items)}

    {bi_span("原理结构", "Principle")}

    {bi_span("说明入口、参考、脚本、评估和报告如何组成一个稳定闭环。", "Explains how entrypoint, references, scripts, evals, and reports form a stable loop.")}

    {charts["layers"]}

    {bi_span("执行流程", "Execution Flow")}

    {render_ordered_steps(summary.get("logic_steps", []))}

    {bi_span("调用方式", "How To Use")}

    {render_ordered_steps(summary.get("usage_steps", []))}

    {bi_span("契约边界", "Contract")}

    {bi_span("把触发、输入、输出和排除场景放在同一屏。", "Keeps trigger, inputs, outputs, and exclusions on the same screen.")}

    {bi_span("触发描述", "Trigger")}

    {bi_span(readable_description_zh(str(trigger.get("description", ""))), en_for(str(trigger.get("description", ""))))}

    {bi_span("输入材料", "Inputs")}

    {render_list(contract.get("inputs", []))}

    {bi_span("输出结果", "Outputs")}

    {render_list(contract.get("outputs", []))}

    {bi_span("不应触发", "Should Not Trigger")}

    {render_list(contract.get("should_not_trigger", []))}

    {bi_span("质量评估", "Quality")}

    {bi_span("展示强项、缺口和建议,避免只给分不解释。", "Shows strengths, gaps, and recommendations instead of scores without explanation.")}

    {quality_rows}
    {bi_span("类型", "Type")}{bi_span("证据", "Evidence")}{bi_span("建议", "Action")}

    {bi_span("执行证据", "Execution Evidence")}

    {render_list(execution_items)}

    {bi_span("盲评审定", "Blind Adjudication")}

    {render_list(review_items)}

    {bi_span("评审原则", "Review Rule")}

    {render_list(["先记录 reviewer 对 A/B 的选择,再打开答案 key 计算一致率。", "缺少真实 reviewer 决策时只显示待评审,不伪造人工结论。"])}

    {bi_span("运行原则", "Run Rule")}

    {render_list(["recorded fixture 只能证明可复现样本,不等同于模型执行。", "只有 provider runner 返回 model metadata 时才计入 model-executed。"])}

    {bi_span("风险治理", "Risk")}

    {bi_span("提前暴露误触发、漂移、证据不足和迁移风险。", "Surfaces trigger, drift, evidence, and portability risks before the package grows.")}

    {charts["risk_heatmap"]}
    {risk_rows}
    {bi_span("风险", "Risk")}{bi_span("信号", "Signal")}{bi_span("应对", "Response")}
    {render_world_class_readiness(world_readiness)}

    {bi_span("包体资产", "Assets")}

    {bi_span("让 reviewer 快速确认关键文件、目录和资产分布。", "Lets reviewers confirm key files, directories, and asset distribution quickly.")}

    {charts["asset_donut"]} {package_rows}
    {bi_span("路径", "Path")}{bi_span("作用", "Role")}{bi_span("类型", "Type")}

    {bi_span("迭代路线", "Roadmap")}

    {bi_span("把下一步升级收束为少数高价值动作。", "Keeps next iteration moves focused and actionable.")}

    {charts["timeline"]}
    {render_roadmap(roadmap.get("items", []))}
    """ 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_report_model(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 the HTML skill 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()