# -*- coding: utf-8 -*-
"""
A-20 · 会议纪要自动生成与任务分解 · 质检脚本（闸3）

三类断言：
  A 数据完整性  —— 表在、列在、行数对得上、关联键不悬空
  B 规则正确性  —— 含**独立重算对账**（质量分/决议明确度/任务置信度全部从因子重算，逐行对账）
  C 指标达标    —— 每个指标一条断言 + 一条 0~1 区间断言，抓分母倒置

运行：python verify.py
"""
import os
import datetime as dt
from openpyxl import load_workbook

BASE = dt.datetime(2026, 9, 30, 9, 0, 0)
XLSX = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                    "..", "03-数据源", "a20_meeting_minutes.xlsx")
XLSX = os.path.normpath(XLSX)

# 门禁常量（与 PRD / gen_data.py 一致）
TRANS_GATE = 0.80
QUALITY_GATE = 0.65
DISPATCH_GATE = 0.70
CLARITY_VAGUE = 0.55
W_TR, W_DEC, W_COV, W_STC = 0.35, 0.25, 0.20, 0.20
W_TDEC, W_TROLE, W_TDL = 0.50, 0.30, 0.20
AUDIT_OFFSET_DAYS = 3

_pass = 0
_fails = []


def ck(cond, msg):
    global _pass
    if cond:
        _pass += 1
    else:
        _fails.append(msg)


def close(a, b, tol=0.005):
    return abs(float(a) - float(b)) <= tol


# ============================== 读表 ==============================
if not os.path.exists(XLSX):
    raise SystemExit("找不到数据源：%s（请先运行 03-数据源/gen_data.py）" % XLSX)

wb = load_workbook(XLSX, data_only=True)
SHEETS = ["1_会议台账", "2_纪要", "3_议题", "4_决议", "5_任务",
          "6_宣贯记录", "7_评价记录", "8_稽核记录", "9_指标汇总"]


def read(name):
    ws = wb[name]
    rows = list(ws.values)
    hdr = list(rows[0])
    out = []
    for r in rows[1:]:
        if r[0] is None:
            continue
        out.append(dict(zip(hdr, r)))
    return hdr, out


MT, meetings = read("1_会议台账")
MI, minutes = read("2_纪要")
TP, topics = read("3_议题")
DC, decisions = read("4_决议")
TS, tasks = read("5_任务")
PB, publicize = read("6_宣贯记录")
EV, evaluation = read("7_评价记录")
AU, audit = read("8_稽核记录")
KX, metrics = read("9_指标汇总")

# ============================== A 数据完整性 ==============================
ck(set(SHEETS).issubset(set(wb.sheetnames)), "A01 缺少标签页：%s" % (set(SHEETS) - set(wb.sheetnames)))

for nm, hdr, need in [
    ("1_会议台账", MT, ["会议编号", "会议类型", "召开时间", "参会人数", "密级", "转写置信度", "生成状态"]),
    ("2_纪要", MI, ["纪要编号", "会议编号", "版本号", "转写置信度", "决议明确度", "议题覆盖度", "结构完整度", "质量分", "出稿判定", "纪要状态", "审核结果"]),
    ("3_议题", TP, ["议题编号", "纪要编号", "议题名称"]),
    ("4_决议", DC, ["决议编号", "纪要编号", "议题编号", "决议内容", "决议清晰度", "明确判定", "到期日", "稽核时点", "稽核状态"]),
    ("5_任务", TS, ["任务编号", "决议编号", "责任岗", "时限(天)", "验收标准", "决议清晰度", "责任岗确定度", "时限确定度", "任务置信度", "派发判定", "任务状态"]),
    ("6_宣贯记录", PB, ["宣贯编号", "纪要编号", "应宣贯人次", "已确认人次", "确认率"]),
    ("7_评价记录", EV, ["评价编号", "纪要编号", "应回收份数", "已回收份数", "是否已过截止日"]),
    ("8_稽核记录", AU, ["稽核编号", "决议编号", "稽核时间", "达成结论", "责任部门", "责任岗"]),
    ("9_指标汇总", KX, ["指标编号", "指标名称", "分子", "分母", "实测值", "目标", "分母说明"]),
]:
    ck(set(need).issubset(set(hdr)), "A02 %s 缺列：%s" % (nm, set(need) - set(hdr)))

