#!/usr/bin/env python3
"""Build the local, inspectable Skill-Up management dashboard."""

from __future__ import annotations

import html
import json
import re
import subprocess
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / "work/2026-08-12-alibaba-skill-up-adoption/skill-up-management-dashboard.html"
RUN_ROOT = ROOT / "work/2026-08-12-alibaba-skill-up-adoption/runs"


def text(value: object) -> str:
    return html.escape(str(value), quote=True)


def first_scalar(path: Path, key: str) -> str:
    pattern = re.compile(rf"^{re.escape(key)}:\s*(.+?)\s*$")
    for line in path.read_text(encoding="utf-8").splitlines():
        match = pattern.match(line)
        if match:
            return match.group(1).strip().strip('"\'')
    return ""


def relative(path: Path) -> str:
    return path.relative_to(ROOT).as_posix()


def skill_up_version() -> str:
    try:
        result = subprocess.run(
            ["skill-up", "--version"],
            cwd=ROOT,
            check=True,
            capture_output=True,
            text=True,
            timeout=10,
        )
        return result.stdout.strip()
    except (FileNotFoundError, subprocess.SubprocessError):
        return "skill-up command unavailable"


def load_suites() -> list[dict[str, object]]:
    suites: list[dict[str, object]] = []
    for eval_path in sorted(ROOT.glob("standards-stack/agent-skills/skills/*/evals/eval.yaml")):
        skill_dir = eval_path.parents[1]
        cases = []
        for case_path in sorted((eval_path.parent / "cases").glob("*.yaml")):
            cases.append(
                {
                    "id": first_scalar(case_path, "id"),
                    "title": first_scalar(case_path, "title"),
                    "description": first_scalar(case_path, "description"),
                    "path": relative(case_path),
                }
            )
        suites.append(
            {
                "skill": skill_dir.name,
                "eval_path": relative(eval_path),
                "cases": cases,
            }
        )
    return suites


def load_runs() -> list[dict[str, object]]:
    runs: list[dict[str, object]] = []
    if not RUN_ROOT.exists():
        return runs
    for result_path in sorted(RUN_ROOT.glob("*/iteration-*/result.json")):
        payload = json.loads(result_path.read_text(encoding="utf-8"))
        case_results = payload.get("case_results", [])
        statuses = [str(item.get("status", "UNKNOWN")) for item in case_results]
        status = statuses[0] if len(set(statuses)) == 1 and statuses else "MIXED"
        report_path = result_path.with_name("report.html")
        runs.append(
            {
                "name": result_path.parents[1].name,
                "iteration": result_path.parent.name,
                "status": status,
                "cases": len(case_results),
                "started": payload.get("start_time", ""),
                "report": relative(report_path) if report_path.exists() else "",
                "result": relative(result_path),
            }
        )
    return sorted(runs, key=lambda item: str(item["started"]), reverse=True)


