from __future__ import annotations

import csv
import html
import shutil
import subprocess
from pathlib import Path


ROOT = Path(__file__).resolve().parents[2]
WORK = ROOT / "work" / "2026-05-17-requirement-review-sop"
OUTPUTS = WORK / "outputs"
STABLE_MD = ROOT / "standards-stack" / "management" / "product-development-department" / "REQUIREMENT_REVIEW_MEETING_SOP.md"
OBSIDIAN_ROOT = Path("/Users/jack/Documents/Jack-Brain-OS/AI产品经理")


RUBRIC_ROWS = [
    ["维度", "权重", "通过标准", "不通过表现", "主要点评人"],
    ["目标与范围", "25", "一句话需求、In Scope、Out of Scope、拍板事项清楚", "不知道为什么做、做哪些、不做哪些", "需求方/产品"],
    ["场景与规则", "20", "用户场景、主流程、异常规则、权限/数据口径清楚", "只讲页面或功能点，规则和异常不清", "产品/设计/测试"],
    ["实现与风险", "20", "接口、数据、设备、第三方、工期和风险有明确结论", "关键依赖未确认或风险无人负责", "研发/运维/交付"],
    ["验收与上线", "20", "验收标准、测试数据、回归范围、上线/回滚动作清楚", "测试无法写用例，上线后无法判断成败", "测试/运维"],
    ["结论与责任", "15", "结论、责任人、截止时间、PRD/原型/任务回写明确", "会后不知道谁改什么、什么时候完成", "主持人/记录人"],
]


ROLE_REVIEW_ROWS = [
    ["角色", "只问一个问题", "必须给出的结论"],
    ["需求方", "这个方案是否解决真实业务问题？", "确认/不确认；如不确认，缺什么"],
    ["产品", "目标、范围、规则、取舍是否讲清？", "需要回写的 PRD/原型/任务项"],
    ["设计", "流程和页面状态是否可设计？", "缺哪些状态、页面或交互说明"],
    ["研发", "技术上能不能做，风险在哪里？", "可做/有条件可做/不可做，以及依赖项"],
    ["测试", "能不能验收？", "验收用例、测试数据、阻塞项"],
    ["交付/运维/客服", "上线后谁配置、培训、响应、回滚？", "交付动作和客户侧风险"],
]