for nm, rows in [("会议", meetings), ("纪要", minutes), ("议题", topics), ("决议", decisions),
                 ("任务", tasks), ("宣贯", publicize), ("评价", evaluation), ("稽核", audit)]:
    ck(len(rows) > 0, "A03 %s 表为空" % nm)

DEPTS = set(m["组织部门"] for m in meetings)
ROLES = set(t["责任岗"] for t in tasks if t["责任岗"])
mids = set(m["会议编号"] for m in meetings)
min_ids = set(x["纪要编号"] for x in minutes)
dec_ids = set(d["决议编号"] for d in decisions)

for x in minutes:
    ck(x["会议编号"] in mids, "A04 纪要 %s 的会议编号 %s 悬空" % (x["纪要编号"], x["会议编号"]))
for t in topics:
    ck(t["纪要编号"] in min_ids, "A05 议题 %s 的纪要编号悬空" % t["议题编号"])
for d in decisions:
    ck(d["纪要编号"] in min_ids, "A06 决议 %s 的纪要编号悬空" % d["决议编号"])
for t in tasks:
    ck(t["决议编号"] in dec_ids, "A07 任务 %s 的决议编号悬空" % t["决议编号"])
for p in publicize:
    ck(p["纪要编号"] in min_ids, "A08 宣贯 %s 的纪要编号悬空" % p["宣贯编号"])
for e in evaluation:
    ck(e["纪要编号"] in min_ids, "A09 评价 %s 的纪要编号悬空" % e["评价编号"])
for a in audit:
    ck(a["决议编号"] in dec_ids, "A10 稽核 %s 的决议编号悬空" % a["决议编号"])

# 议题编号在决议里也要存在
top_ids = set(t["议题编号"] for t in topics)
for d in decisions:
    ck(d["议题编号"] in top_ids, "A11 决议 %s 的议题编号 %s 悬空" % (d["决议编号"], d["议题编号"]))

# 会议与纪要 1:1（按会议+版本唯一）
ck(len(set((x["会议编号"], x["版本号"]) for x in minutes)) == len(minutes), "A12 纪要（会议+版本）不唯一")

# 编号唯一
for nm, rows, key in [("会议", meetings, "会议编号"), ("纪要", minutes, "纪要编号"),
                      ("议题", topics, "议题编号"), ("决议", decisions, "决议编号"),
                      ("任务", tasks, "任务编号"), ("宣贯", publicize, "宣贯编号"),
                      ("评价", evaluation, "评价编号"), ("稽核", audit, "稽核编号")]:
    ck(len(set(r[key] for r in rows)) == len(rows), "A13 %s 编号有重复" % nm)

# 议题数 / 决议数与纪要一致
from collections import Counter
top_cnt = Counter(t["纪要编号"] for t in topics)
dec_cnt = Counter(d["纪要编号"] for d in decisions)
for x in minutes:
    ck(top_cnt.get(x["纪要编号"], 0) == x["议题数"], "A14 纪要 %s 议题数对不上" % x["纪要编号"])
    ck(dec_cnt.get(x["纪要编号"], 0) == x["决议数"], "A15 纪要 %s 决议数对不上" % x["纪要编号"])

# 宣贯 / 评价只挂已通过纪要
passed_ids = set(x["纪要编号"] for x in minutes if x["纪要状态"] == "已通过")
ck(set(p["纪要编号"] for p in publicize) == passed_ids, "A16 宣贯覆盖范围 ≠ 已通过纪要")
ck(set(e["纪要编号"] for e in evaluation) == passed_ids, "A17 评价覆盖范围 ≠ 已通过纪要")

# 会议时间在近 6 个月内
for m in meetings:
    ck((BASE - m["召开时间"]).days <= 180, "A18 会议 %s 超出近 6 个月范围" % m["会议编号"])
ck(len(meetings) == 260, "A19 会议总数 %d ≠ 260" % len(meetings))
ck(len(metrics) == 7, "A20 指标数 %d ≠ 7" % len(metrics))