def build() -> str:
    suites = load_suites()
    runs = load_runs()
    case_count = sum(len(suite["cases"]) for suite in suites)
    status_counts = {name: sum(1 for run in runs if run["status"] == name) for name in ("PASS", "FAIL", "ERROR")}

    suite_sections = []
    for suite in suites:
        case_rows = "".join(
            f"""
            <tr>
              <td><code>{text(case['id'])}</code></td>
              <td><strong>{text(case['title'])}</strong><small>{text(case['description'])}</small></td>
              <td><a href="https://zhctpmt.yyangpt.cn/{text(case['path'])}">查看 YAML</a></td>
            </tr>"""
            for case in suite["cases"]
        )
        suite_sections.append(
            f"""
            <article class="suite">
              <div class="suite-head"><div><span>TEST SUITE</span><h3>{text(suite['skill'])}</h3></div><b>{len(suite['cases'])} cases</b></div>
              <p class="path"><code>{text(suite['eval_path'])}</code></p>
              <table><thead><tr><th>Case ID</th><th>目的</th><th>定义</th></tr></thead><tbody>{case_rows}</tbody></table>
            </article>"""
        )

    run_rows = "".join(
        f"""
        <tr>
          <td><strong>{text(run['name'])}</strong><small>{text(run['iteration'])}</small></td>
          <td><span class="status {text(str(run['status']).lower())}">{text(run['status'])}</span></td>
          <td>{text(run['cases'])}</td>
          <td>{text(str(run['started']).replace('T', ' ')[:19])}</td>
          <td>{f'<a href="{text(Path(run["report"]).relative_to("work/2026-08-12-alibaba-skill-up-adoption").as_posix())}">HTML</a>' if run['report'] else '—'} · <a href="{text(Path(run['result']).relative_to('work/2026-08-12-alibaba-skill-up-adoption').as_posix())}">JSON</a></td>
        </tr>"""
        for run in runs
    )

    return f"""<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="data:,">
<meta name="description" content="Alibaba Skill-Up 在 zhctprompt 协作空间内的测试套件、运行报告与操作教程。">
<title>Skill-Up 测试管理台</title><style>
:root{{--ink:#18231f;--muted:#65716c;--line:#dbe4df;--soft:#f3f7f5;--panel:#fff;--green:#08745b;--green2:#075446;--blue:#325f9b;--red:#a33c35;--amber:#98621c;--shadow:0 18px 54px rgba(21,66,51,.08)}}
*{{box-sizing:border-box}}html{{scroll-behavior:smooth}}body{{margin:0;color:var(--ink);background:linear-gradient(180deg,#edf6f2 0,#fff 520px);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;line-height:1.68}}a{{color:var(--green2);text-underline-offset:3px}}main{{width:min(1180px,calc(100% - 36px));margin:auto;padding:24px 0 72px}}nav{{position:sticky;top:10px;z-index:10;display:flex;justify-content:space-between;gap:14px;align-items:center;padding:11px 15px;border:1px solid #d4e3dc;border-radius:14px;background:rgba(255,255,255,.94);backdrop-filter:blur(12px)}}nav strong{{color:var(--green2)}}nav div{{display:flex;gap:12px;flex-wrap:wrap}}nav a{{font-size:13px;font-weight:700;text-decoration:none}}header{{margin-top:18px;padding:clamp(34px,6vw,64px);border:1px solid #cee0d7;border-radius:26px;background:#fff;box-shadow:var(--shadow)}}.eyebrow{{color:var(--green);font-size:13px;font-weight:900;letter-spacing:.12em}}h1{{max-width:880px;margin:10px 0 18px;font-size:clamp(40px,6vw,68px);line-height:1.04;letter-spacing:-.045em}}.lead{{max-width:880px;color:#45534d;font-size:18px}}.actions{{display:flex;gap:10px;flex-wrap:wrap;margin-top:24px}}.button{{padding:10px 15px;border-radius:10px;background:var(--green);color:white;text-decoration:none;font-weight:800}}.button.secondary{{background:var(--soft);color:var(--green2);border:1px solid var(--line)}}section{{padding:36px 0;border-bottom:1px solid var(--line)}}h2{{margin:0 0 8px;font-size:28px}}h3{{margin:2px 0;font-size:20px}}.intro{{margin:0 0 20px;color:var(--muted)}}.metrics,.three,.roles{{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}}.metric,.card,.suite{{border:1px solid var(--line);border-radius:17px;background:var(--panel);box-shadow:0 10px 28px rgba(21,66,51,.045)}}.metric,.card{{padding:20px}}.metric b{{display:block;color:var(--green2);font-size:34px;line-height:1.1}}.metric span,.card p{{color:var(--muted)}}.three{{grid-template-columns:repeat(3,1fr)}}.roles{{grid-template-columns:repeat(5,1fr)}}.card h3{{font-size:17px}}.suite{{margin:16px 0;padding:20px}}.suite-head{{display:flex;justify-content:space-between;gap:12px;align-items:center}}.suite-head span{{color:var(--green);font-size:11px;font-weight:900;letter-spacing:.12em}}.suite-head b{{color:var(--green2)}}.path{{overflow:auto;color:var(--muted)}}table{{width:100%;border-collapse:collapse}}th,td{{padding:13px;text-align:left;vertical-align:top;border-bottom:1px solid var(--line)}}th{{background:var(--soft);font-size:13px}}td small{{display:block;color:var(--muted);margin-top:3px}}code,pre{{font-family:"SFMono-Regular",Consolas,monospace}}code{{padding:.12em .38em;border-radius:5px;background:#eef3f0}}pre{{position:relative;overflow:auto;margin:12px 0;padding:18px;border-radius:13px;background:#17231f;color:#e7f2ed;line-height:1.6}}.status{{display:inline-flex;padding:4px 9px;border-radius:999px;font-size:12px;font-weight:900}}.pass{{color:#08745b;background:#e7f5ef}}.fail{{color:var(--red);background:#faecea}}.error{{color:var(--amber);background:#fff3df}}.flow{{display:grid;grid-template-columns:repeat(6,1fr);gap:8px;counter-reset:step}}.flow div{{padding:16px;border:1px solid var(--line);border-radius:14px;background:#fff}}.flow div:before{{counter-increment:step;content:"0" counter(step);display:block;color:var(--green);font-weight:900}}.note{{padding:18px;border-left:5px solid var(--amber);border-radius:12px;background:#fff9ef}}footer{{padding-top:30px;color:var(--muted);font-size:13px}}
@media(max-width:900px){{.metrics,.three,.roles,.flow{{grid-template-columns:1fr 1fr}}}}@media(max-width:620px){{nav{{align-items:flex-start;flex-direction:column}}.metrics,.three,.roles,.flow{{grid-template-columns:1fr}}header{{padding:30px}}table{{display:block;overflow:auto}}}}
</style></head><body><main>
<nav><strong>Skill-Up 管理台</strong><div><a href="#where">在哪管理</a><a href="#suites">用例库</a><a href="#runs">运行记录</a><a href="#tutorial">操作教程</a><a href="#governance">团队规则</a></div></nav>
<header><div class="eyebrow">AGENT SKILL EVALUATION · ZHCTPROMPT</div><h1>从真实问题，到失败报告，再到修复回归</h1><p class="lead">这是 Skill-Up 在协作空间里的统一管理入口。先看用例，再看运行报告；失败只允许归因到 Skill、Eval 或 Engine，对应层修复后先跑失败用例，再跑受影响套件。</p><div class="actions"><a class="button" href="#tutorial">开始使用</a><a class="button secondary" href="https://zhctpmt.yyangpt.cn/work/2026-08-12-alibaba-skill-up-adoption/skill-up-operations-guide.md">打开 Markdown 教程</a><a class="button secondary" href="https://github.com/jiangyang118/skill-up">打开 Jack Fork</a></div></header>
<section><div class="metrics"><div class="metric"><b>{len(suites)}</b><span>测试套件</span></div><div class="metric"><b>{case_count}</b><span>已登记用例</span></div><div class="metric"><b>{len(runs)}</b><span>历史运行记录</span></div><div class="metric"><b>{status_counts['PASS']}</b><span>PASS 运行</span></div></div></section>
<section id="where"><h2>我到底在哪管理？</h2><p class="intro">Skill-Up 当前是“仓库即管理台”，没有独立 SaaS 后台。日常只需记住下面三个入口。</p><div class="three"><div class="card"><h3>① 用例库</h3><p><code>skills/&lt;skill&gt;/evals/</code></p><p>维护输入、通过条件、禁止结论和用例清单。</p></div><div class="card"><h3>② 运行报告</h3><p><code>work/.../runs/</code></p><p>查看每次 PASS、FAIL、ERROR、回答和 HTML 报告。</p></div><div class="card"><h3>③ 团队治理</h3><p><code>skill-up-evaluation-regression</code></p><p>管理失败分类、修复顺序、安全边界、任务与 review。</p></div></div></section>
<section id="suites"><h2>测试用例库</h2><p class="intro">用例已通过配置验证；“已登记”不等于“全部跑过模型”。</p>{''.join(suite_sections)}</section>
<section id="runs"><h2>运行记录</h2><p class="intro">保留红灯是证据，不覆盖旧目录。PASS/FAIL 表示已得到回答并评分；ERROR 表示引擎基础设施没有完成公平评测。</p><div class="suite"><table><thead><tr><th>运行</th><th>状态</th><th>Case</th><th>开始时间</th><th>报告</th></tr></thead><tbody>{run_rows}</tbody></table></div></section>
<section id="tutorial"><h2>五分钟上手</h2><div class="flow"><div><b>看版本</b><small>确认本机命令</small></div><div><b>列用例</b><small>list-cases</small></div><div><b>验配置</b><small>validate</small></div><div><b>试运行</b><small>dry-run</small></div><div><b>跑单例</b><small>include-case</small></div><div><b>看报告</b><small>report.html</small></div></div>
<h3>1. 进入项目并看版本</h3><pre>cd /path/to/zhctprompt
skill-up --version</pre><p><code>{text(skill_up_version())}</code></p>
<h3>2. 查看并验证套件</h3><pre>skill-up list-cases standards-stack/agent-skills/skills/binzhou-one-card-regression-gate/evals/eval.yaml
skill-up validate standards-stack/agent-skills/skills/binzhou-one-card-regression-gate/evals/eval.yaml
skill-up run standards-stack/agent-skills/skills/binzhou-one-card-regression-gate/evals/eval.yaml --dry-run</pre>
<h3>3. 只跑一个用例</h3><pre>skill-up run standards-stack/agent-skills/skills/binzhou-one-card-regression-gate/evals/eval.yaml \
  --include-case-name recharge-arrival-not-closed \
  --output-dir work/2026-08-12-alibaba-skill-up-adoption/runs/regression-recharge-YYYYMMDD \
  --format html --iteration 1</pre>
<div class="note"><strong>关键：</strong>每次使用新的 output-dir。失败目录不能覆盖；修复后先重跑失败 case，再跑整个相关套件。</div></section>
<section id="governance"><h2>失败怎么管</h2><div class="three"><div class="card"><h3>SKILL_DEFECT</h3><p>Skill 路由或业务判断错误。修改 Skill，不碰正确断言。</p></div><div class="card"><h3>EVAL_DEFECT</h3><p>回答正确但断言过死，或错误答案能漏过。修改 case judge。</p></div><div class="card"><h3>ENGINE_INFRA</h3><p>网络、Codex、凭据、超时、runtime 出错。修引擎，不误伤业务 Skill。</p></div></div><h2 style="margin-top:32px">团队责任</h2><div class="roles"><div class="card"><h3>产品/交付</h3><p>真实问题和验收边界</p></div><div class="card"><h3>测试</h3><p>正负与边界场景</p></div><div class="card"><h3>Agent</h3><p>YAML、运行、初分</p></div><div class="card"><h3>开发/维护人</h3><p>修正确认层</p></div><div class="card"><h3>Reviewer</h3><p>防弱化、防越界</p></div></div></section>
<section><h2>刷新这个管理台</h2><p>新增用例或运行报告后，在仓库根目录执行：</p><pre>python3 scripts/build-skill-up-management-dashboard.py</pre><p>然后验证生成页，再提交协作空间。页面每次从当前 case YAML 与 result.json 重建，不单独维护第二份状态。</p></section>
<section><h2>安全边界</h2><div class="note">默认只使用本机 runtime、Codex 单轮隔离和本地规则 judge。OpenSandbox、远程 judge、MCP、OTLP、自定义 HTTP engine、沙箱绕过以及客户/生产敏感数据都不默认启用。Skill-Up PASS 证明 Agent Skill 行为，不自动等于生产支付、硬件或第三方资金链 E2E。</div></section>
<footer>数据来源：项目内 eval YAML 与脱敏 Skill-Up result.json。管理台是可再生视图；Markdown、YAML 和 JSON 仍是真源。</footer>
</main></body></html>"""


def main() -> None:
    OUTPUT.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT.write_text(build(), encoding="utf-8")
    print(f"built {relative(OUTPUT)}")


if __name__ == "__main__":
    main()