SOP_MD = """# 需求评审会 SOP

- 版本：v1.2
- 更新日期：2026-05-19
- 适用对象：需求方、产品、设计、研发、测试、交付、运维、客服
- 核心原则：评审不是讲文档，而是对齐 `目标、范围、规则、风险、验收、责任人`。

## 1. 会议信息

| 项目 | 填写 |
| --- | --- |
| 项目/产品 |  |
| 需求名称 |  |
| 需求编号/云效链接 |  |
| 主持人/记录人 |  |
| 决策人 |  |
| 参会角色 | 需求方 / 产品 / 设计 / 前端 / 后端 / 测试 / 交付运维 |
| 本次必须拍板 |  |
| 本次不讨论 |  |
| 材料链接 | PRD：；原型：；接口：；测试草案： |

## 2. 会前 6 项检查

缺任一项，只开预沟通，不开正式评审。

| 检查项 | 结论 |
| --- | --- |
| 一句话需求：为谁、在什么场景、解决什么问题 |  |
| In Scope / Out of Scope 已写清 |  |
| 主流程和异常流程能讲通 |  |
| 关键规则、权限、数据/埋点、接口/设备影响已列出 |  |
| 验收标准和测试数据有草案 |  |
| 关键人已提前同步，重大分歧没有留到会上首次暴露 |  |

## 3. 会议流程

| 环节 | 时间 | 讲清楚什么 |
| --- | --- | --- |
| 目标 | 5 分钟 | 为什么开会、今天要拍板什么 |
| 场景 | 5 分钟 | 谁遇到什么问题，不做有什么影响 |
| 方案 | 15 分钟 | 做什么、不做什么、主流程、异常、取舍 |
| 评估 | 15 分钟 | 研发可行性、测试验收、上线风险 |
| 结论 | 5 分钟 | 范围、分歧、责任人、截止时间 |

控场规则：细节争论先记录，主流程先过；每个分歧必须落到 `当场拍板 / 补证据 / 移出本期`。

## 4. 参会人点评

| 角色 | 只问一个问题 | 必须给出的结论 |
| --- | --- | --- |
| 需求方 | 这个方案是否解决真实业务问题？ | 确认/不确认；如不确认，缺什么 |
| 产品 | 目标、范围、规则、取舍是否讲清？ | 需要回写的 PRD/原型/任务项 |
| 设计 | 流程和页面状态是否可设计？ | 缺哪些状态、页面或交互说明 |
| 研发 | 技术上能不能做，风险在哪里？ | 可做/有条件可做/不可做，以及依赖项 |
| 测试 | 能不能验收？ | 验收用例、测试数据、阻塞项 |
| 交付/运维/客服 | 上线后谁配置、培训、响应、回滚？ | 交付动作和客户侧风险 |

## 5. 评分表

总分 100。`90+` 通过；`75-89` 有条件通过；`75 以下` 补材料后重评。

| 维度 | 权重 | 通过标准 | 不通过表现 | 主要点评人 |
| --- | --- | --- | --- | --- |
| 目标与范围 | 25 | 一句话需求、In Scope、Out of Scope、拍板事项清楚 | 不知道为什么做、做哪些、不做哪些 | 需求方/产品 |
| 场景与规则 | 20 | 用户场景、主流程、异常规则、权限/数据口径清楚 | 只讲页面或功能点，规则和异常不清 | 产品/设计/测试 |
| 实现与风险 | 20 | 接口、数据、设备、第三方、工期和风险有明确结论 | 关键依赖未确认或风险无人负责 | 研发/运维/交付 |
| 验收与上线 | 20 | 验收标准、测试数据、回归范围、上线/回滚动作清楚 | 测试无法写用例，上线后无法判断成败 | 测试/运维 |
| 结论与责任 | 15 | 结论、责任人、截止时间、PRD/原型/任务回写明确 | 会后不知道谁改什么、什么时候完成 | 主持人/记录人 |

一票否决：

- 需求方不确认场景或范围。
- 研发明确不可行且没有替代方案。
- 测试无法定义验收标准。
- 关键接口、数据、权限、设备或上线风险无人负责。

## 6. 会后纪要

| 项目 | 内容 |
| --- | --- |
| 已确认 |  |
| 未确认 |  |
| 本期范围 |  |
| 本期不做 |  |
| 分歧与处理 |  |
| 风险与依赖 |  |
| 需要回写的 PRD/原型/接口/测试项 |  |
| 需要同步的非参会人 |  |

| 行动项 | 负责人 | 截止时间 | 状态 |
| --- | --- | --- | --- |
|  |  |  | 未开始/进行中/完成/阻塞 |
|  |  |  | 未开始/进行中/完成/阻塞 |
|  |  |  | 未开始/进行中/完成/阻塞 |
"""


ROUTING_NOTE = """# ROUTING NOTE - 需求评审会 SOP

## Goal

将需求评审会 SOP 精简为可直接填写、可直接评分、可直接闭环的版本。

## Source Context

- 原始文件：`/Users/jack/Downloads/AI产品经理/如何有效开好需求评审会.docx`
- 原始文件 SHA256：`419944e2c2e5e783ef5b6a2154228c1964acc7009d4fbebfa35663a05b21cf98`
- Obsidian 来源根：`/Users/jack/Documents/Jack-Brain-OS/AI产品经理`
- Obsidian 来源索引：`source/obsidian-ai-pm-source-index.csv`
- 精简原则：保留来源索引，不把 Obsidian 笔记正文堆进 SOP。

## Scope

- v1.2 只保留会议信息、会前检查、会议流程、角色点评、评分表、会后纪要。
- 删除解释性章节、重复表格和细粒度模板。
- 保留 Markdown 真源、HTML 审阅版、Word 版和 CSV。

## Review Criteria

- 一屏能看懂结构。
- 开会时能直接填。
- 评分维度不超过 5 个。
- 会后能落到责任人和回写项。
"""


