# ============================================================ # 兜底验证器 v1(moduleId: core-fallback-verify, 可重生 ✅) # 《Pi-Agent兜底能力详细方案》§4.1/§4.5 + GOAL-P2 交付 4 + P2-DESIGN §4: # 兜底执行后的 world diff、验证规则(行数/数量对账)、报告生成。 # 铁律:报告里的每个数字都只许来自冻结快照(cp_before/cp_after 的 # world 深拷贝),绝不引用 Pi 报告文本或 Pi 自述。 # ============================================================ from __future__ import annotations import hashlib import json from pathlib import Path from typing import Any # 对账覆盖的业务主数据表(分表 diff 的固定口径) _DIFF_TABLES = ( "salesOrders", "flexOrders", "materials", "flexMaterials", "flexEquipment", "flexMolds", "flexOperations", "flexRoutings", "flexBom", "productionOrders", "workOrders", ) # 数量对账类规则的数据源字段(存在即计入 quantityDelta) _QUANTITY_FIELDS = ("quantity", "stock") def _canonical(item: Any) -> str: """条目的规范 JSON(排序键 + 紧凑分隔符),modified 判定与指纹共用口径。""" return json.dumps(item, ensure_ascii=True, sort_keys=True, separators=(",", ":"), default=str) def _entry_key(item: dict) -> tuple: """条目身份键:数值 id 优先,其次 orderNo/code,最后整条 canonical(防无键表)。""" if isinstance(item, dict): if isinstance(item.get("id"), int): return ("id", item["id"]) if item.get("orderNo"): return ("orderNo", str(item["orderNo"])) if item.get("code"): return ("code", str(item["code"])) return ("json", _canonical(item)) def _quantity_of(item: dict) -> float: total = 0.0 for field in _QUANTITY_FIELDS: value = item.get(field) if isinstance(item, dict) else None if isinstance(value, (int, float)): total += float(value) return total def world_diff(before: dict, after: dict) -> dict: """分表 diff:{table: {"added","removed","modified","quantityDelta"}}。 modified 判定 = 同身份键条目内容变化(canonical JSON 不等); quantityDelta 对含 quantity/stock 字段的条目求和(added 减 removed)。 """ diff: dict[str, dict] = {} for table in _DIFF_TABLES: before_rows = before.get(table) or [] after_rows = after.get(table) or [] before_map = {_entry_key(r): r for r in before_rows if isinstance(r, dict)} after_map = {_entry_key(r): r for r in after_rows if isinstance(r, dict)} added_keys = [k for k in after_map if k not in before_map] removed_keys = [k for k in before_map if k not in after_map] modified = sum( 1 for k in before_map.keys() & after_map.keys() if _canonical(before_map[k]) != _canonical(after_map[k]) ) qty_delta = ( sum(_quantity_of(after_map[k]) for k in added_keys) - sum(_quantity_of(before_map[k]) for k in removed_keys) ) if added_keys or removed_keys or modified: diff[table] = { "added": len(added_keys), "removed": len(removed_keys), "modified": modified, "quantityDelta": qty_delta, } return diff def check_expectations(plan: dict, diff: dict) -> list[dict]: """逐条比对计划 expected(结构化字段)与实际 diff。 返回 [{"step","expect","actual","ok"} ...];容差 = 0——任一声明字段不等即 ok=False, 调用方据此把报告 verdict 判为 MISMATCH(显式,不圆场)。 """ checks: list[dict] = [] for step in plan.get("steps") or []: seq = step.get("seq") for expect in step.get("expected") or []: if not isinstance(expect, dict) or not expect.get("table"): continue table = str(expect["table"]) actual = (diff.get(table) or {}).copy() actual.setdefault("added", 0) actual.setdefault("removed", 0) actual.setdefault("modified", 0) ok = True for field in ("added", "removed", "modified"): if field in expect and expect[field] is not None \ and int(expect[field]) != int(actual.get(field) or 0): ok = False checks.append({ "step": seq, "expect": {k: expect[k] for k in ("table", "added", "removed", "modified") if k in expect}, "actual": {"table": table, **{k: actual.get(k, 0) for k in ("added", "removed", "modified")}}, "ok": ok, }) return checks def diff_summary_lines(diff: dict) -> list[str]: """分表 diff 的人类可读摘要行(回复文案与报告共用)。""" lines = [] for table, d in diff.items(): parts = [] if d["added"]: parts.append(f"+{d['added']}") if d["removed"]: parts.append(f"-{d['removed']}") if d["modified"]: parts.append(f"~{d['modified']}") line = f"{table} {'/'.join(parts)}" if d.get("quantityDelta"): line += f"(数量净变化 {d['quantityDelta']:g})" lines.append(line) return lines or ["(无业务主数据变化)"] def build_report(run_dir: Path, plan: dict, *, cp_before_id: str, cp_after_id: str, before_world: dict, after_world: dict, checks: list[dict]) -> Path: """生成 outbox/verify-report.md:计划摘要 / 分表 diff 表 / 对账结论 / 证据引用。 数字来源 = 两个冻结快照的 world(调用方保证传入的是 checkpoint 仓内深拷贝), 返回报告路径。verdict:全部对账通过 = PASS;任一不符 = MISMATCH(显式标注)。 """ diff = world_diff(before_world, after_world) verdict = "PASS" if all(c["ok"] for c in checks) else "MISMATCH" lines = [ "# 兜底执行验证报告", "", f"- 运行:{plan.get('runId') or ''}", f"- 场景:{plan.get('scenario') or ''} · 步骤数 {len(plan.get('steps') or [])}", f"- 检查点:执行前 `{cp_before_id}` → 执行后 `{cp_after_id}`", "", "## 分表 diff(before → after)", "", "| 表 | 新增 | 移除 | 修改 | 数量净变化 |", "|----|------|------|------|-----------|", ] for table in _DIFF_TABLES: d = diff.get(table) if not d: continue lines.append(f"| {table} | {d['added']} | {d['removed']} | {d['modified']} " f"| {d['quantityDelta']:g} |") if not diff: lines.append("| (无变化) | 0 | 0 | 0 | 0 |") lines += ["", "## 对账结论", ""] if checks: for c in checks: mark = "✅" if c["ok"] else "❌" lines.append(f"- {mark} 步骤{c['step']} 期望 {json.dumps(c['expect'], ensure_ascii=False)}" f" · 实际 {json.dumps(c['actual'], ensure_ascii=False)}") else: lines.append("- (计划未声明结构化预期,仅呈现实际 diff)") lines += [ "", f"**verdict: {verdict}**", "", f"本报告全部数字来自检查点 {cp_before_id} 与 {cp_after_id} 的冻结快照。", ] out = Path(run_dir) / "outbox" / "verify-report.md" out.parent.mkdir(parents=True, exist_ok=True) out.write_text("\n".join(lines) + "\n", encoding="utf-8") return out def report_fingerprint(text: str) -> str: """报告文本 sha256(审计 rationale 引用用,不落全文)。""" return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]