# ============================== B 规则正确性 ==============================
# B01 转写置信度闸门：涉密 / 不可生成 / 转写不足的会议不生成纪要
min_mids = set(x["会议编号"] for x in minutes)
for m in meetings:
    if m["密级"] == "涉密":
        ck(m["会议编号"] not in min_mids, "B01 涉密会议 %s 生成了纪要" % m["会议编号"])
    if m["录制状态"] in ("转写失败", "音频缺失", "时长过短（<3 分钟）"):
        ck(m["会议编号"] not in min_mids, "B02 不可生成会议 %s 生成了纪要" % m["会议编号"])
    if m["转写置信度"] is not None and m["转写置信度"] < TRANS_GATE:
        ck(m["会议编号"] not in min_mids, "B03 转写不足会议 %s 生成了纪要" % m["会议编号"])

# B04 质量分独立重算（从四个因子原值重算，逐行对账）
for x in minutes:
    q = W_TR * x["转写置信度"] + W_DEC * x["决议明确度"] + W_COV * x["议题覆盖度"] + W_STC * x["结构完整度"]
    ck(close(q, x["质量分"]), "B04 纪要 %s 质量分重算 %.4f ≠ 表内 %.4f" % (x["纪要编号"], q, x["质量分"]))

# B05 决议明确度 = 该纪要明确决议占比（独立重算）
for x in minutes:
    ds = [d for d in decisions if d["纪要编号"] == x["纪要编号"]]
    if not ds:
        continue
    ratio = len([d for d in ds if d["明确判定"] == "明确"]) / len(ds)
    ck(close(ratio, x["决议明确度"]),
       "B05 纪要 %s 决议明确度重算 %.4f ≠ 表内 %.4f" % (x["纪要编号"], ratio, x["决议明确度"]))

# B06 出稿判定与质量分一致
for x in minutes:
    expect = "出稿" if x["质量分"] >= QUALITY_GATE else "未出稿"
    ck(x["出稿判定"] == expect,
       "B06 纪要 %s 出稿判定 %s 与质量分 %.4f 不符" % (x["纪要编号"], x["出稿判定"], x["质量分"]))
    if x["出稿判定"] == "未出稿":
        ck(x["纪要状态"] == "未出稿" and x["审核结果"] in ("", None),
           "B07 未出稿纪要 %s 仍进入审核" % x["纪要编号"])

# B08 审核结果 ↔ 纪要状态
for x in minutes:
    r, s = x["审核结果"], x["纪要状态"]
    if r == "通过":
        ck(s == "已通过", "B08 纪要 %s 审核通过但状态为 %s" % (x["纪要编号"], s))
    elif r == "驳回":
        ck(s == "已驳回", "B09 纪要 %s 审核驳回但状态为 %s" % (x["纪要编号"], s))
    elif r == "审核中":
        ck(s == "审核中", "B10 纪要 %s 审核中但状态为 %s" % (x["纪要编号"], s))
    if r in ("通过", "驳回"):
        ck(x["审核人"] not in ("", None), "B11 纪要 %s 已审核但无审核人" % x["纪要编号"])

# B12 驳回必须有驳回原因，且存在下一版本
for x in minutes:
    if x["审核结果"] == "驳回":
        ck(x["驳回原因"] not in ("", None), "B12 纪要 %s 驳回无原因" % x["纪要编号"])
        nxt = [y for y in minutes if y["会议编号"] == x["会议编号"] and
               int(y["版本号"].replace("v", "")) == int(x["版本号"].replace("v", "")) + 1]
        ck(len(nxt) == 1, "B13 纪要 %s 驳回后无重生成版本" % x["纪要编号"])

# B14 版本号连续（v1 起）
vmap = {}
for x in minutes:
    vmap.setdefault(x["会议编号"], []).append(int(x["版本号"].replace("v", "")))
for k, vs in vmap.items():
    ck(sorted(vs) == list(range(1, len(vs) + 1)), "B14 会议 %s 纪要版本号不连续：%s" % (k, sorted(vs)))

# B15 决议清晰度 ↔ 明确判定（R2 闸门）
for d in decisions:
    expect = "待明确" if d["决议清晰度"] < CLARITY_VAGUE else "明确"
    ck(d["明确判定"] == expect,
       "B15 决议 %s 明确判定 %s 与清晰度 %.4f 不符" % (d["决议编号"], d["明确判定"], d["决议清晰度"]))
    if expect == "待明确":
        ck(any(w in str(d["决议内容"]) for w in ("再议", "待定", "另行通知", "视情况", "未形成结论")),
           "B16 待明确决议 %s 内容不含含糊标志词" % d["决议编号"])