SUMMARY = """# 需求评审会 SOP 生成总结

## 结果

已按用户反馈将 SOP 从 v1.1 精简为 v1.2。新版本只保留 6 块：会议信息、会前检查、会议流程、参会人点评、评分表、会后纪要。

## 输出文件

- `standards-stack/management/product-development-department/REQUIREMENT_REVIEW_MEETING_SOP.md`：团队长期复用 Markdown 真源。
- `outputs/requirement-review-meeting-sop.html`：HTML 审阅版。
- `outputs/需求评审会SOP_可填写模板.docx`：可编辑 Word 版。
- `outputs/requirement-review-evaluation-rubric.csv`：5 项评分指标。
- `source/obsidian-ai-pm-source-index.csv`：Obsidian AI 产品经理来源索引。

## 保留与删除

保留：目标范围、场景规则、实现风险、验收上线、结论责任。

删除：长篇适用范围说明、过细的需求内容模板、重复的边界表、单独回写表和复盘问题。

## Review Status

- 当前状态：v1.2，待真实需求评审使用后复盘。
- 下一步：用一次真实会议检验是否还需要继续压缩或调整评分权重。
"""


OBSIDIAN_DISTILLED = """# Obsidian AI 产品经理材料去重补强

## 来源范围

- Obsidian vault：`/Users/jack/Documents/Jack-Brain-OS`
- 本次使用目录：`AI产品经理/`
- 全量来源索引：`source/obsidian-ai-pm-source-index.csv`

## v1.2 只保留 4 条

1. 评审前先有一句话需求。
2. 需求必须讲清 `目标 -> 场景 -> 范围 -> 规则 -> 验收`。
3. 复杂需求必须提前识别角色、终端、系统、数据、流程和交付边界。
4. 评审结论必须回写 PRD、原型、接口、测试或任务。
"""


def write_text(path: Path, content: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content.rstrip() + "\n", encoding="utf-8")


