上一节把两卷 30 章的产物装配成了一份可追溯索引,30/30 与 45/45 说明东西齐了。但齐全不等于正确:战力排序是不是真的一致?卡池概率加起来是不是 1000‰?赛季压缩之后进度到底落没落回健康带?这些是各章各自证明过的,可交付之前还需要在同一个地方、用同一套判据、一次性复核一遍——这就是全链路验收。
验收的对象不是散文,是产物。它把 out/n01_* 到 out/n15_* 读进来,对每个关键字段做一次断言,最后给出一份带层级统计的验收报告。任何一条 hard 检查失败,退出码就是 1,设计案不能交付。这一节也是全章唯一同时覆盖 S 仿真、P 原型、B 基准 三层的地方。
验收的两种检查:声明式与派生
一份好的验收清单要能被人读懂、被人改。所以检查被拆成两类:
- 声明式检查:每条写成
{artifact, path, op, value},从某个产物的某个字段取值,按一个比较算子对阈值求值。这类检查是数据,不是代码——换一条不变量只要改配置,不用动脚本;同一个配置 Python 与 TypeScript 两侧都能读。 - 派生检查:需要跨字段、跨行计算的,比如「分段发放求和等于总额」「数组反演数全零」「赛季默认档落在健康带」。这些在脚本里用几行代码实现,因为它们不是单点取值,而是聚合。
配置里 op 的取值集合是 eq / ne / ge / le / gt / lt / len_eq / abs_le / exists,kind 是 hard(失败即验收不通过)或 soft(只打软告警)。
{
"version": 1,
"note": "全链路验收不变量:每条声明式写成 {artifact, path, op, value},Python 与 TypeScript 两侧读同一份配置执行,逐位一致。op ∈ {eq, ne, ge, le, gt, lt, len_eq, abs_le, exists}。kind=hard 失败即验收不通过,kind=soft 只打软告警。另有若干需要聚合计算的派生检查(数组全零、求和守恒、单调、区间命中)在脚本内实现。tier 标注该产物来自哪一层。",
"checks": [
{ "id": "power_same_unit", "tier": "S", "artifact": "n01_power", "path": "concordancePermille.sameUnit", "op": "eq", "value": 1000, "kind": "hard", "desc": "同型对局战力排序全对" },
{ "id": "power_overall", "tier": "S", "artifact": "n01_power", "path": "concordancePermille.overall", "op": "ge", "value": 800, "kind": "hard", "desc": "战力排序整体一致性不低于 800‰" },
{ "id": "attrs_nocycle", "tier": "S", "artifact": "n01_attrs", "path": "hasCycle", "op": "eq", "value": false, "kind": "hard", "desc": "派生属性依赖图无环" },
{ "id": "attrs_no_dangling", "tier": "S", "artifact": "n01_attrs", "path": "danglingRefs", "op": "len_eq", "value": 0, "kind": "hard", "desc": "无悬空属性引用" },
{ "id": "damage_min", "tier": "S", "artifact": "n03_damage", "path": "minDamage", "op": "eq", "value": 1, "kind": "hard", "desc": "伤害地板为 1" },
{ "id": "damage_cap", "tier": "S", "artifact": "n03_damage", "path": "reductionCapPct", "op": "eq", "value": 70, "kind": "hard", "desc": "减伤上限 70%" },
{ "id": "damage_cap_holds", "tier": "S", "artifact": "n03_damage", "path": "maxReductionCappedPct", "op": "eq", "value": 70, "kind": "hard", "desc": "扫描后实际减伤不超过上限" },
{ "id": "roundcross_p", "tier": "P", "artifact": "n03_round_cross", "path": "spearman300_vs_cavalry300.logHash", "op": "eq", "value": 1910851607, "kind": "hard", "desc": "P 层回放指纹与 Python 侧逐位一致" },
{ "id": "cost_bench", "tier": "B", "artifact": "n02_bench", "path": "meanAbsResidualPermille", "op": "le", "value": 100, "kind": "hard", "desc": "成本曲线拟合平均残差在阈值内" },
{ "id": "random_dev", "tier": "B", "artifact": "n03_random_bench", "path": "deviationBp", "op": "abs_le", "value": 100, "kind": "hard", "desc": "随机项模拟均值与解析期望偏差在容差内" },
{ "id": "random_warn", "tier": "B", "artifact": "n03_random_bench", "path": "warnCount", "op": "eq", "value": 0, "kind": "hard", "desc": "随机项基准无告警" },
{ "id": "counter_ok", "tier": "S", "artifact": "n04_counter_matrix", "path": "ok", "op": "eq", "value": true, "kind": "hard", "desc": "克制矩阵方向正确(镜像≈50、克制>50、被克<50)" },
{ "id": "strength_converged", "tier": "S", "artifact": "n04_strength", "path": "converged", "op": "eq", "value": true, "kind": "hard", "desc": "强度平衡迭代收敛" },
{ "id": "pity_hardcap", "tier": "S", "artifact": "n05_pity", "path": "pityHardCapHits", "op": "eq", "value": 0, "kind": "hard", "desc": "有保底时从不触硬上限" },
{ "id": "gacha_sum", "tier": "S", "artifact": "n06_gacha", "path": "allSumOk", "op": "eq", "value": true, "kind": "hard", "desc": "卡池概率归一(合计 1000‰)" },
{ "id": "gacha_exact", "tier": "S", "artifact": "n06_gacha", "path": "allExact", "op": "eq", "value": true, "kind": "hard", "desc": "单卡概率可整除(公示表精确)" },
{ "id": "elo_converge", "tier": "S", "artifact": "n07_elo", "path": "eloConvergeRound", "op": "gt", "value": 0, "kind": "hard", "desc": "ELO 在有限回合内收敛" },
{ "id": "fairness_stable", "tier": "B", "artifact": "n07_fairness_bench", "path": "stable", "op": "eq", "value": true, "kind": "hard", "desc": "匹配公平性结论跨试验稳定" },
{ "id": "inflation_bench", "tier": "B", "artifact": "n08_inflation_bench", "path": "warn", "op": "eq", "value": false, "kind": "hard", "desc": "膨胀闭式对照偏差在阈值内" },
{ "id": "protection", "tier": "S", "artifact": "n09_protection", "path": "allPass", "op": "eq", "value": true, "kind": "hard", "desc": "免费玩家体验保护门槛全过" },
{ "id": "tier_monotonic", "tier": "S", "artifact": "n10_tier_ladder", "path": "monotonic", "op": "eq", "value": true, "kind": "hard", "desc": "定价档位单位价值单调递增" },
{ "id": "elasticity_bench", "tier": "B", "artifact": "n10_elasticity_bench", "path": "pass", "op": "eq", "value": true, "kind": "hard", "desc": "价格弹性解析最优价与离散峰值吻合" },
{ "id": "ltv_payback", "tier": "S", "artifact": "n11_ltv", "path": "paybackDay", "op": "gt", "value": 0, "kind": "hard", "desc": "回本周期有限" },
{ "id": "ltv_bench", "tier": "B", "artifact": "n11_ltv_bench", "path": "pass", "op": "eq", "value": true, "kind": "hard", "desc": "LTV 逐日递推与闭式几何级数一致" },
{ "id": "churn_auc", "tier": "B", "artifact": "n12_churn_bench", "path": "aucPermille", "op": "ge", "value": 700, "kind": "hard", "desc": "流失预警模型 AUC 达标" },
{ "id": "churn_gain", "tier": "B", "artifact": "n12_churn_bench", "path": "matchedRecallGainPermille", "op": "gt", "value": 0, "kind": "hard", "desc": "同告警量下模型召回优于规则" },
{ "id": "stats_bench", "tier": "B", "artifact": "n13_stats_bench", "path": "pass", "op": "eq", "value": true, "kind": "hard", "desc": "第一类错误率覆盖、p 值均匀、功效达标" },
{ "id": "type1_coverage", "tier": "B", "artifact": "n13_stats_bench", "path": "checks.type1.pass", "op": "eq", "value": true, "kind": "hard", "desc": "第一类错误率落在名义水平附近" },
{ "id": "event_budget", "tier": "B", "artifact": "n14_event_budget_bench", "path": "weightedInBand", "op": "eq", "value": true, "kind": "hard", "desc": "加权奖励预算落在目标带内" },
{ "id": "gvg_stable", "tier": "B", "artifact": "n15_gvg_bench", "path": "stable", "op": "eq", "value": true, "kind": "hard", "desc": "GVG 战场结论跨试验稳定" },
{ "id": "power_cross_structural", "tier": "S", "artifact": "n01_power", "path": "concordancePermille.crossUnit", "op": "ge", "value": 700, "kind": "soft", "desc": "跨型一致性有下界(结构盲区仍在)" }
]
}
一次跑完三层:S 仿真、P 原型、B 基准
下面这段是 S 层验收脚本。它读上面那份配置,对每条声明式检查按算子求值,再追加若干派生检查,最后按 tier 分组统计通过率,产出验收报告 JSON + CSV + SVG。
# file: sim/n16_acceptance.py
# tier: S
# verify: python3 sim/n16_acceptance.py --config config/ --out out/
"""全链路验收:把前面各章的产物 out/n01_* … out/n15_* 汇总成一份验收报告,逐项断言关键不变量。
两类检查:声明式的(读 config/n16_acceptance.json,按 {artifact, path, op, value} 求值,TypeScript
侧也读同一份配置)与派生的(数组全零、求和守恒、单调、区间命中,在本脚本内实现)。报告写
out/n16_acceptance.json(+ csv + svg),hard 检查失败则退出码 1。纯标准库,SVG 用 xml.etree 手写。"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
OPS = {"eq": lambda a, v: a == v, "ne": lambda a, v: a != v, "ge": lambda a, v: a >= v,
"le": lambda a, v: a <= v, "gt": lambda a, v: a > v, "lt": lambda a, v: a < v,
"len_eq": lambda a, v: len(a) == v, "abs_le": lambda a, v: abs(a) <= v,
"exists": lambda a, v: a is not None}
def load(path: str) -> dict:
with open(path, encoding="utf-8") as fh:
return json.load(fh)
def resolve(doc: dict, path: str):
cur = doc
for key in path.split("."):
cur = cur[key]
return cur
def derived(o: dict) -> list[dict]:
"""聚合检查:跨字段/跨行计算的不变量。"""
r = []
def add(cid, tier, desc, ok, detail):
r.append({"id": cid, "tier": tier, "kind": "hard", "desc": desc, "pass": bool(ok), "actual": detail})
ext = o["n01_ext"]["rows"]
add("ext_mul_exact", "S", "乘法战力与真值同构(反演恒 0)",
all(x["mulInversions"] == 0 for x in ext), [x["mulInversions"] for x in ext])
add("ext_k2_exact", "S", "二阶公式在 k=2 精确", ext[0]["add2Inversions"] == 0, ext[0]["add2Inversions"])
add("ext_add_degrades", "S", "一阶加法反演数随属性数不降",
ext[-1]["addInversions"] >= ext[0]["addInversions"], [ext[0]["addInversions"], ext[-1]["addInversions"]])
sc = {s["id"]: s for s in o["n08_compress"]["schemes"]}
add("season_default_healthy", "S", "赛季默认档把进度压回健康带",
sc["season_default"]["healthy"], sc["season_default"]["finalIndexStart"])
add("season_extremes_unhealthy", "S", "无重置与全额清零都不健康",
(not sc["keep_all"]["healthy"]) and (not sc["zero_all"]["healthy"]),
[sc["keep_all"]["healthy"], sc["zero_all"]["healthy"]])
st = o["n14_settlement"]
pj = sum(b["payoutJade"] for b in st["bands"])
ph = sum(b["payoutHonor"] for b in st["bands"])
add("settlement_conserved", "S", "分段发放求和等于结算总额(玉璧与荣誉各守恒)",
pj == st["summary"]["totalPayoutJade"] and ph == st["summary"]["totalPayoutHonor"],
{"jade": [pj, st["summary"]["totalPayoutJade"]], "honor": [ph, st["summary"]["totalPayoutHonor"]]})
elo = o["n07_elo"]
add("elo_rmse_dropped", "S", "ELO 收敛后 RMSE 显著低于开局",
elo["eloFinalRmse"] < elo["eloStartRmse"], [elo["eloStartRmse"], elo["eloFinalRmse"]])
ib = o["n08_inflation_bench"]
add("inflation_closedform", "B", "膨胀闭式对照偏差在阈值内",
abs(ib["closedFormDeviationPermille"]) <= ib["thresholdPermille"],
[ib["closedFormDeviationPermille"], ib["thresholdPermille"]])
return r
def write_svg(path: str, by_tier: dict) -> None:
width, top, bh, gap, left = 640, 34, 30, 12, 200
height = top + len(by_tier) * (bh + gap) + 20
svg = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(width),
"height": str(height), "viewBox": f"0 0 {width} {height}"})
ET.SubElement(svg, "rect", {"width": str(width), "height": str(height), "fill": "#ffffff"})
ET.SubElement(svg, "text", {"x": "16", "y": "28", "font-size": "16",
"font-family": "sans-serif"}).text = "验收不变量按层通过情况(通过 / 总数)"
span = width - left - 70
for i, (tier, c) in enumerate(sorted(by_tier.items())):
y, w = top + i * (bh + gap), span * c["pass"] // max(c["total"], 1)
ET.SubElement(svg, "text", {"x": str(left - 8), "y": str(y + bh - 9), "font-size": "12",
"text-anchor": "end", "font-family": "sans-serif"}).text = f"{tier} 层"
ET.SubElement(svg, "rect", {"x": str(left), "y": str(y), "width": str(span), "height": str(bh), "fill": "#eaeef2"})
ET.SubElement(svg, "rect", {"x": str(left), "y": str(y), "width": str(max(w, 1)), "height": str(bh),
"fill": "#1f6feb" if c["pass"] == c["total"] else "#d1242f"})
ET.SubElement(svg, "text", {"x": str(left + span + 8), "y": str(y + bh - 9), "font-size": "12",
"font-family": "sans-serif"}).text = f"{c['pass']}/{c['total']}"
ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
args = ap.parse_args()
cfg = load(os.path.join(args.config, "n16_acceptance.json"))
cache, checks = {}, []
for c in cfg["checks"]:
cache.setdefault(c["artifact"], load(os.path.join(args.out, f"{c['artifact']}.json")))
actual = resolve(cache[c["artifact"]], c["path"])
checks.append({"id": c["id"], "tier": c["tier"], "kind": c["kind"], "desc": c["desc"],
"artifact": c["artifact"], "path": c["path"], "op": c["op"], "value": c["value"],
"actual": actual, "pass": OPS[c["op"]](actual, c["value"])})
for n in ("n01_ext", "n08_compress", "n14_settlement", "n07_elo", "n08_inflation_bench"):
cache.setdefault(n, load(os.path.join(args.out, f"{n}.json")))
checks += derived(cache)
hard = [c for c in checks if c["kind"] == "hard" and not c["pass"]]
soft = [c for c in checks if c["kind"] == "soft" and not c["pass"]]
by_tier: dict[str, dict] = {}
for c in checks:
t = by_tier.setdefault(c["tier"], {"total": 0, "pass": 0})
t["total"] += 1
t["pass"] += 1 if c["pass"] else 0
summary = {"version": 1, "checks": checks,
"totals": {"checks": len(checks), "passed": sum(1 for c in checks if c["pass"]),
"hardFail": len(hard), "softFail": len(soft)},
"byTier": by_tier, "verdict": "PASS" if not hard else "FAIL"}
os.makedirs(args.out, exist_ok=True)
with open(os.path.join(args.out, "n16_acceptance.json"), "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
with open(os.path.join(args.out, "n16_acceptance.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["id", "tier", "kind", "artifact", "path", "op", "value", "actual", "pass"])
for c in checks:
w.writerow([c["id"], c["tier"], c["kind"], c.get("artifact", "-"), c.get("path", "-"),
c.get("op", "-"), c.get("value", "-"), c["actual"], c["pass"]])
write_svg(os.path.join(args.out, "n16_acceptance.svg"), by_tier)
print(f"{summary['verdict']} 检查 {len(checks)} 项 · 通过 {summary['totals']['passed']} "
f"· hard 失败 {len(hard)} · soft 告警 {len(soft)}")
for c in checks:
flag = "ok " if c["pass"] else ("!! " if c["kind"] == "hard" else "~ ")
print(f" [{c['tier']}] {flag}{c['id']:<26} {c['desc']}")
print(" " + " · ".join(f"{t} {v['pass']}/{v['total']}" for t, v in sorted(by_tier.items())))
return 0 if not hard else 1
if __name__ == "__main__":
raise SystemExit(main())
本机实跑(python3 sim/n16_acceptance.py --config config/ --out out/)最后一行按层统计:
PASS 检查 39 项 · 通过 39 · hard 失败 0 · soft 告警 0
...
B 14/14 · P 1/1 · S 24/24
过得稳不稳:验收余量基准
验收只说「过 / 不过」,余量基准说的是「过得稳不稳」。对每条带阈值的检查算一个余量:ge / gt 看实际超出下界多少,le / lt 看离上界还有多少,abs_le 看离绝对上界还有多少,都按 |实际 − 阈值| / |阈值| 归一成百分比。余量低于 10% 的判为「贴线」,只打软告警——贴线不是失败,是提醒你这条不变量经不起设计变更的折腾。
# file: sim/n16_acceptance_bench.py
# tier: B
# verify: python3 sim/n16_acceptance_bench.py --config config/ --out out/
"""验收余量基准:读 out/n16_acceptance.json,对每条带阈值的检查算「离失效还有多远」。
验收只说「过 / 不过」,余量基准说的是「过得稳不稳」。对 ge/le/gt/lt/abs_le 这类不等式,
余量 = |实际 − 阈值| / |阈值|;余量低于 warnPct 的判为「贴线」,只打软告警。软告警不硬失败,
退出码恒为 0。纯标准库。"""
from __future__ import annotations
import argparse
import json
import os
def load(path: str) -> dict:
with open(path, encoding="utf-8") as fh:
return json.load(fh)
def margin_of(op: str, actual, value):
"""返回 (余量, 方向);非不等式或非数值返回 None。"""
if op not in ("ge", "gt", "le", "lt", "abs_le"):
return None
if not isinstance(actual, (int, float)) or isinstance(actual, bool):
return None
base = abs(value) or 1
if op in ("ge", "gt"):
return (actual - value) / base, "下界"
if op in ("le", "lt"):
return (value - actual) / base, "上界"
return (value - abs(actual)) / base, "绝对上界"
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
ap.add_argument("--warn-pct", type=float, default=10.0)
args = ap.parse_args()
rep = load(os.path.join(args.out, "n16_acceptance.json"))
rows = []
for c in rep["checks"]:
m = margin_of(c.get("op", ""), c.get("actual"), c.get("value"))
if m:
rows.append({"id": c["id"], "tier": c["tier"], "op": c["op"], "actual": c["actual"],
"value": c["value"], "direction": m[1],
"marginPct": round(m[0] * 100, 1), "tight": m[0] * 100 < args.warn_pct})
rows.sort(key=lambda r: r["marginPct"])
tight = [r for r in rows if r["tight"]]
with open(os.path.join(args.out, "n16_acceptance_bench.json"), "w", encoding="utf-8") as f:
json.dump({"version": 1, "warnPct": args.warn_pct, "compared": len(rows), "tightCount": len(tight),
"tight": tight, "rows": rows, "verdict": "tight" if tight else "comfortable"},
f, ensure_ascii=False, indent=2)
print(f"{'WARN' if tight else 'OK'} 比较 {len(rows)} 条阈值检查 · 贴线(余量 < {args.warn_pct}%){len(tight)} 条")
for r in rows[:8]:
print(f" [{r['tier']}] {r['id']:<26} 实际 {r['actual']} / 阈值 {r['value']} "
f"余量 {r['marginPct']}%{' ~ 贴线' if r['tight'] else ''}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
本机实跑(python3 sim/n16_acceptance_bench.py --config config/ --out out/):
WARN 比较 8 条阈值检查 · 贴线(余量 < 10.0%)1 条
[S] power_overall 实际 848 / 阈值 800 余量 6.0% ~ 贴线
[S] power_cross_structural 实际 791 / 阈值 700 余量 13.0%
[B] churn_auc 实际 881 / 阈值 700 余量 25.9%
[B] cost_bench 实际 2 / 阈值 100 余量 98.0%
读这份验收报告:三层都过了,但有一条贴线
- 39 项检查全过,hard 失败 0。覆盖 S 仿真 24 项、B 基准 14 项、P 原型 1 项——这就是全链路的意思:不是某一章自证,而是跨章、跨层一次性复核。经济守恒(
settlement_conserved)、战力排序(power_same_unit)、概率归一(gacha_sum)、匹配收敛(elo_converge)、赛季压缩在带内(season_default_healthy)、实验第一类错误率覆盖(type1_coverage)都在其中。 - 唯一贴线的是
power_overall,余量 6.0%。战力排序整体一致性实测 848‰、阈值 800‰,只差 48‰。这条不是错,是脆:任何一次属性表微调都可能把它推下阈值。第 1 章就说过跨型一致性是结构盲区,power_cross_structural那条 soft 检查(791‰ / 700‰)也正是为它留的告警位。 - P 层只放一条检查是刻意的。
roundcross_p断言的是原型层战斗回放指纹与 Python 侧逐位一致——一条就够,因为它证明的是「两个语言算出同一个数」这件事本身;16.3 会把这条链路展开成完整回放。 - 余量基准是验收的补集。验收回答「对不对」,基准回答「稳不稳」。一份能交付的设计案,两者都要给:前者是门禁,后者是风险清单。
小结
- 全链路验收把
out/n01_*…out/n15_*汇总成一份报告,声明式检查(配置驱动,跨语言可复用)+ 派生检查(聚合计算,脚本内实现)。 - 本机实跑:39 项全过,hard 失败 0,S 24 / B 14 / P 1;关键不变量——经济守恒、战力排序、概率归一、匹配收敛、赛季压缩在带内、实验第一类错误率覆盖——全部命中。
- B 层余量基准进一步指出:
power_overall是唯一贴线项(余量 6.0%),是需盯的风险点,不是失败。 - 验收过了,但设计案还没交付。下一节用一段配置驱动的原型回放,把「同一份配置在 S 与 P 两侧算出同一串数」这件事亲手跑一遍,然后收尾交付。
阅读导航:上一节:16.1 把两卷装配成完整设计案 · 下一节:16.3 原型回放与设计案交付 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。