以下情境为教学假设,人物与数字不代表经核实的真实案例。
卷二前 12 章分别拆过合同的八个部分、责任上限、账期、坏账、保险、IP 与渠道。拆开讲有一个副作用:读者知道每个条款重要,却不知道在自己的合同里先看哪一条。收口要做的事,是把条款重新压回一张表,让每条条款旁边站着一个金额。
这一节搭一张 8 列 × 3 行的合同矩阵。列是条款维度,行是三个案例;每格不是一句描述,而是「条款值 + 它在本卷算出的金额」。矩阵搭完后,再用十项评分给三份合同打一个 0 到 10 的强度分,最后说明这张表怎么带进卷三。
为什么合同要压成矩阵
合同文本按时间顺序写:当事人、服务范围、价格、期限、责任、IP、数据、终止。这个顺序适合阅读,不适合比较。三家公司的合同放在一起时,真正要比的是同一个维度上的不同选择:账期是 Net 15 还是 Net 30,责任上限是 12 个月费用还是合同金额 100%。
矩阵的作用是强制对齐。它要求每一行回答同一个问题,每一格给出同一种东西。只要有一格写不出金额,就说明这条条款还没有被翻译成财务后果;在本书的口径里,算不出来的条款等于没有条款。
矩阵也不能只有条款值。Net 30 是一个术语,12,684 美元才是它对现金的占用。把两者放在同一格里,读者才能看到「同样的 Net 30,对不同生意是不同量级的压力」。这也是本节所有金额都来自前 12 章、而不是重新假设一套数字的原因。
三案例的八列矩阵
下表是矩阵的主体。每格第一行是条款值,第二行是对应金额;金额口径来自本卷已发布的章节,不是本节新造的。
| 条款 | LedgerLite | Tidecast | Nord Studio |
|---|---|---|---|
| 当事人 | 新加坡 Pte Ltd;260 家订阅客户 $240,240 年收入 | Wyoming LLC;Steam 占净得 74.7% $138,171 净收入 | 英国 Ltd;两名合伙人各 50% $154,320 年收入 |
| 账期 | Net 15;平均占用 $9,873 实际 DSO 18.2 天 | 平台月结 30 天;未结算 $8,597.25 90 天收入暴露 $19,079 | Net 30;平均占用 $12,684 实际 DSO 26.6 天 |
| 责任上限 | 12 个月费用;公司层面 $240,240 单份合同 $924 | 平台协议单方上限;按平台费 $44,215 计 卖方无谈判空间 | 合同金额 100%;单项目 $8,500 年咨询收入 $112,200 |
| 续约窗口 | 年付 cohort 60 天窗口 $11,847 30 天窗口 $5,924 | 平台定义,无合同锁定 可锁定金额 $0 | 咨询续约率 70%;风险收入 $33,660 60 天额外窗口 $5,533 |
| SLA | 99.5%;月度抵扣 10% 年上限 $24,024 | 平台可用性条款;停机一周 $2,657 12 月一周 $8,060 | 99.0%;产品线 5% 抵扣 $2,106 若含咨询线 $7,716 |
| IP 归属 | 自有代码与商标;AGPL 合规成本 $85,672 占 ARR 35.7% | 音乐买断、不得转授权;3 年保底 $180,000 整体出售锚 $248,708 | 交付物归客户、产品归工作室 单合同交付物 $8,500 |
| 数据条款 | DPA + SCC;首年 $5,400 泄露自留 $10,000 | 客户数据在平台,无导出承诺 可保证金额 $0 | 无 DPA,泄露通知未约定 可保证金额 $0 |
| 终止 | 提前 30 天;通知期收入 $5,924 占 SDE 4.5% | 平台下架;剩余授权缺口 $38,000 Steam 占销量 80% | 合伙人退出买断 $77,220 分 24 个月支付 |
这张表读起来密,但结构很清楚。LedgerLite 的每一格都有金额,因为订阅合同的条款可以被拆到客户和月份;Tidecast 有两格是 0,不是因为没有风险,而是因为平台条款不允许卖方锁定任何金额;Nord 的金额最大的一格不在合同里,在合伙人退出条款里。
每格金额从哪里来
矩阵里最容易被误读的是责任上限。LedgerLite 的公司层面上限是 $240,240,但单份合同只有 $924;这两个数字不矛盾,前者是 260 份合同加总后的风险预算,后者是任何一家客户能索赔的上限。比较三家时要用同一个层级:公司对公司,合同对合同。拿 $240,240 去比 Nord 单项目的 $8,500,会得出「LedgerLite 风险高 28 倍」的错误结论。
账期那一行的金额是平均现金占用,不是年末应收。LedgerLite 的 Net 15 对应 $9,873,Nord 的 Net 30 对应 $12,684,Tidecast 的平台月结 30 天对应 $8,597.25。年末应收是时点余额,平均占用是全年水平;矩阵用后者,因为条款影响的是全年现金,不是某一天的表。
续约窗口的金额是「窗口锁定的收入」,不是「续约概率」。LedgerLite 的年付 cohort 是 $72,072,60 天窗口锁 $11,847;这 $11,847 是合同期内已经确定的收入,不是明年能不能续的期望值。把锁定收入和续约概率混在一起,会把一个条款问题算成一个销售问题。
SLA 的金额是抵扣上限,不是停机损失。LedgerLite 的年度最大抵扣是 $24,024,Nord 的产品线是 $2,106;如果 Nord 把咨询线也算进 SLA 基数,抵扣会变成 $7,716。同一份 99.0% 的承诺,基数不同,金额差 3.7 倍。
IP 那一行,LedgerLite 放的是 AGPL 合规成本 $85,672,Tidecast 放的是三年授权保底 $180,000,Nord 放的是单合同交付物 $8,500。三个数字不是同一种东西,但它们回答同一个问题:IP 条款一旦出事,最坏要花多少钱才能把资产拿回来或换掉。
数据条款的两个 0 需要单独解释。Tidecast 的客户数据在平台,Nord 没有 DPA,矩阵给 0 不是说数据风险为零,而是说「合同没有给出可计算的保障金额」。0 在矩阵里是一个警告,不是一个安全值。
终止那一行,LedgerLite 是通知期内的收入 $5,924,Tidecast 是剩余授权缺口 $38,000,Nord 是合伙人买断 $77,220。三者的共同点是:终止条款的金额往往比责任上限更接近真实退出成本,因为它包含了过渡、替换和重新签约的时间。
条款强度评分:十项,满分 10
金额回答「最坏多少」,评分回答「条款写全了没有」。评分用十项检查,每项 1 分,满分 10 分。评分标准是教学口径,不是法律意见;它的用途是横向比较,不是判断合同是否有效。
| # | 检查项 | LedgerLite | Tidecast | Nord Studio |
|---|---|---|---|---|
| 1 | 签约主体与收款主体一致 | ✓ | ✓ | ✓ |
| 2 | 验收条件可判定 | ✓ | ✓ | ✗ |
| 3 | 账期与逾期后果明确 | ✓ | ✓ | ✓ |
| 4 | 责任上限有明确基数 | ✓ | ✗ | ✓ |
| 5 | 续约窗口双向对称 | ✗ | ✗ | ✗ |
| 6 | SLA 有统计周期与抵扣基数 | ✓ | ✓ | ✓ |
| 7 | IP 归属或授权范围明确 | ✓ | ✓ | ✓ |
| 8 | 数据条款含通知时限 | ✓ | ✗ | ✗ |
| 9 | 终止后的过渡与导出可计算 | ✗ | ✗ | ✗ |
| 10 | 保险要求与保额匹配 | ✓ | ✗ | ✓ |
| 合计 | 8 | 5 | 6 |
三个案例里有两项是共同失分:续约窗口双向对称、终止后的过渡与导出;数据条款则是 Tidecast 与 Nord 的共同失分。这不是巧合。独立开发者的合同通常由客户或平台提供,卖方只能在「接受」和「不签」之间选;能改的往往是金额和期限,改不了的是对称性和退出安排。
LedgerLite 拿 8 分,落在强档。它失掉的两项是续约窗口和终止过渡:年付客户有 60 天窗口,但窗口是单向的,客户可以走,卖方不能同等提前退出;终止后的数据导出也没有写进合同。这两项不影响它现在的收入,但会影响买方的尽调结论。
Tidecast 拿 5 分,落在弱档。它失掉的是责任上限、续约、数据、终止和保险。平台协议的责任上限由平台单方定义,卖方没有谈判空间;客户数据在平台,导出承诺为零;平台下架后的剩余授权缺口是 $38,000。这三项加起来,解释了为什么它的合同矩阵看起来「简单」,实际风险却不低。
Nord 拿 6 分,落在及格档。它失掉的是验收、续约、数据和终止。咨询合同的验收条件偏软,续约率 70% 是观察值而不是合同权利,数据条款缺失,合伙人退出只有口头约定。最后一项最贵:$77,220 的买断金额没有写进任何文件,退出时只能重新谈。
评分和金额要一起看。LedgerLite 的 8 分对应 $240,240 的责任上限和 $13,337 的未覆盖缺口;Tidecast 的 5 分对应 $180,000 的授权保底和 $232,430 的总缺口;Nord 的 6 分对应 $8,500 的单项目上限和 $27,462 的缺口。分数低的合同不一定金额大,但金额大的缺口通常出现在分数低的合同里。
矩阵怎么读:三组对照
第一组是责任上限与保险。LedgerLite 的上限 $240,240 由 $1M 网络安全险承接,单次最大损失 $280,947 下缺口只有 $10,000;Nord 的单项目上限 $8,500 由 $500K E&O 承接,专业责任缺口 $5,000;Tidecast 没有责任上限谈判空间,也没有保险,IP 缺口 $144,000、平台缺口 $88,430。三家的差别不在保额大小,而在「上限、保险、缺口」能不能连成一条线。责任上限的计算口径见 2.1 责任上限:最坏损失怎么算 ,保险与缺口的联动见 12.3 保险、备份与应急预案的联动 。
第二组是账期与应收。LedgerLite 的 Net 15 占用 $9,873,Nord 的 Net 30 占用 $12,684,Tidecast 的平台月结 30 天占用 $8,597.25。绝对值上 Nord 最高,但占 SDE 的比例是另一回事:LedgerLite 的应收是 SDE 的 9.2%,Tidecast 的平台未结算是 7.1%,Nord 是 32.8%。同样的 30 天,对 SDE 只有 $34,320 的 Nord 是三分之一个季度的 owner earnings。账期的完整成本见 3.1 账期的真实成本:Net 15、30、60 差多少 。
第三组是 IP 与终止。LedgerLite 的 AGPL 合规成本 $85,672 相当于一年 ARR 的 35.7%;Tidecast 的三年授权保底 $180,000 相当于整体出售锚 $248,708 的 72.4%;Nord 的合伙人买断 $77,220 相当于 4.5× SDE 估值 $154,440 的 50.0%。这三个比例说明,IP 和终止条款不是「出事才用」的备份条款,它们在正常退出时就会吃掉一大块交易对价。
矩阵还可以反过来用:先看哪一格的金额占 SDE 比例最高,再回去看那条条款。LedgerLite 最高的是责任上限(1.84× SDE),Tidecast 最高的是授权保底(1.49× SDE),Nord 最高的是合伙人买断(2.25× SDE)。三家的第一优先级因此不同:LedgerLite 先看保险,Tidecast 先看授权终止,Nord 先看合伙人退出。
写成代码:contract-matrix.ts
矩阵的代码只做三件事:固定八个条款的顺序、校验每格都有值和金额、用十项检查算强度分。它不判断条款是否合法,也不替读者谈判;它只保证三份合同被放在同一套坐标里。
buildContractMatrix 拒绝缺列的合同,因为缺一列就不是同一张表;scoreContract 要求恰好十项检查,少一项就报错,而不是按零分算——没检查不等于没风险。最弱案例按得分率排序,分数相同时保持输入顺序,保证多次运行结果一致。
// file: src/contract/contract-matrix.ts
// tier: L1
// verify: node --test test/contract/contract-matrix.test.ts
import { type Cents } from "../core/money.ts";
import { type Bps, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";
export type ClauseKey =
| "parties"
| "payment"
| "liability"
| "renewal"
| "sla"
| "ip"
| "data"
| "termination";
export type ClauseCell = {
value: string;
amountCents: Cents;
};
export type ContractCase = {
name: string;
clauses: Record<ClauseKey, ClauseCell>;
strengthChecks: boolean[];
};
export type MatrixClause = {
key: ClauseKey;
label: string;
value: string;
amountCents: Cents;
};
export type MatrixRow = {
name: string;
clauses: MatrixClause[];
score: number;
maxScore: number;
scoreBps: Bps;
grade: "strong" | "adequate" | "weak";
};
export type MatrixResult = {
rows: MatrixRow[];
weakestCase: string;
weakestScoreBps: Bps;
};
export const CLAUSE_ORDER: ClauseKey[] = [
"parties",
"payment",
"liability",
"renewal",
"sla",
"ip",
"data",
"termination",
];
export const CLAUSE_LABELS: Record<ClauseKey, string> = {
parties: "当事人",
payment: "账期",
liability: "责任上限",
renewal: "续约窗口",
sla: "SLA",
ip: "IP 归属",
data: "数据条款",
termination: "终止",
};
/** Ten checks, one point each. The list is a teaching rubric, not a legal test. */
export const STRENGTH_ITEMS: string[] = [
"签约主体与收款主体一致",
"验收条件可判定",
"账期与逾期后果明确",
"责任上限有明确基数",
"续约窗口双向对称",
"SLA 有统计周期与抵扣基数",
"IP 归属或授权范围明确",
"数据条款含通知时限",
"终止后的过渡与导出可计算",
"保险要求与保额匹配",
];
export const STRENGTH_MAX = STRENGTH_ITEMS.length;
function validAmount(value: Cents): boolean {
return Number.isInteger(value) && value >= 0;
}
function isClauseKey(value: string): value is ClauseKey {
return (CLAUSE_ORDER as string[]).includes(value);
}
/**
* Scores a contract on the ten-item rubric. Every item must be present as a
* boolean; a missing item is a gap in the review, not a silent zero. The
* grade lines are fixed teaching thresholds: 8 strong, 6 adequate, below 6
* weak. They compare review completeness, not legal enforceability.
*/
export function scoreContract(
checks: boolean[],
): Result<{ score: number; maxScore: number; scoreBps: Bps; grade: MatrixRow["grade"] }> {
if (checks.length !== STRENGTH_MAX) {
return fail(`expected ${STRENGTH_MAX} checks, got ${checks.length}`);
}
let score = 0;
for (const check of checks) {
if (typeof check !== "boolean") {
return fail("every check must be a boolean");
}
if (check) {
score += 1;
}
}
const scoreBps = divide(score, STRENGTH_MAX);
const grade: MatrixRow["grade"] =
scoreBps >= 8000 ? "strong" : scoreBps >= 6000 ? "adequate" : "weak";
return ok({ score, maxScore: STRENGTH_MAX, scoreBps, grade });
}
/**
* Builds the eight-column matrix. Each cell carries both the clause value and
* the amount that the clause puts at stake in the current chapter. The two
* are deliberately separate: "Net 30" is a term, 12,684 dollars is its cash
* consequence. The weakest case is the lowest score ratio, with input order
* breaking ties so repeated runs agree.
*/
export function buildContractMatrix(cases: ContractCase[]): Result<MatrixResult> {
if (cases.length === 0) {
return fail("at least one contract case is required");
}
const rows: MatrixRow[] = [];
for (const item of cases) {
if (item.name.trim() === "") {
return fail("case name cannot be empty");
}
const clauses: MatrixClause[] = [];
for (const key of CLAUSE_ORDER) {
const cell = item.clauses[key];
if (cell === undefined) {
return fail(`${item.name} is missing clause ${key}`);
}
if (!isClauseKey(key)) {
return fail(`unknown clause ${key}`);
}
if (cell.value.trim() === "") {
return fail(`${item.name} clause ${key} needs a value`);
}
if (!validAmount(cell.amountCents)) {
return fail(`${item.name} clause ${key} needs a non-negative amount`);
}
clauses.push({
key,
label: CLAUSE_LABELS[key],
value: cell.value,
amountCents: cell.amountCents,
});
}
const scored = scoreContract(item.strengthChecks);
if (!scored.ok) {
return scored;
}
rows.push({
name: item.name,
clauses,
score: scored.value.score,
maxScore: scored.value.maxScore,
scoreBps: scored.value.scoreBps,
grade: scored.value.grade,
});
}
let weakestCase = rows[0].name;
let weakestScoreBps = rows[0].scoreBps;
for (const row of rows) {
if (row.scoreBps < weakestScoreBps) {
weakestCase = row.name;
weakestScoreBps = row.scoreBps;
}
}
return ok({ rows, weakestCase, weakestScoreBps });
}
把三份合同代进去,结果是 LedgerLite 8 分、Tidecast 5 分、Nord 6 分,最弱案例是 Tidecast。代码给出的 weakestCase 不是为了排名,而是为了提醒:下一节算风险敞口时,Tidecast 的 0 应收最容易被误读成「没有应收风险」。
小结
- 合同矩阵把八个条款压成同一套坐标:当事人、账期、责任上限、续约、SLA、IP、数据、终止。
- 每格必须同时有条款值和金额;只有条款值,读者知道术语,不知道后果。
- LedgerLite 的矩阵最完整:Net 15 占用 $9,873,12 个月费用上限 $240,240,SLA 年抵扣上限 $24,024。
- Tidecast 的矩阵最薄:应收为零,但平台未结算 $8,597.25,责任上限不可谈,3 年授权保底 $180,000。
- Nord 的矩阵里金额最大的一格不在合同正文,在合伙人退出:买断 $77,220,分 24 个月。
- 十项强度评分:LedgerLite 8 分、Tidecast 5 分、Nord 6 分;三家共同失分在续约对称性与终止过渡。
- 矩阵的第一读法是「金额占 SDE 比例」,不是「条款数量」;比例最高的那一格决定先谈什么。
- 代码只校验结构和算分,不提供法律意见;条款效力由适用法律决定。
阅读导航:上一节:12.3 保险、备份与应急预案的联动 · 下一节:13.2 应收与风险敞口 。
常见错误与边界
- 把矩阵当成合同清单。 矩阵只收八个会产生财务后果的维度;保密、通知、不可抗力等条款重要,但不在这张表里。
- 用公司层面上限比单份合同上限。 LedgerLite 的 $240,240 是 260 份合同加总,单份只有 $924;层级不同,比较没有意义。
- 把账期金额当年末应收。 矩阵用的是全年平均占用;年末余额是时点数,两者不能直接替换。
- 把续约窗口当续约概率。 窗口锁定的是合同期内的收入,不是明年续约的可能性。
- 把 SLA 抵扣当停机损失。 抵扣是收入折减,停机损失还包括支持成本和客户流失。
- 把 0 当成没有风险。 Tidecast 的数据条款和 Nord 的 DPA 都记 0,含义是「合同没有给出可计算保障」,不是「风险为零」。
- 只看得分不看金额。 8 分的合同也可能有 $13,337 的缺口;分数衡量写全程度,金额衡量最坏后果。
- 试图一次补齐十项。 三家的共同失分在续约与终止;先补这两项,比逐条修改格式有效。
- 把评分当法律意见。 评分是教学口径,不判断条款是否可执行;具体效力以适用法律和合同文本为准。
- 把矩阵做完就结束。 矩阵是下一节风险敞口的输入;条款金额不进入敞口表,就只是一张好看的对照表。
本节的边界是「条款值到金额的映射」:只覆盖八个条款维度、十项强度检查和三个案例,不展开合同解释、判例和法条。所有金额来自本卷前 12 章的教学设定;正式交易中的合同审查应由具备执业资格的专业人士完成。下一节会把矩阵里的金额转成应收、坏账准备、风险期望损失与未覆盖缺口,做一次横向对照。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。