def write_csv(path: Path, rows: list[list[str]]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8-sig", newline="") as f:
        writer = csv.writer(f)
        writer.writerows(rows)


def classify_obsidian_source(path: Path) -> str:
    rel = path.relative_to(OBSIDIAN_ROOT).as_posix()
    if rel.startswith("01-PRD与需求/"):
        return "PRD与需求"
    if rel.startswith("02-产品方法论/"):
        return "产品方法论"
    if rel.startswith("03-AI工具与工作流/"):
        return "AI工具与工作流"
    if rel.startswith("04-研发协作与流程/"):
        return "研发协作与流程"
    if rel.startswith("真实项目交付基线/"):
        return "真实项目交付基线"
    if rel.startswith("codex-ai-pm-pack/"):
        return "Codex AI PM Pack"
    if rel.startswith("_source/"):
        return "来源清单"
    return "总控与模板"


def obsidian_relevance(path: Path) -> str:
    text = path.read_text(encoding="utf-8", errors="ignore")
    keywords = ["需求评审", "PRD", "验收", "边界", "UML", "原型", "回写", "角色", "流程", "规则"]
    score = sum(1 for keyword in keywords if keyword in text)
    if score >= 4:
        return "high"
    if score >= 2:
        return "medium"
    return "low"


def build_obsidian_source_index() -> list[list[str]]:
    rows = [["source_id", "relative_path", "category", "line_count", "relevance", "applied_policy"]]
    if not OBSIDIAN_ROOT.exists():
        rows.append(["OBS-000", str(OBSIDIAN_ROOT), "missing", "0", "blocked", "Obsidian AI产品经理目录不存在"])
        return rows

    applied_sources = {
        "00-学习总结.md",
        "10-专属Skill设计.md",
        "11-PRD-UML-原型工作流.md",
        "12-标准模板.md",
        "16-系统分析与软件设计分层方法.md",
        "真实项目交付基线/01-需求澄清.md",
        "真实项目交付基线/02-项目PRD.md",
    }
    for idx, path in enumerate(sorted(OBSIDIAN_ROOT.rglob("*.md")), start=1):
        text = path.read_text(encoding="utf-8", errors="ignore")
        rel = path.relative_to(OBSIDIAN_ROOT).as_posix()
        applied = "distilled_then_simplified" if rel in applied_sources else "source_index_only"
        rows.append([f"OBS-{idx:03d}", rel, classify_obsidian_source(path), str(text.count("\n") + 1), obsidian_relevance(path), applied])
    return rows


def table_html(rows: list[list[str]]) -> str:
    head, *body = rows
    head_html = "".join(f"<th>{html.escape(cell)}</th>" for cell in head)
    body_html = []
    for row in body:
        body_html.append("<tr>" + "".join(f"<td>{html.escape(cell)}</td>" for cell in row) + "</tr>")
    return f"<table><thead><tr>{head_html}</tr></thead><tbody>{''.join(body_html)}</tbody></table>"


def build_html() -> str:
    return f"""<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>需求评审会 SOP</title>
  <style>
    :root {{
      --ink: #17202a;
      --muted: #5d6d7e;
      --line: #d6dbdf;
      --soft: #f4f6f7;
      --brand: #0f766e;
      --warn: #92400e;
    }}
    body {{
      margin: 0;
      color: var(--ink);
      background: #fff;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
      line-height: 1.55;
    }}
    header {{
      padding: 36px 48px 24px;
      border-bottom: 1px solid var(--line);
      background: #f8fafc;
    }}
    main {{ max-width: 1120px; padding: 24px 48px 56px; }}
    h1 {{ margin: 0 0 10px; font-size: 32px; letter-spacing: 0; }}
    h2 {{ margin: 28px 0 12px; font-size: 21px; border-top: 1px solid var(--line); padding-top: 18px; }}
    .meta {{ color: var(--muted); display: grid; gap: 4px; }}
    .principle {{ margin-top: 16px; padding: 14px 16px; border-left: 5px solid var(--brand); background: #ecfdf5; font-weight: 650; }}
    .flow {{ display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; margin: 16px 0; }}
    .flow div {{ border: 1px solid var(--line); background: var(--soft); border-radius: 8px; padding: 10px; text-align: center; font-weight: 650; }}
    table {{ width: 100%; border-collapse: collapse; margin: 10px 0 20px; font-size: 14px; }}
    th, td {{ border: 1px solid var(--line); padding: 8px 10px; vertical-align: top; }}
    th {{ background: var(--soft); text-align: left; }}
    .note {{ color: var(--warn); font-weight: 650; }}
    @media (max-width: 760px) {{
      header, main {{ padding-left: 18px; padding-right: 18px; }}
      .flow {{ grid-template-columns: 1fr 1fr; }}
      table {{ font-size: 13px; }}
    }}
  </style>
</head>
<body>
  <header>
    <h1>需求评审会 SOP</h1>
    <div class="meta">
      <span>版本：v1.2 · 更新日期：2026-05-19</span>
      <span>目标：让会议结论落到范围、风险、验收、责任人和回写项。</span>
    </div>
    <div class="principle">只保留开会要用的内容：会议信息、会前检查、会议流程、角色点评、评分表、会后纪要。</div>
  </header>
  <main>
    <div class="flow">
      <div>信息</div><div>检查</div><div>流程</div><div>点评</div><div>评分</div><div>纪要</div>
    </div>
    <section>
      <h2>1. 会议信息</h2>
      {table_html([
        ["项目", "填写"],
        ["项目/产品", ""],
        ["需求名称", ""],
        ["需求编号/云效链接", ""],
        ["主持人/记录人", ""],
        ["决策人", ""],
        ["本次必须拍板", ""],
        ["本次不讨论", ""],
        ["材料链接", "PRD：；原型：；接口：；测试草案："],
      ])}
    </section>
    <section>
      <h2>2. 会前 6 项检查</h2>
      <p class="note">缺任一项，只开预沟通，不开正式评审。</p>
      {table_html([
        ["检查项", "结论"],
        ["一句话需求：为谁、在什么场景、解决什么问题", ""],
        ["In Scope / Out of Scope 已写清", ""],
        ["主流程和异常流程能讲通", ""],
        ["关键规则、权限、数据/埋点、接口/设备影响已列出", ""],
        ["验收标准和测试数据有草案", ""],
        ["关键人已提前同步，重大分歧没有留到会上首次暴露", ""],
      ])}
    </section>
    <section>
      <h2>3. 会议流程</h2>
      {table_html([
        ["环节", "时间", "讲清楚什么"],
        ["目标", "5 分钟", "为什么开会、今天要拍板什么"],
        ["场景", "5 分钟", "谁遇到什么问题，不做有什么影响"],
        ["方案", "15 分钟", "做什么、不做什么、主流程、异常、取舍"],
        ["评估", "15 分钟", "研发可行性、测试验收、上线风险"],
        ["结论", "5 分钟", "范围、分歧、责任人、截止时间"],
      ])}
    </section>
    <section>
      <h2>4. 参会人点评</h2>
      {table_html(ROLE_REVIEW_ROWS)}
    </section>
    <section>
      <h2>5. 评分表</h2>
      <p>总分 100。90+ 通过；75-89 有条件通过；75 以下补材料后重评。</p>
      {table_html(RUBRIC_ROWS)}
      <p class="note">一票否决：需求方不确认范围；研发不可行且无替代方案；测试无法定义验收；关键风险无人负责。</p>
    </section>
    <section>
      <h2>6. 会后纪要</h2>
      {table_html([
        ["项目", "内容"],
        ["已确认", ""],
        ["未确认", ""],
        ["本期范围", ""],
        ["本期不做", ""],
        ["分歧与处理", ""],
        ["风险与依赖", ""],
        ["需要回写的 PRD/原型/接口/测试项", ""],
        ["需要同步的非参会人", ""],
      ])}
      {table_html([
        ["行动项", "负责人", "截止时间", "状态"],
        ["", "", "", "未开始/进行中/完成/阻塞"],
        ["", "", "", "未开始/进行中/完成/阻塞"],
        ["", "", "", "未开始/进行中/完成/阻塞"],
      ])}
    </section>
  </main>
</body>
</html>
"""


def render_docx_from_markdown(md_path: Path, docx_path: Path) -> None:
    subprocess.run(["pandoc", str(md_path), "-o", str(docx_path)], cwd=ROOT, check=True)


def main() -> None:
    OUTPUTS.mkdir(parents=True, exist_ok=True)

    write_text(WORK / "ROUTING_NOTE.md", ROUTING_NOTE)
    write_text(WORK / "summary.md", SUMMARY)
    write_text(WORK / "source" / "obsidian-ai-pm-distilled-additions.md", OBSIDIAN_DISTILLED)
    write_text(OUTPUTS / "需求评审会SOP_可填写模板.md", SOP_MD)
    write_text(STABLE_MD, SOP_MD)
    write_text(OUTPUTS / "requirement-review-meeting-sop.html", build_html())
    write_csv(OUTPUTS / "requirement-review-evaluation-rubric.csv", RUBRIC_ROWS)
    write_csv(OUTPUTS / "requirement-review-role-comment-card.csv", ROLE_REVIEW_ROWS)
    write_csv(WORK / "source" / "obsidian-ai-pm-source-index.csv", build_obsidian_source_index())
    write_csv(
        WORK / "source" / "source-index.csv",
        [
            ["source_id", "type", "path", "sha256", "derived_output", "evidence_level", "notes"],
            [
                "SRC-001",
                "docx",
                "/Users/jack/Downloads/AI产品经理/如何有效开好需求评审会.docx",
                "419944e2c2e5e783ef5b6a2154228c1964acc7009d4fbebfa35663a05b21cf98",
                "source/如何有效开好需求评审会.source.md",
                "C",
                "外部方法文章式材料，只作流程参考",
            ],
            [
                "SRC-002",
                "obsidian-vault-directory",
                "/Users/jack/Documents/Jack-Brain-OS/AI产品经理",
                "n/a",
                "source/obsidian-ai-pm-source-index.csv; source/obsidian-ai-pm-distilled-additions.md",
                "C/D",
                "保留来源索引，正文只保留精简评审规则",
            ],
        ],
    )

    docx_path = OUTPUTS / "需求评审会SOP_可填写模板.docx"
    render_docx_from_markdown(OUTPUTS / "需求评审会SOP_可填写模板.md", docx_path)

    downloads_dir = Path("/Users/jack/Downloads/AI产品经理")
    downloads_dir.mkdir(parents=True, exist_ok=True)
    shutil.copy2(docx_path, downloads_dir / docx_path.name)
    shutil.copy2(OUTPUTS / "requirement-review-meeting-sop.html", downloads_dir / "需求评审会SOP_审阅版.html")


if __name__ == "__main__":
    main()