# B17 待明确决议不生成任务（R2）
vague_ids = set(d["决议编号"] for d in decisions if d["明确判定"] == "待明确")
for t in tasks:
    ck(t["决议编号"] not in vague_ids, "B17 待明确决议 %s 生成了任务 %s" % (t["决议编号"], t["任务编号"]))

# B18 任务置信度独立重算
for t in tasks:
    c = W_TDEC * t["决议清晰度"] + W_TROLE * t["责任岗确定度"] + W_TDL * t["时限确定度"]
    ck(close(c, t["任务置信度"]), "B18 任务 %s 置信度重算 %.4f ≠ 表内 %.4f" % (t["任务编号"], c, t["任务置信度"]))

# B19 派发判定与置信度一致
DISPATCHED_STATES = ("已派发", "执行中", "已提交", "验收中", "已完成", "已取消")
for t in tasks:
    if t["任务状态"] in DISPATCHED_STATES:
        if "自动派发" in str(t["派发判定"]):
            ck(t["任务置信度"] >= DISPATCH_GATE,
               "B19 任务 %s 自动派发但置信度 %.4f < %.2f" % (t["任务编号"], t["任务置信度"], DISPATCH_GATE))
        elif "人工确认后派发" in str(t["派发判定"]):
            ck(t["任务置信度"] < DISPATCH_GATE,
               "B20 任务 %s 人工确认但置信度 %.4f ≥ %.2f（应自动派发）" % (t["任务编号"], t["任务置信度"], DISPATCH_GATE))
    if t["任务状态"] == "待指派":
        ck(not t["责任岗"], "B21 待指派任务 %s 却填了责任岗" % t["任务编号"])
        ck(not t["责任人"], "B22 待指派任务 %s 却填了责任人" % t["任务编号"])

# B23 三要素齐全（K3 硬红线）：已派发任务必须有责任岗 + 时限 + 验收标准
for t in tasks:
    if t["任务状态"] in DISPATCHED_STATES:
        ck(bool(t["责任岗"]) and t["时限(天)"] and bool(t["验收标准"]),
           "B23 已派发任务 %s 三要素不全" % t["任务编号"])
        ck(t["派发时间"] is not None and t["到期日"] is not None,
           "B24 已派发任务 %s 缺派发时间或到期日" % t["任务编号"])
        ck((t["到期日"] - t["派发时间"]).days == t["时限(天)"],
           "B25 任务 %s 到期日 - 派发时间 ≠ 时限" % t["任务编号"])

# B26 任务清晰度必须等于来源决议的清晰度（两级回溯一致性）
dec_clarity = {d["决议编号"]: d["决议清晰度"] for d in decisions}
for t in tasks:
    ck(close(t["决议清晰度"], dec_clarity[t["决议编号"]]),
       "B26 任务 %s 清晰度与来源决议 %s 不一致" % (t["任务编号"], t["决议编号"]))

# B27 已完成必须有验收通过；已取消不得有验收通过
for t in tasks:
    if t["任务状态"] == "已完成":
        ck(t["验收结论"] == "验收通过", "B27 已完成任务 %s 验收结论为 %s" % (t["任务编号"], t["验收结论"]))
        ck(t["验收时间"] is not None, "B28 已完成任务 %s 无验收时间" % t["任务编号"])
    if t["任务状态"] == "已取消":
        ck(t["验收结论"] != "验收通过", "B29 已取消任务 %s 却验收通过" % t["任务编号"])

# B30 超期天数与升级层级一致
for t in tasks:
    if t["超期天数"] and t["超期天数"] > 0:
        ck(t["任务状态"] == "执行中", "B30 超期任务 %s 状态为 %s（应为执行中）" % (t["任务编号"], t["任务状态"]))
        ck(t["升级层级"] == "部门负责人", "B31 超期任务 %s 未升级到部门负责人" % t["任务编号"])
        ck(t["到期日"] < BASE, "B32 任务 %s 标记超期但到期日未过" % t["任务编号"])
    else:
        ck(not t["升级层级"], "B33 任务 %s 未超期却有升级层级" % t["任务编号"])

