15.1 立项评审材料与流程

立项评审不该是一次汇报,而应是一道可判定的闸门。本节把前十四章的产出登记成一份评审材料清单,给每项材料配上负责人、交付时点与通过判据三件套,再用一段 S 层脚本逐项断言缺项即失败,并给出 go / conditional-go / no-go 三档结论的触发条件,让评审有据可依、可被复核。

本节目标:把前十四章的产出登记成一份可判定的评审材料清单,让立项评审从「讲一遍故事」变成「逐项核对判据」。

站内有一个 PRD 原型目录 ,里面是一批可交互原型文档——每个产品一份 PRD,讲的是这个产品长什么样、有哪些页面、每个页面放什么。本章与它的分工是清楚的:那边是单品的原型文档,这边是把卷一前十四章的产出装配成一份可评审、可验收、可交付的设计案。原型回答「做出来是什么样」,设计案回答「凭什么现在投人做、做完怎么验收」。

前十四章一路把「做什么、怎么做、值不值得做」拆成了可量化的结论:品类评分、包体预算、留存假设、收益模型、开发排期。但这些结论散落在十四份文档里,如果直接丢进评审会,结果一定是制作人讲、老板听、谁印象深谁说了算。本章要做的,是把它们收束成一份能被逐条核对的东西。

15.1 立项评审材料与流程

15.1.1 评审不是汇报,而是一道闸门

大多数小团队的「立项评审」其实是汇报:主策花四十分钟讲一遍愿景,老板凭感觉点头或摇头。这种评审有两个致命问题——没有明确的输入(讲完就算过),没有明确的判据(过不过看心情)。它的产物是一句「那就做吧」,而这句话在下一次分歧出现时毫无约束力。

真正有用的评审是一道闸门(gate),它必须有三个明确的东西:

  • 输入:一组材料,缺任何一项,评审会开不成。
  • 判据:每项材料自带「通过」的标准,标准是可以被核对的,不是感觉。
  • 输出:一个明确结论——go(全量投入)、conditional-go(带条件的有限投入)、no-go(不做或回炉)。

闸门的意义在于可复核。三个月后项目卡住,回头看当时评审通过的判据,能回答「是我们判断错了,还是执行走样了」。没有判据的评审,事后连复盘都无从下手。

15.1.2 材料清单:三件套缺一不可

一份材料要能被评审,光有内容不够,必须同时具备三件套:

要素回答什么问题缺了会怎样
负责人 owner谁主答、谁兜底会上无人能拍板,问题悬空
交付时点 dueMilestone评审会前什么时候必须到位会上临时找数据,结论不可信
通过判据 passCriteria达到什么标准算「过」无法判定,只能靠印象

这三件套是本节的核心主张。一份再漂亮的一页纸,如果没写「负责人是谁、什么时候交、过没过看哪几条」,它在评审会上就是不可判定的。

下面这份是 D 层配置:它把十二项评审材料、五个角色、三个里程碑登记成唯一真源。每项材料都带齐了 owner / dueMilestone / passCriteria。

