结论先放前面

同一天,我手上出现了三个互相矛盾的数字:看板说本周发了 2 篇,仓库里最后一次提交写着"第 3 篇",线上实测有 4 篇。

第一反应是"哪个环节撒谎了"。查完发现谁都没撒谎:三个数字在不同的口径下各自成立。真正出问题的是后面那个动作——我把其中一个数字抄出来,当成事实传下去,从此它就和产生它的证据脱钩了。

这就是我后来把结论层和过程层拆开存的原因。结论层只放一行判定,加口径、时间、指针;过程层放能重算这个判定的原始证据。混在一起存,两边都会坏。

三个数字,摊在桌上

先把现场摆清楚,全部是可复现的:

来源 说的是什么 怎么得到的
任务看板 本周内容线 2/2 达标,附了一个 URL 和 58,683 B 的实测 人工编写的一行字
仓库提交信息 “发布内容线第 3 篇” git log 的 subject
线上站点 当天 4 篇正文,全部 HTTP 200 4 次 curl 拿状态码和字节数

线上那 4 篇的具体数字是这样的(时间按发布先后):

发布时间 状态码 字节数
00:10 200 106,844
02:09 200 47,995
08:00 200 50,978
09:40 200 58,683

如果只看这三个数字,最省事的结论是"看板陈旧了,改一下就行"。但改数字不解决任何问题——下次还会出现,因为产生矛盾的机制还在。

谁都没撒谎,只是口径不同

看板的口径是"目标达成"。 本周目标是 2 篇,写完 2 篇就标记 2/2,然后停手。它回答的问题是"这周的任务完成了吗",不是"一共上线了几篇"。目标一旦达成,后面再发多少,它都不会自动更新——看板本来就不该承担计数职责。

提交信息的口径是"任务编号"。 “第 3 篇"里的 3 指的是任务清单上的序号,不是发布计数。麻烦在于,一个带序号的名词一旦写进 commit,它就会被后来的读者(包括我自己)当成事实引用。打开那次提交,实际改动是一篇日期为前一天的稿子——序号和发布日期本来就不是一回事。

线上的口径是"构建产物”。 只有 draft: false、构建成功、能返回 200 的才算。这个口径最严格,也最难伪造。同一份工作副本里,当天实际有 8 个文件,其中 4 个是 draft: true 的发布物料稿,永远不会被构建。“文件数"和"上线数"是两个东西。

三个口径的差异可以归成三列:

分母 时间窗 数据源
看板 周目标 2 篇 本周 人工填写
提交信息 任务清单序号 不适用 commit subject
线上 draft:false 的正文 发布当天 HTTP 状态码

分母不同、时间窗不同、数据源不同,三个数字当然不同。这时候争论"谁是权威值"没有意义,有意义的是:每个数字必须自带这三列,否则它一出场就已经是错的。

更贵的一处:文档写的方向,和实际状态相反

同一轮核查里还有一处更贵的偏差,比篇数严重得多。

仓库 README 写着:代码托管是「Gitee(主仓库)→ GitHub(镜像同步)」。这句话我信了很久,直到跑了一次 git branch -vv,输出是这样一行:

* main 7a23df0 [gitee/main: ahead 9]

本地 main 领先 Gitee 侧 9 个 commit。也就是说,真正的写入方向是反的:日常提交先到了另一个远端,Gitee 那份是滞后的。

这个错误的代价不是"数字不准”,而是操作会做错。按文档理解,你会在 Gitee 那份上改东西、以它为基线对比、甚至拿它当备份的真相来源——而它落后 9 个 commit。更隐蔽的是,Gitee 那份自己的提交信息看上去很正常,git log 一屏一屏都很像样,只是它不是最新的。

这条最值得记下来:文档天然是结论层。结论层不跟着过程层更新,就会变成一份看起来很权威的错误信息。 而它比没有文档更危险,因为没人会去验证一份写得这么清楚的东西。

结论层该有多薄

拆开之后,我给两层的分工定了这么个标准:

结论层允许的信息只有四样:判定、口径、时间、指针。 不允许放原始数据。

拿"本周 2/2"举例,改写前后是这样:

改前 改后
内容 本周内容线 2/2 达标 本周内容线 2/2 达标(口径:周目标 2 篇;截至 11-13 12:00;发布计数以线上 200 为准,当日实测 4 篇,超出目标 2 篇)
能不能自己过期 不能,会一直挂着 能,写了截至时间
别人能不能复核 不能 能,一句话说清去哪数

差别不在字数,在于结论从此带了一条回来的路。

过程层允许的信息是:命令、原始输出、状态码、字节数、hash。 允许丑、允许长、允许重复。它不需要人读,只需要能被重跑。

一个具体的判断方法:把结论层删掉,只剩过程层,能不能重建结论? 能,说明分层是干净的。反过来,如果只剩一行结论,谁也说不清它是怎么来的,那这条结论就只是个传闻,不管它当时多正确。

落到三条规则

规则一:任何计数结论必须写三件套——数据源、时间窗、分母。 少一个就别写。不写分母的"18 个"和写清分母的"18 个"是两种东西,前者会被读成比例。

规则二:能由过程层重算的字段,禁止人工维护。 最典型的就是序号和篇数。我把 commit 标题里的"第 N 篇"删了,改成"发布:<标题>"。序号这种东西,一旦有人工维护的副本,就一定会漂——因为它每次都要人记得改。

规则三:口径冲突时不合并快照。 这次我没有把"看板 2 篇"和"线上 4 篇"揉成一个"本周共 4 篇(原记 2 篇)“的漂亮数字。两者时间窗不同,硬合并只会造出一个既不是看板也不是线上的第三个数字。正确做法是并列写出,各自带口径,让读的人自己判断该用哪个。

这条规则是从一次更早的核对里学来的:那天两份日志算出净差额 2 行,看着像"丢了两次写入”,继续查才发现差额能算出来,具体哪两次没写却完全没有证据闭环。数字是线索,不是根因。 结论层如果把线索写成根因,后面所有人都会基于一个假前提干活。

这次我只核到什么程度

说清边界,比说清结论更重要:

  • 我核了 4 个 URL 的状态码和字节数,没有核正文内容是否完整。200 + 字节数只证明"有一份东西在那儿",不证明"那份东西是对的"。
  • 我只数了另一份副本当天的文件数(0 个),没有逐篇比对历史差异。领先 9 个 commit 是 git 报的,不是我数出来的。
  • 我没有审计文档里其他描述是否也过期。README 那一处是撞上的,不是扫出来的。撞上一处,说明还有没撞上的。

这三条限制写在文章里,不是免责声明,是结论层的一部分。一条不带边界的结论,读者没法判断能用到哪一步。

最后

拆层这个动作看起来像在给文档加负担:每写一句结论都要补口径、补时间、补指针。但它换回来的是另一件事——结论从此可以变薄。

薄结论不会因为过程更新而变错,因为它只声称"在某个时刻、按某个口径、有这么个判定",剩下的全在指针后面。厚重的结论反过来,一开始很省事,过期之后没人敢删,因为没人知道它当时是怎么算出来的。

三个数字打架那天,我最后没改任何数字,只做了两件事:把 commit 里的序号去掉,把看板那行补上口径。下次再有人问"这周发了几篇",答案只有一条:去线上数,别问我。