# B34 宣贯 / 评价 / 稽核基本约束
for p in publicize:
    ck(0 <= p["已确认人次"] <= p["应宣贯人次"],
       "B34 宣贯 %s 已确认 %d 超出应宣贯 %d" % (p["宣贯编号"], p["已确认人次"], p["应宣贯人次"]))
    ck(p["代签记录"] == "无（AI 不代签）", "B35 宣贯 %s 出现代签记录" % p["宣贯编号"])
    ck(close(p["确认率"], p["已确认人次"] / p["应宣贯人次"]),
       "B36 宣贯 %s 确认率对不上" % p["宣贯编号"])

for e in evaluation:
    ck(0 <= e["已回收份数"] <= e["应回收份数"],
       "B37 评价 %s 已回收超出应回收" % e["评价编号"])
    ck(1.0 <= float(e["平均分"]) <= 5.0, "B38 评价 %s 平均分越界" % e["评价编号"])

dec_due = {d["决议编号"]: d for d in decisions}
for a in audit:
    d = dec_due[a["决议编号"]]
    ck((a["稽核时间"] - d["到期日"]).days == AUDIT_OFFSET_DAYS,
       "B39 稽核 %s 时点 ≠ 决议到期日 +%d 天" % (a["稽核编号"], AUDIT_OFFSET_DAYS))
    ck(d["稽核状态"] == "已稽核", "B40 决议 %s 有稽核记录但状态为 %s" % (d["决议编号"], d["稽核状态"]))
    ck(d["达成结论"] == a["达成结论"], "B41 决议 %s 达成结论与稽核记录不一致" % d["决议编号"])
    ck(a["稽核时间"] <= BASE, "B42 稽核 %s 时点晚于基准日，不应已稽核" % a["稽核编号"])
    if a["达成结论"] == "未达成":
        ck(a["未达成原因类别"] not in ("", None), "B43 稽核 %s 未达成但无原因" % a["稽核编号"])
    else:
        ck(a["未达成原因类别"] in ("", None), "B44 稽核 %s 达成却有未达成原因" % a["稽核编号"])
    ck(a["责任部门"] in DEPTS, "B45 稽核 %s 责任部门 %s 不在部门清单内" % (a["稽核编号"], a["责任部门"]))

# B58 已稽核决议的任务必须全部处于终态（否则会出现「决议已判达成、任务还躺着」）
task_by_dec = {}
for t in tasks:
    task_by_dec.setdefault(t["决议编号"], []).append(t)
for a in audit:
    ts = task_by_dec.get(a["决议编号"], [])
    ck(bool(ts), "B58 已稽核决议 %s 没有任何任务" % a["决议编号"])
    for t in ts:
        ck(t["任务状态"] in ("已完成", "已取消"),
           "B59 已稽核决议 %s 的任务 %s 仍处于 %s" % (a["决议编号"], t["任务编号"], t["任务状态"]))

# B60 未稽核的明确决议，若有任务仍在进行中，则不得早于稽核时点被判达成
dec_clear_local = [d for d in decisions if d["纪要编号"] in passed_ids and d["明确判定"] == "明确"]
for d in dec_clear_local:
    if d["决议编号"] in set(a["决议编号"] for a in audit):
        continue
    ck(d["稽核状态"] == "待稽核", "B60 决议 %s 无稽核记录但状态为 %s" % (d["决议编号"], d["稽核状态"]))

# B46 未稽核决议不得有达成结论
audited_ids = set(a["决议编号"] for a in audit)
for d in decisions:
    if d["决议编号"] in audited_ids:
        continue
    ck(d["达成结论"] in ("", None), "B46 决议 %s 未稽核却有达成结论" % d["决议编号"])

# B47 稽核不出自然人（G-43 不追责到人）：责任岗必须是岗位名，不含编号/姓名后缀
for a in audit:
    ck(not any(ch.isdigit() for ch in str(a["责任岗"])),
       "B47 稽核 %s 责任岗 %s 含编号，疑似落到个人" % (a["稽核编号"], a["责任岗"]))
    ck(a["责任岗"] in ROLES, "B48 稽核 %s 责任岗 %s 不在岗位清单内" % (a["稽核编号"], a["责任岗"]))