{
  "version": 1,
  "game": "merge-island",
  "note": "卷一立项评审材料清单(D 层)。每项材料必须给出负责人 owner / 交付时点 dueMilestone / 通过判据 passCriteria 三件套,缺一即不可评审。",
  "gate": {
    "name": "卷一立项评审",
    "timing": "M3 里程碑末,玩法原型验证结论产出之后",
    "decisionOptions": ["go", "conditional-go", "no-go"],
    "passRule": "全部 required 材料齐备,且每项通过其全部 passCriteria"
  },
  "roles": [
    {"id": "producer", "name": "制作人", "responsibility": "主持评审、汇总分歧、拍板 go/conditional-go/no-go"},
    {"id": "design-lead", "name": "主策", "responsibility": "主答品类方向、核心循环、关卡与成长"},
    {"id": "tech-lead", "name": "主程", "responsibility": "主答平台约束、包体预算、原型可行性"},
    {"id": "analyst", "name": "数据分析", "responsibility": "主答收益模型、口径与情景假设"},
    {"id": "finance", "name": "发行/财务", "responsibility": "主答开发预算、排期与止损线"}
  ],
  "milestones": [
    {"id": "M1", "name": "方向确认", "week": 2},
    {"id": "M2", "name": "玩法定稿", "week": 6},
    {"id": "M3", "name": "立项评审", "week": 10}
  ],
  "materials": [
    {"id": "market-one-pager", "name": "市场与机会一页纸", "fromChapter": 11, "owner": "design-lead", "dueMilestone": "M1", "required": true, "passCriteria": ["品类方向一句话说清", "差异化机会可被榜单数据支撑", "标注榜单观察时点"]},
    {"id": "genre-selection", "name": "品类选择与加权评分", "fromChapter": 3, "owner": "design-lead", "dueMilestone": "M1", "required": true, "passCriteria": ["给出权重与打分表", "对权重做敏感度测试", "备选品类不少于 3 个"]},
    {"id": "platform-constraint", "name": "平台约束与包体预算", "fromChapter": 4, "owner": "tech-lead", "dueMilestone": "M1", "required": true, "passCriteria": ["四条包体限额写成预算表", "首屏自足性有结论", "约束对玩法的裁剪点已列出"]},
    {"id": "user-lifecycle", "name": "目标用户与生命周期", "fromChapter": 5, "owner": "design-lead", "dueMilestone": "M2", "required": true, "passCriteria": ["画像场景具体可验证", "付费意愿分层有依据", "留存结构给出假设值"]},
    {"id": "core-loop", "name": "核心循环与最小原型", "fromChapter": 6, "owner": "design-lead", "dueMilestone": "M2", "required": true, "passCriteria": ["核心循环拆到动作级", "循环时长与反馈密度量化", "最小可玩原型范围明确"]},
    {"id": "level-curve", "name": "关卡与难度曲线", "fromChapter": 7, "owner": "design-lead", "dueMilestone": "M2", "required": true, "passCriteria": ["关卡结构分级", "难度曲线与心流区间对照", "校准参数可调"]},
    {"id": "progression", "name": "成长与放置收益", "fromChapter": 8, "owner": "design-lead", "dueMilestone": "M2", "required": true, "passCriteria": ["成长曲线给出锚点", "再来一局钩子明确", "离线收益有上限设计"]},
    {"id": "social", "name": "社交与裂变机制", "fromChapter": 9, "owner": "design-lead", "dueMilestone": "M3", "required": false, "passCriteria": ["社交面有载体", "K 因子给出假设区间", "有社交压力保护措施"]},
    {"id": "monetization-frontload", "name": "变现前置设计", "fromChapter": 10, "owner": "analyst", "dueMilestone": "M3", "required": true, "passCriteria": ["广告形态与场景清单", "内购点结构清楚", "变现与体验平衡点有依据"]},
    {"id": "prototype-result", "name": "玩法原型验证结论", "fromChapter": 12, "owner": "design-lead", "dueMilestone": "M2", "required": true, "passCriteria": ["可玩性指标达标", "迭代决策有记录", "结论可复现"]},
    {"id": "revenue-model", "name": "收益模型与情景分析", "fromChapter": 13, "owner": "analyst", "dueMilestone": "M3", "required": true, "passCriteria": ["DAU×ARPDAU 口径一致", "给出悲观/基准/乐观三情景", "假设参数全部标注"]},
    {"id": "budget-schedule", "name": "开发预算与排期", "fromChapter": 14, "owner": "finance", "dueMilestone": "M3", "required": true, "passCriteria": ["人力工时逐项列出", "外包素材成本有报价", "止损线写进文档"]}
  ]
}

注意两点。其一,材料不是按文档排的,是按评审时要回答的问题排的:每一项都对应前十四章里某一章的结论,fromChapter 就是溯源字段。其二,required 区分必交与可选——社交裂变标了 false,因为《合成小岛》的社交是加分项而非立项前提,这种取舍必须显式写进配置,而不是会上临时争论。

15.1.3 校验脚本:让缺项无处藏身

配置写完了,怎么保证它真的每项都齐?靠人眼扫一遍不可靠——十二项材料、每项三个字段、还有一个角色表和里程碑表要交叉核对。下面这段是 S 层校验脚本,它读上面那份配置,逐项断言三件套齐备、owner 属于角色表、dueMilestone 属于里程碑表、fromChapter 在 1–15 之内;任何一项缺漏,脚本以非 0 退出。

#!/usr/bin/env python3
# file: sim/ch15_check_review.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch15_check_review.py --config config/ --out out/
"""立项评审材料清单校验:读 config/ch15_review.json,断言每项评审材料都齐备
「负责人 owner / 交付时点 dueMilestone / 通过判据 passCriteria」三件套,
并校验负责人属于角色表、来源章号在 1–15 之内。任一材料缺项即非 0 退出。

产出 out/ch15_review.json(逐项结果 + 汇总)+ out/ch15_review.csv(逐材料一行)。
全程整数与字符串,无浮点。
"""

from __future__ import annotations

import argparse
import csv
import json
import os
import sys

