读完四卷,最危险的问题是「接下来做什么」
某团队用三个月读完了四卷。复盘会上,有人问:「我们算不算把客户端架构做完了?」
没有人能回答。因为「读完」和「做到」之间差着一份检查表:哪些承诺已经落地,哪些只有文档,哪些根本没开始。没有这份表,团队只有两种选择——继续加功能,或者停下来等下一次事故。
13.3 做三件事:回顾四卷各自留下的承诺模型,把承诺变成十五项收官检查表,再用一组明确的停止条件回答「什么时候可以不继续演进」。
四卷不是四个专题,而是一条承诺链
四卷看起来主题不同,实际上回答的是同一条链上的四个问题:先定义什么必须一致,再保证内容与实时状态可同步,然后保证交易不出错,最后保证多端多团队下这些承诺仍然成立。
| 卷 | 核心问题 | 留下的承诺 | 实验工程 |
|---|---|---|---|
| 端形态与体验基础 | 同一功能在五个端上如何既一致又可用? | 承诺分级、共享内核、能力探测 | base-lab |
| 内容、社交与实时互动 | 内容流、实时消息与弱网状态如何保持一致? | 数据流分层、实时排序、弱网恢复 | interact-lab |
| 交易、增长与平台化 | 一笔交易在增长实验与平台复用下如何不出错? | 金额与状态单一事实源、幂等、归因边界 | trade-lab |
| 规模化与架构演进 | 五端、五团队、五节奏下如何把应用做成平台? | 平台边界、能力矩阵、治理、迁移退出 | evolve-lab |
这条链的顺序不能颠倒。共享内核之前没有承诺矩阵,共享出去的就是偶然一致;交易正确性之前谈增长,实验会把错误放大;平台化之前没有治理,能力矩阵只会变成一张自动打分的幻灯片。
十五项收官检查表
检查表按四卷分组,每项都写清楚承诺、要回答的问题、证据形态与状态。状态只有三种:
covered:有证据,且已经进入日常流程。partial:部分端或部分团队做到,仍有未登记的例外。missing:没有证据,或只停留在文档。
覆盖率把 partial 按半分计算。示例检查表的 15 项中有 7 项覆盖、6 项部分覆盖、2 项缺失,覆盖率约 66.67%。这不是某个团队的真实测评分,而是随代码附带的示例数据;真实使用时替换 SERIES_CHECKLIST 即可。
失分最集中的两处值得注意:弱网恢复与无障碍验收都是 missing。它们不是新技术,而是最容易在「功能优先」的节奏里被无限推迟的承诺。
把四卷的承诺写成检查表
下面的模块把四卷、十五项检查表、建议映射与停止条件都做成纯函数。decideNextStep 是收官的核心:它不评价团队好坏,只根据四个可观测条件判断下一步该继续、巩固还是停止。
// file: src/summary/series-review.ts
// tier: L1
// verify: node --test test/summary/series-review.test.ts
export type VolumeId = "foundation" | "interaction" | "trade" | "evolve";
export type PromiseId =
| "consistency" | "shared-core" | "capability"
| "content-flow" | "realtime" | "weak-network"
| "money" | "idempotency" | "experiment"
| "platform" | "governance" | "migration"
| "observability" | "security" | "a11y";
export type Volume = {
id: VolumeId;
title: string;
question: string;
sections: number;
lab: string;
promises: PromiseId[];
};
export type ChecklistItem = {
id: string;
volume: VolumeId;
promise: PromiseId;
question: string;
evidence: string;
status: "covered" | "partial" | "missing";
};
export type SeriesSummary = {
volumes: number;
sections: number;
promises: number;
covered: number;
partial: number;
missing: number;
coveragePct: number;
};
export type VolumeCoverage = {
total: number;
covered: number;
partial: number;
missing: number;
pct: number;
};
export const VOLUMES: Volume[] = [
{
id: "foundation", title: "端形态与体验基础", sections: 45, lab: "base-lab",
question: "同一功能在五个端上如何既一致又可用?",
promises: ["consistency", "shared-core", "capability"],
},
{
id: "interaction", title: "内容、社交与实时互动", sections: 45, lab: "interact-lab",
question: "内容流、实时消息与弱网状态如何保持一致?",
promises: ["content-flow", "realtime", "weak-network"],
},
{
id: "trade", title: "交易、增长与平台化", sections: 45, lab: "trade-lab",
question: "一笔交易在增长实验与平台复用下如何不出错?",
promises: ["money", "idempotency", "experiment"],
},
{
id: "evolve", title: "规模化与架构演进", sections: 45, lab: "evolve-lab",
question: "五端、五团队、五节奏下如何把应用做成平台?",
promises: ["platform", "governance", "migration", "observability", "security", "a11y"],
},
];
const check = (
id: string, volume: VolumeId, promise: PromiseId, question: string, evidence: string,
status: ChecklistItem["status"],
): ChecklistItem => ({ id, volume, promise, question, evidence, status });
export const SERIES_CHECKLIST: ChecklistItem[] = [
check("f-consistency", "foundation", "consistency", "哪些行为必须逐字一致,哪些允许等价或不同?", "五端承诺矩阵与跨端契约用例", "covered"),
check("f-shared-core", "foundation", "shared-core", "共享代码的依赖方向是否单向且可检查?", "core / protocol / adapter / ui 依赖图", "covered"),
check("f-capability", "foundation", "capability", "端能力缺失时是降级、阻塞还是隐藏?", "能力探测表与四类处理策略示例", "partial"),
check("i-content-flow", "interaction", "content-flow", "内容流的数据、缓存与渲染是否分段治理?", "内容流分层图与分段预算", "covered"),
check("i-realtime", "interaction", "realtime", "实时消息在重连与多端同时在线时如何排序?", "重连补偿用例与消息序号断言", "partial"),
check("i-weak-network", "interaction", "weak-network", "弱网与断网恢复是否验证过降级与回补?", "弱网场景时间线与恢复用例", "missing"),
check("t-money", "trade", "money", "金额与订单状态是否只有一个事实来源?", "金额整数分用例与字段所有权表", "covered"),
check("t-idempotency", "trade", "idempotency", "重复提交与结果未知是否先查后重试?", "幂等键用例与未知结果查询时间线", "covered"),
check("t-experiment", "trade", "experiment", "实验分流是否可复现且变更可追溯?", "同用户同分组的复现用例", "partial"),
check("e-platform", "evolve", "platform", "平台能力是否进入发布门禁并按缺口排序?", "能力矩阵评分与缺口排名报告", "partial"),
check("e-governance", "evolve", "governance", "每个架构决定是否有责任人与评审记录?", "ADR 目录、所有权表与评审覆盖率", "partial"),
check("e-migration", "evolve", "migration", "每次迁移是否有退出条件与回滚触发条件?", "迁移退出条件清单与回滚演练", "covered"),
check("e-observability", "evolve", "observability", "线上问题能否在分钟内定位到端与版本?", "最小观测契约与告警闭环记录", "covered"),
check("e-security", "evolve", "security", "数据分级与第三方 SDK 是否定期复核?", "数据分级表与 SDK 权限差异报告", "partial"),
check("e-a11y", "evolve", "a11y", "无障碍验收是否进入组件契约而不是一次性检查?", "读屏、键盘与动效偏好验收用例", "missing"),
];
const ADVICE: Record<PromiseId, string> = {
consistency: "先固化三级承诺矩阵,再谈共享代码",
"shared-core": "收敛共享内核,画出 core → protocol → adapter → ui 依赖图",
capability: "补齐能力探测的四类处理策略,并给出缺省降级",
"content-flow": "把内容流拆成数据、缓存、渲染三段,分别设预算",
realtime: "为实时通道补多端顺序与重连后一致性用例",
"weak-network": "补齐弱网下的降级与恢复验证,不要只测有网路径",
money: "金额与订单状态继续以服务端为唯一事实来源",
idempotency: "把幂等键与未知结果先查后重试写进发布门禁",
experiment: "实验分流结果要可复现,配置变更留版本记录",
platform: "把能力矩阵接入发布门禁,缺口排名决定下一迭代",
governance: "补齐 ADR、所有权表与技术债台账的责任人",
migration: "每次迁移先写退出条件与回滚触发条件",
observability: "最小观测契约覆盖五端,告警闭环有响应人",
security: "数据分级与 SDK 清单每季度复核",
a11y: "把无障碍验收纳入组件契约,避免迭代退化",
};
function ratio(covered: number, partial: number, total: number): number {
return total === 0 ? 0 : Math.round(((covered + partial * 0.5) / total) * 10000) / 100;
}
export function summarizeSeries(items: ChecklistItem[]): SeriesSummary {
const covered = items.filter((i) => i.status === "covered").length;
const partial = items.filter((i) => i.status === "partial").length;
const missing = items.filter((i) => i.status === "missing").length;
return {
volumes: VOLUMES.length,
sections: VOLUMES.reduce((acc, volume) => acc + volume.sections, 0),
promises: items.length,
covered, partial, missing,
coveragePct: ratio(covered, partial, items.length),
};
}
export function checklistByVolume(items: ChecklistItem[]): Record<VolumeId, ChecklistItem[]> {
const grouped = {} as Record<VolumeId, ChecklistItem[]>;
for (const volume of VOLUMES) grouped[volume.id] = items.filter((i) => i.volume === volume.id);
return grouped;
}
export function coverageByVolume(items: ChecklistItem[]): Record<VolumeId, VolumeCoverage> {
const grouped = checklistByVolume(items);
const result = {} as Record<VolumeId, VolumeCoverage>;
for (const volume of VOLUMES) {
const scoped = grouped[volume.id];
const covered = scoped.filter((i) => i.status === "covered").length;
const partial = scoped.filter((i) => i.status === "partial").length;
const missing = scoped.filter((i) => i.status === "missing").length;
result[volume.id] = {
total: scoped.length, covered, partial, missing,
pct: ratio(covered, partial, scoped.length),
};
}
return result;
}
export function openItems(items: ChecklistItem[]): ChecklistItem[] {
return items.filter((item) => item.status !== "covered");
}
export function validateChecklist(items: ChecklistItem[]): string[] {
const issues: string[] = [];
const seen = new Set<string>();
const volumes = new Map(VOLUMES.map((volume) => [volume.id, volume]));
for (const item of items) {
if (seen.has(item.id)) issues.push(`检查项 id 重复:${item.id}`);
seen.add(item.id);
if (item.evidence.trim() === "") issues.push(`缺少证据:${item.id}`);
const volume = volumes.get(item.volume);
if (volume === undefined) issues.push(`未知卷:${item.id}`);
else if (!volume.promises.includes(item.promise)) issues.push(`承诺 ${item.promise} 不属于卷 ${item.volume}`);
}
return issues;
}
export function adviceFor(promises: PromiseId[]): string[] {
const out: string[] = [];
const seen = new Set<PromiseId>();
for (const promise of promises) {
if (seen.has(promise)) continue;
seen.add(promise);
out.push(ADVICE[promise]);
}
return out;
}
export function nextSteps(items: ChecklistItem[]): string[] {
return adviceFor(openItems(items).map((item) => item.promise));
}
export type EvolutionState = {
highRiskOpen: number;
platformScore: number;
changeFailureRate: number;
evidenceCoverage: number;
teamReady: boolean;
};
export type StopDecision = { decision: "continue" | "consolidate" | "stop"; reasons: string[] };
export function decideNextStep(state: EvolutionState): StopDecision {
const reasons: string[] = [];
if (state.highRiskOpen > 0) reasons.push(`仍有 ${state.highRiskOpen} 个高风险项未关闭`);
if (state.platformScore < 3.5) reasons.push(`平台验收分 ${state.platformScore.toFixed(2)} 低于 3.5`);
if (state.evidenceCoverage < 0.8) {
reasons.push(`证据覆盖率 ${Math.round(state.evidenceCoverage * 100)}% 低于 80%`);
}
if (reasons.length > 0) return { decision: "continue", reasons };
if (!state.teamReady) {
return { decision: "consolidate", reasons: ["平台达标但团队尚未准备好接管日常演进"] };
}
if (state.changeFailureRate > 0.2) {
return {
decision: "consolidate",
reasons: [`变更失败率 ${Math.round(state.changeFailureRate * 100)}% 高于 20%,先还债`],
};
}
return {
decision: "stop",
reasons: ["高风险清零、平台达标、证据充分且变更稳定,可以冻结架构转入维护"],
};
}
下一站建议与停止条件
检查表的未关闭项会自动生成建议。示例数据给出八条,顺序与检查表一致:能力探测 → 实时顺序 → 弱网恢复 → 实验复现 → 平台门禁 → 治理责任 → SDK 复核 → 无障碍契约。这些建议不是排期,而是下一轮评审的输入;每条都要先补证据,再谈完成。
停止条件则回答另一个问题:什么时候可以不再做架构演进?模块把它分成三类决策:
| 决策 | 触发条件 | 动作 |
|---|---|---|
continue | 有高风险未关闭、平台分低于 3.5、或证据覆盖低于 80% | 按缺口排名继续演进 |
consolidate | 平台达标但团队未准备好,或变更失败率高于 20% | 暂停新能力,先还债与补责任 |
stop | 高风险清零、平台达标、证据充分、团队就绪且变更稳定 | 冻结架构,转入维护与小步优化 |
stop 最容易误读。它不是「产品停止迭代」,而是停止架构层面的扩张:不再新增平台抽象、不再改共享内核边界、不再引入新端形态。业务照常迭代,架构只做维护性修复。没有这个停止条件,平台化会变成无限扩张,最后没人能说清哪一版是稳定基线。
判据与反例
反例一:把读完当做到。 检查表全部标成 covered,但证据栏写的是「已完成」。这种表能让复盘会好看,却无法回答任何一个具体问题。证据必须是可复查的产物:矩阵、用例、流水线记录、演练报告。
反例二:永远 continue。 如果团队从不设定停止条件,每次评审都会找出新缺口,架构演进就永远不会结束。停止条件不是懒惰,而是承认「当前架构已经足够支撑业务」。
还有一个边界:四卷不是唯一路径。读者可以只做卷一与卷三,把内容与实时互动留给更专业的团队;也可以用四卷的检查表审视一套已经存在的架构。检查表是判据,不是课程表。
小结
四卷留下的不是四个专题,而是一条承诺链:先定义一致,再保证同步,再保证交易正确,最后保证多端多团队下承诺仍然成立。十五项检查表把这条链变成可自查的清单,覆盖率与未关闭项给出下一站建议。
收官真正要回答的问题是:哪些承诺已经有证据,哪些还没有,以及什么时候可以停止扩张。 能回答这三个问题,四卷才算读完。
阅读导航:上一节:13.2 平台化验收:能力矩阵与治理清单 · 下一节:目录与学习路径 。
常见错误与边界
- 把「读完四卷」当成「架构已经做完」,没有检查表。
- 检查表全部标绿,证据栏只写「已完成」「已确认」。
- 覆盖率只算
covered,把大量partial当成缺失或当成完成。 - 未关闭项直接变成排期,不先补证据与责任人。
- 停止条件只看平台分,不看高风险项、证据覆盖与变更失败率。
- 把
stop理解成产品停止迭代,导致团队不敢设定停止条件。 - 把检查表当课程表,要求每个团队按同一顺序做完十五项。
- 一次收官之后不再复查,半年后检查表与实际架构脱节。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。