# B49 未完成（进行中）任务不得有验收结论
for t in tasks:
    if t["任务状态"] in ("已派发", "执行中"):
        ck(t["验收结论"] in ("", None), "B49 任务 %s 未提交却有验收结论" % t["任务编号"])
        ck(t["验收时间"] is None, "B50 任务 %s 未提交却有验收时间" % t["任务编号"])
    if t["任务状态"] in ("已提交", "验收中"):
        ck(t["提交时间"] is not None, "B51 任务 %s 已提交但无提交时间" % t["任务编号"])
        ck(t["验收结论"] in ("", None), "B52 任务 %s 验收中却已有验收结论" % t["任务编号"])
    if t["任务状态"] == "不派发":
        ck("人工判定不派" in str(t["派发判定"]), "B53 任务 %s 状态不派发但判定为 %s" % (t["任务编号"], t["派发判定"]))
        ck(t["派发时间"] is None and t["到期日"] is None, "B54 不派发任务 %s 却有派发时间" % t["任务编号"])
        ck(t["验收结论"] != "验收通过", "B55 不派发任务 %s 却验收通过" % t["任务编号"])
        ck(t["任务置信度"] < DISPATCH_GATE, "B56 不派发任务 %s 置信度 %.4f ≥ %.2f" % (t["任务编号"], t["任务置信度"], DISPATCH_GATE))

# 任务状态域合法
VALID_STATES = ("待指派", "不派发", "已派发", "执行中", "已提交", "验收中", "已完成", "已取消")
for t in tasks:
    ck(t["任务状态"] in VALID_STATES, "B57 任务 %s 状态 %s 不在状态域内" % (t["任务编号"], t["任务状态"]))

# ============================== C 指标达标 ==============================
n_reviewed = len([x for x in minutes if x["审核结果"] in ("通过", "驳回")])
n_first_pass = len([x for x in minutes if x["审核结果"] == "通过" and x["版本号"] == "v1"])
K1 = n_first_pass / n_reviewed

dec_passed = [d for d in decisions if d["纪要编号"] in passed_ids]
dec_clear = [d for d in dec_passed if d["明确判定"] == "明确"]
n_dec_clear = len(dec_clear)
tsk_by_dec = {}
for t in tasks:
    tsk_by_dec.setdefault(t["决议编号"], []).append(t)
n_dec_with_task = len([d for d in dec_clear if tsk_by_dec.get(d["决议编号"])])
K2 = n_dec_with_task / n_dec_clear

n_disp = len([t for t in tasks if t["任务状态"] in DISPATCHED_STATES])
n_three = len([t for t in tasks if t["任务状态"] in DISPATCHED_STATES
               and t["责任岗"] and t["时限(天)"] and t["验收标准"]])
K3 = n_three / n_disp

n_term = len([t for t in tasks if t["任务状态"] in ("已完成", "已取消")])
n_done = len([t for t in tasks if t["任务状态"] == "已完成"])
K4 = n_done / n_term

n_should_pub = sum(p["应宣贯人次"] for p in publicize)
n_done_pub = sum(p["已确认人次"] for p in publicize)
K5 = n_done_pub / n_should_pub

n_should_evl = sum(e["应回收份数"] for e in evaluation if e["是否已过截止日"] == "是")
n_done_evl = sum(e["已回收份数"] for e in evaluation if e["是否已过截止日"] == "是")
K6 = n_done_evl / n_should_evl

n_aud = len(audit)
n_ach = len([a for a in audit if a["达成结论"] == "达成"])
K7 = n_ach / n_aud

ck(K1 >= 0.70, "C01 K1 纪要一次通过率 %.4f < 70%%" % K1)
ck(0.0 <= K1 <= 1.0, "C02 K1 %.4f 越界（分母倒置？）" % K1)
ck(K2 == 1.0, "C03 K2 决议落地覆盖率 %.4f ≠ 100%%（硬红线：定了 N 件就必须挂 N 组任务）" % K2)
ck(K3 == 1.0, "C04 K3 任务三要素齐全率 %.4f ≠ 100%%（硬红线：缺一不许派）" % K3)
ck(K4 >= 0.70, "C05 K4 任务闭环率 %.4f < 70%%" % K4)
ck(0.0 <= K4 <= 1.0, "C06 K4 %.4f 越界（分母倒置？）" % K4)
ck(K5 >= 0.85, "C07 K5 宣贯覆盖率 %.4f < 85%%" % K5)
ck(0.0 <= K5 <= 1.0, "C08 K5 %.4f 越界（分母倒置？）" % K5)
ck(K6 >= 0.60, "C09 K6 评价回收率 %.4f < 60%%" % K6)
ck(0.0 <= K6 <= 1.0, "C10 K6 %.4f 越界（分母是否误用「全部」而非「应回收」）" % K6)
ck(K7 >= 0.70, "C11 K7 决议达成率 %.4f < 70%%" % K7)
ck(0.0 <= K7 <= 1.0, "C12 K7 %.4f 越界（分母倒置？）" % K7)