REQUIRED_FIELDS = ("owner", "dueMilestone", "passCriteria")


def load(cfg_dir):
    with open(os.path.join(cfg_dir, "ch15_review.json"), encoding="utf-8") as f:
        return json.load(f)


def check_material(m, role_ids, milestone_ids, chapter_max):
    """返回该材料的缺项与非法项清单。空列表表示该材料通过。"""
    problems = []
    for field in REQUIRED_FIELDS:
        val = m.get(field)
        if field == "passCriteria":
            if not isinstance(val, list) or not val:
                problems.append(f"缺 passCriteria(通过判据为空)")
            elif any((not isinstance(c, str)) or (not c.strip()) for c in val):
                problems.append("passCriteria 含空判据")
        else:
            if not isinstance(val, str) or not val.strip():
                problems.append(f"缺 {field}")
    if m.get("owner") and m["owner"] not in role_ids:
        problems.append(f"owner {m['owner']} 不在角色表")
    if m.get("dueMilestone") and m["dueMilestone"] not in milestone_ids:
        problems.append(f"dueMilestone {m['dueMilestone']} 不在里程碑表")
    ch = m.get("fromChapter")
    if not isinstance(ch, int) or not (1 <= ch <= chapter_max):
        problems.append(f"fromChapter {ch} 越界")
    return problems


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", default="config/")
    ap.add_argument("--out", default="out/")
    args = ap.parse_args()

    cfg = load(args.config)
    role_ids = {r["id"] for r in cfg["roles"]}
    milestone_ids = {m["id"] for m in cfg["milestones"]}
    materials = cfg["materials"]

    rows, failures = [], []
    for m in materials:
        problems = check_material(m, role_ids, milestone_ids, 15)
        rows.append({
            "id": m["id"],
            "name": m["name"],
            "fromChapter": m["fromChapter"],
            "owner": m.get("owner"),
            "dueMilestone": m.get("dueMilestone"),
            "criteriaCount": len(m.get("passCriteria") or []),
            "required": bool(m.get("required")),
            "pass": not problems,
            "problems": problems,
        })
        if problems:
            failures.append((m["id"], problems))

    covered_chapters = sorted({m["fromChapter"] for m in materials})
    required_missing = [m["id"] for m in materials
                        if m.get("required") and check_material(m, role_ids, milestone_ids, 15)]

    os.makedirs(args.out, exist_ok=True)
    report = {
        "source": "sim/ch15_check_review.py",
        "gate": cfg["gate"]["name"],
        "materialCount": len(materials),
        "requiredCount": sum(1 for m in materials if m.get("required")),
        "roleCount": len(cfg["roles"]),
        "coveredChapters": covered_chapters,
        "coveragePermille": len(covered_chapters) * 1000 // 15,
        "materials": rows,
        "failures": [{"id": i, "problems": p} for i, p in failures],
        "ok": not failures,
    }
    with open(os.path.join(args.out, "ch15_review.json"), "w", encoding="utf-8") as f:
        json.dump(report, f, ensure_ascii=False, indent=2)
    with open(os.path.join(args.out, "ch15_review.csv"), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(["id", "name", "fromChapter", "owner", "dueMilestone",
                    "criteriaCount", "required", "pass"])
        for r in rows:
            w.writerow([r["id"], r["name"], r["fromChapter"], r["owner"], r["dueMilestone"],
                        r["criteriaCount"], str(r["required"]).lower(), str(r["pass"]).lower()])

    print(f"gate={cfg['gate']['name']} materials={len(materials)} "
          f"required={report['requiredCount']} roles={len(cfg['roles'])}")
    print(f"覆盖章号 {covered_chapters}({len(covered_chapters)}/15)")
    for r in rows:
        mark = "PASS" if r["pass"] else "FAIL"
        print(f"  {mark} {r['id']:<24} owner={r['owner']:<12} due={r['dueMilestone']} "
              f"criteria={r['criteriaCount']}")
    for mid, probs in failures:
        print(f"  FAIL {mid}: {'; '.join(probs)}")
    if required_missing:
        print(f"  FAIL 必交材料缺项:{required_missing}")
    print("== 评审材料清单:OK" if report["ok"] else "== 评审材料清单:FAIL")
    return 0 if report["ok"] else 1


if __name__ == "__main__":
    sys.exit(main())

本机实跑输出:

gate=卷一立项评审 materials=12 required=11 roles=5
覆盖章号 [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14](12/15)
  PASS market-one-pager         owner=design-lead  due=M1 criteria=3
  PASS genre-selection          owner=design-lead  due=M1 criteria=3
  PASS platform-constraint      owner=tech-lead    due=M1 criteria=3
  PASS user-lifecycle           owner=design-lead  due=M2 criteria=3
  PASS core-loop                owner=design-lead  due=M2 criteria=3
  PASS level-curve              owner=design-lead  due=M2 criteria=3
  PASS progression              owner=design-lead  due=M2 criteria=3
  PASS social                   owner=design-lead  due=M3 criteria=3
  PASS monetization-frontload   owner=analyst      due=M3 criteria=3
  PASS prototype-result         owner=design-lead  due=M2 criteria=3
  PASS revenue-model            owner=analyst      due=M3 criteria=3
  PASS budget-schedule          owner=finance      due=M3 criteria=3
== 评审材料清单:OK

逐条读这份输出。十二项材料全部 PASS,required 为 11(社交那项可选)。覆盖章号是 3–14,不含 1、2、15:第 1、2 章的市场与竞品结论被并进了「市场与机会一页纸」和「品类选择」两项里,第 15 章本身是评审章,不给自己准备材料。这不是缺漏,而是材料粒度按问题而非按章节的自然结果——脚本报出覆盖率是 12/15,是让人一眼看到「哪些章的结论还没有材料承接」,而不是要求每章各配一项。

这套脚本真正的价值在失败时。把任意一项材料的 dueMilestone 删掉再跑,输出会变成:

  FAIL market-one-pager         owner=design-lead  due=None criteria=3
  FAIL market-one-pager: 缺 dueMilestone
  FAIL 必交材料缺项:['market-one-pager']
== 评审材料清单:FAIL

脚本以退出码 1 结束。这意味着这份校验可以直接挂进交付流水线:材料不齐,评审会就开不成——闸门是硬的,不是靠自觉。数字层面这些都是本机相对量级,换一份材料清单结论会变;可移植的是这套「配置即真源 + 脚本断言」的流程。

15.1.4 评审流程与角色分工

材料齐了,流程也要固定下来,否则会开成漫谈。一条可复用的评审流程是四步:

  1. 会前核对:跑一遍校验脚本,材料不全直接推迟,不占用会议时间。
  2. 逐项过判据:每项材料由负责人主答,评审人逐条对照 passCriteria 打勾或打叉,不做整体印象评价。
  3. 汇总分歧:只讨论打叉的条目,打勾的直接过。分歧点记进会议纪要。
  4. 给结论:制作人基于打叉数量与严重度,给 go / conditional-go / no-go。

角色分工也在配置里登记好了:主策主答品类、循环、关卡、成长;主程主答平台约束与包体;数据分析主答收益模型与口径;发行/财务主答预算与止损线;制作人主持并拍板。谁主答,谁就要为那一项的判据负责——这是把「负责人」写进材料三件套的意义。

15.1.5 三种决策与触发条件

评审的输出不是「做/不做」两分,而是三档,每档有明确的触发条件:

结论触发条件后续动作
go全部 required 材料通过,无严重打叉全量投入,进入卷二引擎与实现
conditional-go少数非致命项打叉带条件立项,限定范围,设定复查点
no-go致命项打叉(如收益模型不成立、预算远超止损线)回炉或放弃,产出物归档

关键在于 conditional-go 这一档。很多团队只有「做」和「不做」,于是把「有点问题但想做」硬塞进 go,问题一路带到开发中。把中间档显式定义出来,问题就被写成了条件与复查点,而不是被乐观情绪盖过去。

小结

  • 立项评审是一道闸门,有明确的输入(材料)、判据(passCriteria)与输出(go / conditional-go / no-go),它的价值是可复核。
  • 每项材料必须有三件套:负责人、交付时点、通过判据。缺任一即不可判定。
  • D 层配置把十二项材料、五个角色、三个里程碑写成唯一真源;S 层脚本逐项断言三件套齐备,缺项以非 0 退出,可直接挂进交付流水线。
  • 本机实跑:十二项材料全 PASS,覆盖章号 3–14(材料粒度按问题而非按章);删掉一项 dueMilestone 后脚本报 FAIL 并以退出码 1 结束。
  • 评审给三档结论,conditional-go 把「有点问题但想做」写成条件与复查点,避免问题带进开发。

材料清单是「评审要什么」,下一节把前十四章的产出装配成设计案本身——那是设计案的目录结构问题。

阅读导航:上一节:里程碑与风险缓冲 · 下一节:设计案装配 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「minigame」更多文章

  1. 附录 F:全书公式、参数与数据集索引
  2. 9.3 同意管理与数据出境
  3. 9.2 COPPA 与儿童向游戏的限制