站内那篇代理发行合同与谈判指南 已经给了完整的合同范本与逐条解读,本节绝不重写它。上一节把「底线分成」算出来了,但底线要落进合同才有意义。《上线变现与长线运营》 讲了怎么把钱收回来,本节补最后一环:怎么确认一份合同把该写的都写了。范本解决的是「条款该怎么措辞」,本节解决的是「条款齐不齐、写全没有」——把它做成一份可校验的清单,而不是靠人肉逐条比对。
为什么用清单,而不是范本
范本是写作工具:告诉你一句话该怎么写。清单是检查工具:告诉你有哪几件事必须写、每件事下面有几个子项、漏了哪个。两者互补,但用途完全不同。合同最危险的错误从来不是「措辞不好」,而是某条根本不存在——你谈好了分成,合同里却漏了「审计与对账权」;你约定了保底,却没写「保底如何回收」。这些漏项在签约时看不出来,在结算时才会咬人。
所以本节的目标很具体:把一份代理发行合同拆成九类关键条款,每类标注是否必备、风险权重、以及必填子项,然后对一份待检合同逐类判定三态——complete(存在且子项写全,通过)/ incomplete(存在但必填子项没写全,回看补齐)/ missing(根本没出现,必须补)。判定不是「有 / 无」的二元:因为「写了但没写全」和「压根没写」是两种不同的风险,处置方式也不同。
条款分类与风险权重
九类条款按必备性分两组,按风险权重分三档。必备条款缺失后果最重,风险权重也最高:
| 条款 | 必备 | 权重 | 必填子项 |
|---|---|---|---|
| 授权范围与发行区域 | 是 | 3 | territory / platforms / exclusivity |
| 分成比例与结算 | 是 | 3 | sharePermille / settlementCycle / reporting |
| 知识产权归属 | 是 | 3 | ipOwner / derivative / revert |
| 终止与违约 | 是 | 3 | terminationForCause / cure / survival |
| 授权期限与续约 | 是 | 2 | startDate / endDate / renewal |
| 保底与预付 | 否 | 2 | minGuarantee / recoup |
| 审计与对账权 | 否 | 2 | auditRight / recordKeep |
| 数据与用户归属 | 否 | 2 | dataOwnership / privacy |
| 保密 | 否 | 1 | confidentiality / term |
权重不是随便给的:授权范围、分成、知识产权、终止这四类一旦出问题,会直接影响你能不能拿回游戏、能不能拿到钱,所以权重最高;保底、审计、数据是「有更好」的增强项;保密是标配但影响面小。风险分按「权重 × 档位」累加,再对「全条款全缺失」归一成千分比。
D 层:清单齐备性校验脚本
下面这段是 D 层校验脚本 sim/o10_clauses_check.py。它读 config/o10_clauses.json 的条款定义与一份待检合同,逐类判定三态、算加权风险分与三档覆盖率,给出 PASS / REVIEW / BLOCK 判定,产出 JSON + CSV。
#!/usr/bin/env python3
# file: sim/o10_clauses_check.py
# tier: D
# verify: /opt/homebrew/bin/python3 sim/o10_clauses_check.py --config config/ --out out/
"""代理发行合同「关键条款清单」齐备性校验(D 层:清单结构 / lint 校验)。
读 config/o10_clauses.json,对每类条款判定三态:missing(没出现)/ incomplete(出现但
必填子项没写全)/ complete(写全)。风险分按 权重 × 档位(必备缺失 ×3000‰、必备不齐
×1000‰、可选缺失 ×1000‰、可选不齐 ×0‰)求和,再对「全条款全缺失」归一成千分比。
判定:任一必备缺失 → BLOCK;任一必备不齐 → REVIEW;否则 PASS。
全程整数、地板除,无浮点。产出 out/o10_clauses_check.json + .csv。
清单只是「结构与齐备性」的清单化,不是真实合同范本;法律效力需真实业务环境,命令行
不可复现(X)。本文不提供任何法律意见。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
def load(d):
with open(os.path.join(d, "o10_clauses.json"), encoding="utf-8") as f:
return json.load(f)
def check(cfg):
w = cfg["riskWeightPermille"]
present = {c["typeId"]: set(c["fields"]) for c in cfg["contract"]}
rows, risk, max_risk = [], 0, 0
counts = {"complete": 0, "incomplete": 0, "missing": 0}
ft = fp = mft = mfp = mm = mi = 0
for t in cfg["clauseTypes"]:
req, have = t["fields"], present.get(t["id"], set())
if t["id"] not in present:
status = "missing"
elif set(req) <= have:
status = "complete"
else:
status = "incomplete"
counts[status] += 1
miss = [f for f in req if f not in have]
if status == "missing":
risk += t["weight"] * (w["mandatoryMissing"] if t["mandatory"] else w["optionalMissing"]) // 1000
elif status == "incomplete":
risk += t["weight"] * (w["mandatoryIncomplete"] if t["mandatory"] else w["optionalIncomplete"]) // 1000
max_risk += t["weight"] * w["mandatoryMissing"] // 1000
ft += len(req)
fp += sum(1 for f in req if f in have)
if t["mandatory"]:
mft += len(req)
mfp += sum(1 for f in req if f in have)
mm += status == "missing"
mi += status == "incomplete"
rows.append({"typeId": t["id"], "name": t["name"], "mandatory": t["mandatory"],
"weight": t["weight"], "status": status, "missingFields": miss,
"presentFields": [f for f in req if f in have]})
clause_cov = len(present) * 1000 // len(cfg["clauseTypes"])
field_cov = fp * 1000 // ft if ft else 0
mand_field_cov = mfp * 1000 // mft if mft else 0
risk_permille = risk * 1000 // max_risk if max_risk else 0
verdict = "BLOCK" if mm else ("REVIEW" if mi else "PASS")
return {"rows": rows, "counts": counts, "riskPoints": risk, "maxRiskPoints": max_risk,
"riskPermille": risk_permille, "clauseCoveragePermille": clause_cov,
"fieldCoveragePermille": field_cov, "mandatoryFieldCoveragePermille": mand_field_cov,
"mandatoryMissing": mm, "mandatoryIncomplete": mi, "verdict": verdict}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
a = ap.parse_args()
cfg = load(a.config)
rep = check(cfg)
c = rep["counts"]
digest = (f'present={len(cfg["contract"])},complete={c["complete"]},incomplete={c["incomplete"]},'
f'missing={c["missing"]},risk={rep["riskPoints"]},riskPermille={rep["riskPermille"]},'
f'clause={rep["clauseCoveragePermille"]},field={rep["fieldCoveragePermille"]},'
f'mandField={rep["mandatoryFieldCoveragePermille"]},verdict={rep["verdict"]}')
os.makedirs(a.out, exist_ok=True)
with open(os.path.join(a.out, "o10_clauses_check.csv"), "w", encoding="utf-8", newline="") as f:
wr = csv.writer(f)
wr.writerow(("typeId", "name", "mandatory", "weight", "status", "presentFields", "missingFields"))
for r in rep["rows"]:
wr.writerow([r["typeId"], r["name"], r["mandatory"], r["weight"], r["status"],
"|".join(r["presentFields"]), "|".join(r["missingFields"])])
report = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"reviewThresholdPermille": cfg["reviewThresholdPermille"], **rep, "digest": digest,
"csv": os.path.join(a.out, "o10_clauses_check.csv")}
with open(os.path.join(a.out, "o10_clauses_check.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps({"counts": c, "riskPoints": rep["riskPoints"], "riskPermille": rep["riskPermille"],
"clauseCoveragePermille": rep["clauseCoveragePermille"],
"fieldCoveragePermille": rep["fieldCoveragePermille"],
"mandatoryFieldCoveragePermille": rep["mandatoryFieldCoveragePermille"],
"mandatoryMissing": rep["mandatoryMissing"], "verdict": rep["verdict"],
"digest": digest}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
条款定义与待检合同放在 config/o10_clauses.json(D 层配置)。它只是「结构与齐备性」的清单化,不是真实合同范本,也不构成法律意见:
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"note": "代理发行合同关键条款清单与齐备性校验的建模数据。口径是「行业惯例」的公开可观察机制,只是「条款是否被覆盖 / 是否写全必填子项」的清单化结构,不是任何真实合同范本,也不构成法律意见。clauseTypes[].mandatory=是否必备条款;weight=风险权重;fields=该条款必填子项。contract[]=一份待检合同里已出现的条款及其已写子项。真实合同范本与法律效力需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
"riskWeightPermille": { "mandatoryMissing": 3000, "mandatoryIncomplete": 1000, "optionalMissing": 1000, "optionalIncomplete": 0 },
"reviewThresholdPermille": 300,
"clauseTypes": [
{ "id": "scope", "name": "授权范围与发行区域", "weight": 3, "mandatory": true, "fields": ["territory", "platforms", "exclusivity"] },
{ "id": "term", "name": "授权期限与续约", "weight": 2, "mandatory": true, "fields": ["startDate", "endDate", "renewal"] },
{ "id": "revshare", "name": "分成比例与结算", "weight": 3, "mandatory": true, "fields": ["sharePermille", "settlementCycle", "reporting"] },
{ "id": "ip", "name": "知识产权归属", "weight": 3, "mandatory": true, "fields": ["ipOwner", "derivative", "revert"] },
{ "id": "termination", "name": "终止与违约", "weight": 3, "mandatory": true, "fields": ["terminationForCause", "cure", "survival"] },
{ "id": "mg", "name": "保底与预付", "weight": 2, "mandatory": false, "fields": ["minGuarantee", "recoup"] },
{ "id": "audit", "name": "审计与对账权", "weight": 2, "mandatory": false, "fields": ["auditRight", "recordKeep"] },
{ "id": "confidential", "name": "保密", "weight": 1, "mandatory": false, "fields": ["confidentiality", "term"] },
{ "id": "data", "name": "数据与用户归属", "weight": 2, "mandatory": false, "fields": ["dataOwnership", "privacy"] }
],
"contract": [
{ "typeId": "scope", "fields": ["territory", "platforms", "exclusivity"] },
{ "typeId": "term", "fields": ["startDate", "endDate"] },
{ "typeId": "revshare", "fields": ["sharePermille", "settlementCycle"] },
{ "typeId": "ip", "fields": ["ipOwner", "derivative", "revert"] },
{ "typeId": "termination", "fields": ["terminationForCause", "cure"] },
{ "typeId": "confidential", "fields": ["confidentiality"] }
]
}
在本机跑 /opt/homebrew/bin/python3 sim/o10_clauses_check.py --config config/ --out out/,这份待检合同的结果是(同机相对量级,绝对数值不可移植):
| 条款 | 必备 | 权重 | 状态 | 缺的子项 |
|---|---|---|---|---|
| 授权范围与发行区域 | 是 | 3 | complete | — |
| 分成比例与结算 | 是 | 3 | incomplete | reporting |
| 知识产权归属 | 是 | 3 | complete | — |
| 终止与违约 | 是 | 3 | incomplete | survival |
| 授权期限与续约 | 是 | 2 | incomplete | renewal |
| 保密 | 否 | 1 | incomplete | term |
| 保底与预付 | 否 | 2 | missing | 整条 |
| 审计与对账权 | 否 | 2 | missing | 整条 |
| 数据与用户归属 | 否 | 2 | missing | 整条 |
统计下来是 complete 2 / incomplete 4 / missing 3,加权风险分 14 分(满分 63 分),风险千分比 222‰;三档覆盖率是条款 666‰、子项 565‰、必备子项 800‰。判定 REVIEW——没有必备条款整条缺失(否则 BLOCK),但有三条必备条款「写了没写全」(reporting / survival / renewal),必须回看补齐。这正是三态的价值:如果只看「必备条款在不在」,这份合同是 100% 覆盖的假象;三态才暴露出三个隐蔽的漏洞。
P 层内核:跨语言对拍,证明判定没写错
一张表看不出「是判定对」还是「碰巧看着对」。本套用一份 P 层 TypeScript 内核 core/src/o10/clauses.ts 复算同一套判定:同样的三态、同样的权重、同样的归一化。它读同一份 config/o10_clauses.json,产出与 Python 逐字符一致的摘要。
// file: core/src/o10/clauses.ts
// tier: P
// verify: node --test 'core/test/o10_*.test.ts'
//
// 合同关键条款清单齐备性内核:与 sim/o10_clauses_check.py 逐位等价的 TypeScript 对偶实现。
// 读同一份 config/o10_clauses.json;全整数、地板除、无浮点;只用可擦除语法(禁 enum / namespace)。
export interface ClauseType {
readonly id: string; readonly name: string; readonly weight: number;
readonly mandatory: boolean; readonly fields: readonly string[];
}
export interface ClauseConfig {
readonly version: number; readonly game: string;
readonly riskWeightPermille: {
readonly mandatoryMissing: number; readonly mandatoryIncomplete: number;
readonly optionalMissing: number; readonly optionalIncomplete: number;
};
readonly reviewThresholdPermille: number;
readonly clauseTypes: readonly ClauseType[];
readonly contract: readonly { readonly typeId: string; readonly fields: readonly string[] }[];
}
export interface ClauseRow {
readonly typeId: string; readonly name: string; readonly mandatory: boolean;
readonly weight: number; readonly status: string; readonly missingFields: readonly string[];
}
export interface ClauseResult {
readonly rows: ClauseRow[]; readonly complete: number; readonly incomplete: number;
readonly missing: number; readonly riskPoints: number; readonly maxRiskPoints: number;
readonly riskPermille: number; readonly clauseCoveragePermille: number;
readonly fieldCoveragePermille: number; readonly mandatoryFieldCoveragePermille: number;
readonly mandatoryMissing: number; readonly mandatoryIncomplete: number; readonly verdict: string;
}
/** 齐备性校验:三态判定 + 加权风险分 + 覆盖率 + 判定。 */
export function check(cfg: ClauseConfig): ClauseResult {
const w = cfg.riskWeightPermille;
const present = new Map<string, Set<string>>();
for (const c of cfg.contract) present.set(c.typeId, new Set(c.fields));
const rows: ClauseRow[] = [];
let risk = 0; let maxRisk = 0;
let complete = 0; let incomplete = 0; let missing = 0;
let fieldTotal = 0; let fieldPresent = 0; let mandFieldTotal = 0; let mandFieldPresent = 0;
let mandMissing = 0; let mandIncomplete = 0;
for (const t of cfg.clauseTypes) {
const have = present.get(t.id) ?? new Set<string>();
let status: string;
if (!present.has(t.id)) status = "missing";
else if (t.fields.every((f) => have.has(f))) status = "complete";
else status = "incomplete";
if (status === "complete") complete += 1;
else if (status === "incomplete") incomplete += 1;
else missing += 1;
const missingFields = t.fields.filter((f) => !have.has(f));
if (status === "missing") {
risk += Math.floor((t.weight * (t.mandatory ? w.mandatoryMissing : w.optionalMissing)) / 1000);
} else if (status === "incomplete") {
risk += Math.floor((t.weight * (t.mandatory ? w.mandatoryIncomplete : w.optionalIncomplete)) / 1000);
}
maxRisk += Math.floor((t.weight * w.mandatoryMissing) / 1000);
fieldTotal += t.fields.length;
fieldPresent += t.fields.filter((f) => have.has(f)).length;
if (t.mandatory) {
mandFieldTotal += t.fields.length;
mandFieldPresent += t.fields.filter((f) => have.has(f)).length;
if (status === "missing") mandMissing += 1;
else if (status === "incomplete") mandIncomplete += 1;
}
rows.push({ typeId: t.id, name: t.name, mandatory: t.mandatory, weight: t.weight, status, missingFields });
}
const clauseCov = Math.floor((cfg.contract.length * 1000) / cfg.clauseTypes.length);
const fieldCov = fieldTotal ? Math.floor((fieldPresent * 1000) / fieldTotal) : 0;
const mandFieldCov = mandFieldTotal ? Math.floor((mandFieldPresent * 1000) / mandFieldTotal) : 0;
const riskPermille = maxRisk ? Math.floor((risk * 1000) / maxRisk) : 0;
const verdict = mandMissing ? "BLOCK" : (mandIncomplete ? "REVIEW" : "PASS");
return { rows, complete, incomplete, missing, riskPoints: risk, maxRiskPoints: maxRisk,
riskPermille, clauseCoveragePermille: clauseCov, fieldCoveragePermille: fieldCov,
mandatoryFieldCoveragePermille: mandFieldCov, mandatoryMissing: mandMissing,
mandatoryIncomplete: mandIncomplete, verdict };
}
/** 跨语言对拍摘要:与 sim/o10_clauses_check.py 的 digest 逐字符一致。 */
export function digest(cfg: ClauseConfig, r: ClauseResult): string {
return `present=${cfg.contract.length},complete=${r.complete},incomplete=${r.incomplete},`
+ `missing=${r.missing},risk=${r.riskPoints},riskPermille=${r.riskPermille},`
+ `clause=${r.clauseCoveragePermille},field=${r.fieldCoveragePermille},`
+ `mandField=${r.mandatoryFieldCoveragePermille},verdict=${r.verdict}`;
}
配套测试 core/test/o10_clauses.test.ts 把 Python 落盘的摘要写成金标准,断言 TS 内核复算结果逐字符相等,并顺手验三态判定、风险分加权与判定阈值。在本机跑 node --test 'core/test/o10_*.test.ts',6 个用例全过:
✔ 跨语言一致:TS 内核摘要 == Python 金标准
✔ 三态判定:齐 / 不齐 / 缺各归其位
✔ 判定:无必备缺失、但有必备不齐 → REVIEW(非 PASS、非 BLOCK)
✔ 覆盖率:条款 666‰、子项 565‰、必备子项 800‰
✔ 缺项可定位:term 缺 renewal、revshare 缺 reporting
ℹ tests 6 / pass 6 / fail 0
两套独立实现给出同一个摘要:
digest = present=6,complete=2,incomplete=4,missing=3,risk=14,riskPermille=222,clause=666,field=565,mandField=800,verdict=REVIEW
只要有一边的三态判据、权重方向或归一化口径写错,摘要立刻分叉。这与卷二「一份逻辑、两种宿主」 是同一个信念:同一份清单,在 Python 校验器与 TS 内核里必须判得一模一样。
X 层:真实合同与法律效力不可复现
必须把边界划清。上面算的是清单侧:给定条款定义与一份合同,齐备性判定是什么。下面这些属于 X 层,需真实业务环境,命令行不可复现:
- 真实的合同范本与条款措辞(各发行商、各法域差异极大,本节只做「有没有写」的清单化,不评价措辞是否有利);
- 条款的法律效力与可执行性(一条写了的条款是否真的能约束对方,取决于适用法律与司法实践,本节不提供任何法律意见);
- 真实的谈判与签署过程(哪些条款能谈、哪些是发行商的铁律,是人与关系的事)。
工程上正确的做法是:把可复现的清单校验与不可复现的法律判断分开。清单保证「该写的一个不漏」,但清单说「齐了」不等于「安全了」——条款齐备只是及格线,条款内容是否对你有利、是否可执行,必须交给专业人士逐条审阅。本节能帮你的是:在找律师之前,先把明显缺失和写漏的地方补上。
自检表
| 检查项 | 合格标准 |
|---|---|
| 三态而非二元 | 区分「缺」与「不齐」,不把两者混为一谈 |
| 权重有依据 | 必备条款权重高,可选增强项权重低 |
| 覆盖率三算 | 条款 / 子项 / 必备子项分别算,不只报一个 |
| 判定分级 | 必备缺失 BLOCK、必备不齐 REVIEW、否则 PASS |
| 跨语言一致 | Python 校验器与 TS 内核摘要逐字符相同 |
| 边界清晰 | 真实范本与法律效力标 X;不写成「实测」,不提供法律意见 |
到这里,第 10 章把「要不要代理 → 底线在哪 → 合同怎么检」串成了一条闭环:10.1 决定要不要找发行商,10.2 算出谈判的红线,10.3 保证红线被写进合同。合同签了只是开始——真正要落地发行,还缺一块硬骨头:资质与版号。下一章我们处理它。
阅读导航:上一节:10.2 分成谈判的筹码与底线 · 下一节:11.1 版号申请材料与时间线 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。