本节目标:把评审、装配、交付三份配置串成一次端到端总验收,产出可读的验收报告,并把卷一的结论交接给卷二。
和本章前两节一样,站内那批 PRD 原型文档 是单品的可交互原型口径;本节收束的是卷一前十四章的产出——把它装配成一份可评审、可验收、可交付的设计案,并在交付前做最后一次逐项核对。
前两节分别解决了「评审要什么」和「设计案怎么摆」。这一节回答最后一个问题:这份设计案怎么算「交付合格」。交付不是把文档打包发出去,而是逐项核对——结构对不对、材料齐不齐、命名一致不一致——都能被一个脚本回答。
15.3 交付物验收与下一卷衔接
15.3.1 验收是交付前的最后一次核对
交付失败往往不是内容不行,而是接不住:开发拿到设计案发现少了一章、引用了一个不存在的文件名、或者验收标准各说各话。验收要做的是把「接得住」变成可核对的项,它有两条线:
- 结构验收:卷一的五十篇齐不齐,文件名与 slug 是否严格一致,命名规则有没有越界。
- 内容验收:评审材料是否三件套齐备、设计案装配是否每章有产出物、白名单与装配清单是否交叉一致。
这两条线合起来,就是一份可交给下一位接手者的验收报告。它的价值不在「证明我做完了」,而在「证明下一个人能无缝接上」。
15.3.2 验收基线:白名单与验收项
下面这份是 D 层配置:它硬编码了卷一全部五十篇的 slug 白名单,登记了三条验收项(A1 评审材料、A2 设计案装配、A3 交付白名单),以及五个卷二衔接点。白名单写死在配置里,是刻意的——它是「文件名 = slug」这条规则的唯一基准,交付时拿实际文件名去对,多一个少一个立刻暴露。
{
"version": 1,
"game": "merge-island",
"note": "卷一交付验收基线(D 层)。硬编码卷一 50 篇 slug 白名单,作为交付前「文件名 = slug」的断言基准;另登记三份配置的验收项与卷二衔接点。",
"volume": "市场品类与产品设计",
"prefix": "mg-design-",
"expectedTotal": 50,
"expectedSections": 45,
"whitelist50": [
"mg-design-toc", "mg-design-intro", "mg-design-preface", "mg-design-appendix-a", "mg-design-appendix-b",
"mg-design-01-1-platform-landscape", "mg-design-01-2-distribution-mechanism", "mg-design-01-3-market-size-growth",
"mg-design-02-1-rank-structure-genre-share", "mg-design-02-2-competitor-teardown-method", "mg-design-02-3-differentiation-opportunity",
"mg-design-03-1-genre-taxonomy", "mg-design-03-2-genre-business-matrix", "mg-design-03-3-genre-scoring-sensitivity",
"mg-design-04-1-package-size-budget", "mg-design-04-2-first-load-cold-start", "mg-design-04-3-platform-capability-review",
"mg-design-05-1-user-persona-scenario", "mg-design-05-2-motivation-payer-tier", "mg-design-05-3-lifecycle-retention-structure",
"mg-design-06-1-core-loop-breakdown", "mg-design-06-2-loop-duration-feedback", "mg-design-06-3-minimal-playable-prototype",
"mg-design-07-1-level-structure", "mg-design-07-2-difficulty-flow-curve", "mg-design-07-3-level-calibration-generation",
"mg-design-08-1-progression-curve", "mg-design-08-2-progress-one-more-run", "mg-design-08-3-offline-earnings-return",
"mg-design-09-1-social-surface", "mg-design-09-2-viral-k-factor", "mg-design-09-3-social-pressure-protection",
"mg-design-10-1-iaa-ad-format-placement", "mg-design-10-2-iap-hybrid-structure", "mg-design-10-3-monetization-experience-balance",
"mg-design-11-1-one-pager-greenlight-doc", "mg-design-11-2-budget-schedule-estimate", "mg-design-11-3-greenlight-stop-loss",
"mg-design-12-1-prototype-scope-goal", "mg-design-12-2-playability-metric-test", "mg-design-12-3-prototype-iteration-decision",
"mg-design-13-1-revenue-dau-arpdau", "mg-design-13-2-hybrid-revenue-structure", "mg-design-13-3-revenue-forecast-scenario",
"mg-design-14-1-staffing-effort-estimate", "mg-design-14-2-outsourcing-asset-cost", "mg-design-14-3-milestone-risk-buffer",
"mg-design-15-1-review-material-process", "mg-design-15-2-design-doc-assembly", "mg-design-15-3-deliverable-acceptance-handoff"
],
"checks": [
{"id": "A1", "name": "评审材料齐备", "source": "ch15_review", "rule": "每项材料均有 owner / dueMilestone / passCriteria,且 owner 在角色表内"},
{"id": "A2", "name": "设计案装配完整", "source": "ch15_design_doc", "rule": "15 章均有产出物,45 个节 slug 命名与归属章号一致"},
{"id": "A3", "name": "交付清单与白名单一致", "source": "ch15_acceptance", "rule": "50 篇 slug 数量与命名规则一致,无重复、无越界"}
],
"handoff": {
"nextVolume": "引擎实现与体验打磨",
"nextPrefix": "mg-engine-",
"note": "卷二尚未落盘,此处只登记衔接点标题,不写跨卷 relref",
"entryPoints": [
{"from": "设计案 §2 平台约束页", "to": "卷二 1.1 引擎与框架选型", "carry": "包体预算与首屏约束作为选型评分的硬约束"},
{"from": "设计案 §3 玩法页", "to": "卷二 5.1 固定步长与确定性", "carry": "核心循环时长与反馈密度作为循环节奏输入"},
{"from": "设计案 §5 原型页", "to": "卷二 10.1 平台 API 抽象", "carry": "最小可玩原型的宿主依赖作为抽象层清单"},
{"from": "设计案 §5 资源页", "to": "卷二 2.2 分包与资源管线", "carry": "外包素材规格与图集粒度作为打包输入"},
{"from": "设计案 §6 交付页", "to": "卷二 15.1 逻辑单元测试", "carry": "验收项作为卷二测试用例的种子"}
]
}
}
handoff.note 特别写明「卷二尚未落盘,只登记衔接点标题,不写跨卷 relref」。这是有意的纪律:衔接点先写标题,等卷二真的落盘再补链接,否则会出现指向不存在页面的死链。
15.3.3 总验收脚本:三份配置串成一次核对
三条验收项分别依赖三份配置,如果各跑各的脚本,验收就成了三次独立操作。更好的做法是把它们串成一次总验收:一个脚本读三份配置、跑三条验收项、产出一份可读报告。
下面这段是 S 层总验收脚本。它读 ch15_review.json、ch15_design_doc.json、ch15_acceptance.json,跑 A1/A2/A3,产出汇总 JSON 与一份逐条 PASS/FAIL 的验收报告文本,并断言卷一五十篇 slug 白名单的数量、命名规则,以及与装配清单的交叉一致性。
#!/usr/bin/env python3
# file: sim/ch15_acceptance.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch15_acceptance.py --config config/ --out out/
"""卷一交付总验收:读 ch15_review.json / ch15_design_doc.json / ch15_acceptance.json
三份配置,跑三条验收项 A1/A2/A3,产出汇总 JSON 与一份可读的验收报告文本
(每条 PASS/FAIL),并断言卷一 50 篇 slug 白名单:数量、命名规则、与装配清单交叉一致。
任一验收项失败即非 0 退出。产出 out/ch15_acceptance.json + out/ch15_acceptance_report.txt。
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
SLUG_RE = re.compile(r"^mg-design-(\d{2})-([1-3])-([a-z0-9]+(?:-[a-z0-9]+)*)$")
FRONT_BACK = {"mg-design-toc", "mg-design-intro", "mg-design-preface",
"mg-design-appendix-a", "mg-design-appendix-b"}
def load(cfg_dir, name):
with open(os.path.join(cfg_dir, f"{name}.json"), encoding="utf-8") as f:
return json.load(f)
def check_a1(review):
"""评审材料:每项材料三件套齐备且 owner 在角色表内。"""
roles = {r["id"] for r in review["roles"]}
bad = [f"{m['id']}: 三件套缺项或 owner 非法" for m in review["materials"]
if not (m.get("owner") in roles and (m.get("dueMilestone") or "").strip()
and isinstance(m.get("passCriteria"), list) and m["passCriteria"])]
return (not bad), f"材料 {len(review['materials'])} 项 / 角色 {len(roles)} 个", bad
def check_a2(doc):
"""设计案装配:15 章各 3 节,45 节 slug 命名与章号一致,每章有产出物。"""
bad, seen = [], set()
if sorted(c.get("ch") for c in doc["chapters"]) != list(range(1, 16)):
bad.append("章号不是 1..15")
for ch in doc["chapters"]:
secs = ch.get("sections") or []
if len(secs) != 3:
bad.append(f"第 {ch['ch']} 章节数 {len(secs)} != 3")
for s in secs:
if s in seen:
bad.append(f"节 slug 重复:{s}")
seen.add(s)
m = SLUG_RE.match(s)
if not m or int(m.group(1)) != ch["ch"]:
bad.append(f"节 slug 不合规或章号错位:{s}")
if not (ch.get("outputs") or []):
bad.append(f"第 {ch['ch']} 章无产出物")
if len(seen) != 45:
bad.append(f"节 slug 合计 {len(seen)} != 45")
return (not bad), f"章 {len(doc['chapters'])} / 节 slug {len(seen)}", bad
def check_a3(acc, doc):
"""交付白名单:50 篇数量、命名规则,且与装配清单的 45 节交叉一致。"""
bad, wl = [], acc["whitelist50"]
if len(wl) != 50:
bad.append(f"白名单 {len(wl)} != 50")
if len(set(wl)) != len(wl):
bad.append("白名单有重复")
secs = [s for s in wl if s not in FRONT_BACK]
if len(secs) != 45:
bad.append(f"节 slug {len(secs)} != 45")
for s in secs:
m = SLUG_RE.match(s)
if not m or not (1 <= int(m.group(1)) <= 15):
bad.append(f"节 slug 不合规或越界:{s}")
for s in wl:
if not s.startswith(acc["prefix"]):
bad.append(f"slug 前缀错:{s}")
doc_secs = {s for ch in doc["chapters"] for s in (ch.get("sections") or [])}
if doc_secs != set(secs):
bad.append("白名单与装配清单不一致")
if FRONT_BACK - set(wl):
bad.append(f"缺前后附 slug:{sorted(FRONT_BACK - set(wl))}")
return (not bad), f"白名单 {len(wl)} 篇(节 {len(secs)} + 前后附 {len(FRONT_BACK)})", bad
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
args = ap.parse_args()
review = load(args.config, "ch15_review")
doc = load(args.config, "ch15_design_doc")
acc = load(args.config, "ch15_acceptance")
results = []
for cid, name, fn in (("A1", "评审材料齐备", lambda: check_a1(review)),
("A2", "设计案装配完整", lambda: check_a2(doc)),
("A3", "交付清单与白名单一致", lambda: check_a3(acc, doc))):
ok, detail, bad = fn()
results.append({"id": cid, "name": name, "pass": ok, "detail": detail, "problems": bad})
ok = all(r["pass"] for r in results)
ho = acc["handoff"]
os.makedirs(args.out, exist_ok=True)
summary = {"source": "sim/ch15_acceptance.py", "volume": acc["volume"], "prefix": acc["prefix"],
"expectedTotal": acc["expectedTotal"], "results": results,
"handoff": {"nextVolume": ho["nextVolume"], "nextPrefix": ho["nextPrefix"],
"entryPointCount": len(ho["entryPoints"])}, "ok": ok}
with open(os.path.join(args.out, "ch15_acceptance.json"), "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
lines = [f"卷一《{acc['volume']}》交付验收报告",
f"白名单基线:{acc['expectedTotal']} 篇(前缀 {acc['prefix']})", "-" * 56]
for r in results:
lines.append(f"[{'PASS' if r['pass'] else 'FAIL'}] {r['id']} {r['name']}({r['detail']})")
lines += [f" - {p}" for p in r["problems"]]
lines.append("-" * 56)
lines.append(f"卷二衔接:{ho['nextVolume']}(前缀 {ho['nextPrefix']}),衔接点 {len(ho['entryPoints'])} 个")
lines += [f" {e['from']} -> {e['to']}:{e['carry']}" for e in ho["entryPoints"]]
lines.append(f"总判定:{'通过(全部 PASS)' if ok else '不通过(存在 FAIL)'}")
text = "\n".join(lines) + "\n"
with open(os.path.join(args.out, "ch15_acceptance_report.txt"), "w", encoding="utf-8") as f:
f.write(text)
sys.stdout.write(text)
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())
本机实跑输出(脚本同时把这段文本写入 out/ch15_acceptance_report.txt):
卷一《市场品类与产品设计》交付验收报告
白名单基线:50 篇(前缀 mg-design-)
--------------------------------------------------------
[PASS] A1 评审材料齐备(材料 12 项 / 角色 5 个)
[PASS] A2 设计案装配完整(章 15 / 节 slug 45)
[PASS] A3 交付清单与白名单一致(白名单 50 篇(节 45 + 前后附 5))
--------------------------------------------------------
卷二衔接:引擎实现与体验打磨(前缀 mg-engine-),衔接点 5 个
设计案 §2 平台约束页 -> 卷二 1.1 引擎与框架选型:包体预算与首屏约束作为选型评分的硬约束
设计案 §3 玩法页 -> 卷二 5.1 固定步长与确定性:核心循环时长与反馈密度作为循环节奏输入
设计案 §5 原型页 -> 卷二 10.1 平台 API 抽象:最小可玩原型的宿主依赖作为抽象层清单
设计案 §5 资源页 -> 卷二 2.2 分包与资源管线:外包素材规格与图集粒度作为打包输入
设计案 §6 交付页 -> 卷二 15.1 逻辑单元测试:验收项作为卷二测试用例的种子
总判定:通过(全部 PASS)
三条验收项全 PASS。这份报告本身就是交付物的一部分——把它随设计案一起交出去,接手者不用重新核对结构,扫一眼报告就知道哪些项已经过了、白名单基准是什么、五个衔接点分别把什么带过去。A3 尤其关键:它断言的是「五十篇白名单」与「装配清单的四十五个节」交叉一致,任何一处笔误都会让两边对不上而报 FAIL。
15.3.4 交接:把接力棒递给卷二
卷一是「做什么」,卷二《引擎实现与体验打磨》是「怎么做出来」。交接不是一句「下一卷见」,而是把卷一的结论映射到卷二的具体章节,让卷二一开篇就知道自己接住了什么。配置里登记了五个衔接点:
| 从(卷一产出) | 到(卷二章节) | 带过去什么 |
|---|---|---|
| 设计案 §2 平台约束页 | 卷二 1.1 引擎与框架选型 | 包体预算与首屏约束,作为选型评分的硬约束 |
| 设计案 §3 玩法页 | 卷二 5.1 固定步长与确定性 | 核心循环时长与反馈密度,作为循环节奏输入 |
| 设计案 §5 原型页 | 卷二 10.1 平台 API 抽象 | 最小可玩原型的宿主依赖,作为抽象层清单 |
| 设计案 §5 资源页 | 卷二 2.2 分包与资源管线 | 外包素材规格与图集粒度,作为打包输入 |
| 设计案 §6 交付页 | 卷二 15.1 逻辑单元测试 | 验收项,作为卷二测试用例的种子 |
五个衔接点覆盖了卷二的选型、循环、平台抽象、资源管线、测试五条主线。特别注意第五个:卷一的验收项直接变成卷二的测试用例种子——本节写的 A1/A2/A3 是结构验收,卷二会把「核心循环跑得对、存档版本能迁移」这类行为验收接上去,两卷的验收口径由此连成一条线。
衔接点只写标题、不写跨卷链接,是因为卷二尚未落盘。等卷二就位,这些标题才会变成真正的 relref。先留标题、后补链接,是跨卷协作里避免死链的基本纪律。
15.3.5 卷一的收束
回看卷一这十五章,它走的是一条从外部约束收敛到内部交付的路径:
- 第 1–3 章向外看:市场有多大、榜单长什么样、品类怎么选。
- 第 4–5 章看约束与用户:平台允许做什么、给谁做。
- 第 6–9 章向内设计:核心循环、关卡、成长、社交。
- 第 10–14 章算账:怎么变现、值不值得做、要多少人多少时间。
- 第 15 章收口:把前十四章的结论装配成可评审、可验收、可交付的设计案。
这条路径的意义在于:卷一的每一个结论,都不是孤立的观点,而是可以被核对、被引用、被装配进设计案的产出物。这正是本套书「可运行」主张在卷一这一端的落点——不只是代码能跑,设计案本身也能被脚本核对。
小结
- 交付不是把文档发出去,而是逐项核对能不能被接住;验收有结构验收与内容验收两条线。
- D 层配置硬编码卷一五十篇 slug 白名单与三条验收项;S 层脚本把三份配置串成一次总验收,产出可读的验收报告文本。
- 本机实跑:A1/A2/A3 全 PASS,白名单五十篇(节 45 + 前后附 5),卷二衔接点五个。
- 交接把卷一结论映射到卷二五个章节;衔接点先写标题、后补链接,跨卷引用不留死链。
- 卷一十五章从市场收敛到交付,每个结论都是可核对、可引用、可装配的产出物。
卷一到此结束。卷二《引擎实现与体验打磨》会从引擎选型开始,把这本设计案一行行变成能跑起来的代码。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。