denoms = [n_reviewed, n_dec_clear, n_disp, n_term, n_should_pub, n_should_evl, n_aud]
ck(len(set(denoms)) == 7, "C13 七个分母出现重复：%s" % denoms)

# 指标表内值对账
for r in metrics:
    kid = r["指标编号"]
    val = {"K1": K1, "K2": K2, "K3": K3, "K4": K4, "K5": K5, "K6": K6, "K7": K7}[kid]
    ck(close(r["实测值"], val), "C14 %s 指标表实测值 %.4f ≠ 重算 %.4f" % (kid, r["实测值"], val))
    ck(close(r["分子"] / r["分母"], val), "C15 %s 分子/分母 ≠ 实测值" % kid)

# 硬红线报数纪律：K2/K3 必须报「达标」且值 =1.0
for r in metrics:
    if r["指标编号"] in ("K2", "K3"):
        ck(str(r["目标"]).startswith("100%"), "C16 %s 目标未写 100%%（硬红线）" % r["指标编号"])
        ck(r["是否达标"] == "达标", "C17 %s 未报达标（硬红线不得平均化）" % r["指标编号"])

# 每个 0~1 指标的分母必须大于分子（防倒置）
ck(n_first_pass <= n_reviewed, "C18 K1 分子 > 分母")
ck(n_dec_with_task <= n_dec_clear, "C19 K2 分子 > 分母")
ck(n_three <= n_disp, "C20 K3 分子 > 分母")
ck(n_done <= n_term, "C21 K4 分子 > 分母")
ck(n_done_pub <= n_should_pub, "C22 K5 分子 > 分母")
ck(n_done_evl <= n_should_evl, "C23 K6 分子 > 分母")
ck(n_ach <= n_aud, "C24 K7 分子 > 分母")

# ============================== 输出 ==============================
print("=" * 60)
print("A-20 · 会议纪要自动生成与任务分解 · 质检结果")
print("=" * 60)
print("数据源：%s" % XLSX)
print("会议 %d ｜ 纪要 %d ｜ 议题 %d ｜ 决议 %d ｜ 任务 %d ｜ 宣贯 %d ｜ 评价 %d ｜ 稽核 %d"
      % (len(meetings), len(minutes), len(topics), len(decisions),
         len(tasks), len(publicize), len(evaluation), len(audit)))
print("-" * 60)
print("七个分母：%s" % denoms)
print("K1 纪要一次通过率   %4d/%4d = %.4f  (目标 ≥70%%)" % (n_first_pass, n_reviewed, K1))
print("K2 决议落地覆盖率   %4d/%4d = %.4f  (目标 =100%% · 硬红线)" % (n_dec_with_task, n_dec_clear, K2))
print("K3 任务三要素齐全率 %4d/%4d = %.4f  (目标 =100%% · 硬红线)" % (n_three, n_disp, K3))
print("K4 任务闭环率       %4d/%4d = %.4f  (目标 ≥70%%)" % (n_done, n_term, K4))
print("K5 宣贯覆盖率       %4d/%4d = %.4f  (目标 ≥85%%)" % (n_done_pub, n_should_pub, K5))
print("K6 评价回收率       %4d/%4d = %.4f  (目标 ≥60%%)" % (n_done_evl, n_should_evl, K6))
print("K7 决议达成率       %4d/%4d = %.4f  (目标 ≥70%%)" % (n_ach, n_aud, K7))
print("-" * 60)
print("断言总数：%d ｜ 通过 %d ｜ 失败 %d" % (_pass + len(_fails), _pass, len(_fails)))
if _fails:
    print("\n❌ 失败明细（前 40 条）：")
    for f in _fails[:40]:
        print("  - %s" % f)
    raise SystemExit(1)
else:
    print("\n✅ %d 项断言全 PASS（闸3 放行）" % _pass)
