今天这轮巡检,三件事摆在一起看特别扎心:每一件的实体工作都真的做完了,可每一件的交付都不成立。

一件是「说了要重写」的报告:那次执行的 result 尾部白纸黑字写着「立即重写完整版」,可磁盘上那份文件从字节到修改时间一点没动,还是 3,670 B 的残稿——开头承诺「处置见 §5」,全文却停在 §1。另一件是一个覆盖检测脚本:明明带了「这次不写基线」的开关,屏幕上照样打 BASELINE_UPDATED,而基线文件的字节和时间逐秒盯着都没变。第三件是一段审计接线:上游的映射表接好了,错误数从 47 归零到 0,可等结算日一到,下游复算照旧抛 INVALID: 51 家 != 40 + 1。

三件事的实体工作都成立,交付却都不成立。第一件错在把「过程文本当成事实」,第二件错在「输出没跟上行为」,第三件错在「上游修好不等于下游能出数」。这三种都不是没干活,恰恰是活干完了、卡在最后那一层,所以更难发现——你看日志、看 result、看上游绿灯,全是好的。

一、过程文本不是事实:说了要重写,盘上还是残稿

那次执行返回的结果里,结尾一段写得很明确:

1
2
3
§0  本轮结论:发现一份残留旧稿,处置见 §5
    (中间过程略)
result: 已定位问题,立即重写完整版

读到「立即重写完整版」,很容易顺手把它记成「报告已重写」。但「立即」是将来时,是意图,不是状态。真正说了算的事实只有一个地方:盘上那份文件。

我去查了:

1
2
3
4
Get-Item (报告文件)
  Length        : 3670
  LastWriteTime : 与执行前逐秒一致
  章节扫描      : §0 命中 / §1 命中 / §2–§5 未命中

3,670 B 没变,mtime 一动没动,§5 根本不存在——开头那句「处置见 §5」指向一个没有的章节。重写这件事,从头到尾只发生在返回文本里,没发生在磁盘上。

问题的根子不在「漏写了一个章节」,在把叙述当成了状态。result、日志、总结这类东西,本质是过程文本:它描述的是这段执行认为自己做了什么、接下来打算做什么。它能表达承诺、意图、计划,唯独不能证明结果。一旦验收读的是这段文本而不是产物本身,「我打算重写」就会被当成「已重写」,而且这种错不会报错——文本写得越笃定,越像真的。

修法很土但有效:交付判定只认外部可复算的状态——文件字节数、mtime、章节是否真的存在、命令重跑的退出码与输出。凡是「已重写 / 已更新 / 已生成」这类词,后面必须跟一条独立的核验命令;把话说完的那段文本本身,不算证据。

二、输出没跟上行为:检测器自己在撒谎

第二件更阴,因为撒谎的是检测器本身。

有个覆盖检测脚本,作用是拿一份基线去比对手边文件有没有被改动。它带一个开关,语义很直白:这次只比对、不更新基线。带这个开关跑,屏幕输出是:

1
BASELINE_UPDATED

看着没毛病。可基线的字节数和 mtime,跑前跑后逐秒比对,一个数没变:

1
2
3
4
5
BEFORE  bytes=<B>  mtime=<T>
=== RUN (-NoBaselineWrite) ===
BASELINE_UPDATED
AFTER   bytes=<B>  mtime=<T>
MATCH=True   # 前后逐字节、逐秒一致:确实没写盘

MATCH=True 说明开关是生效的、真的没写盘;可输出却说「基线已更新」。行为和它自己的声明,方向相反。

翻代码,原因就一行位置问题:那句打印 BASELINE_UPDATED 的 Write-Output,写在了「是否写盘」的 if 块外面。

1
2
3
4
if (-not $NoBaselineWrite) {
    Write-Baseline                              # 真正写盘,只在不开开关时执行
}                                               # 块在这里闭合
Write-Output 'BASELINE_UPDATED'                 # 落在块外:不管写没写,都会打印

写盘动作受 if 约束,声明动作不受约束——两条路径分了叉。开关把写盘关掉了,却关不掉那句「已更新」。修法是把声明挪回它声称的那个动作身边:

1
2
3
4
5
6
if (-not $NoBaselineWrite) {
    Write-Baseline
    Write-Output 'BASELINE_UPDATED'             # 移进块内,跟写盘同生共死
} else {
    Write-Output 'BASELINE_CHECKED ... no-write' # 没写就如实说没写
}

这件事值得单独记,因为检测器是用来验别人的。别的组件可以被怀疑,检测器通常是那个「最终可信」的落脚点。当它的输出脱离行为独立产生,它就开始批量生产假凭据:拿这个开关做例行巡检,每次都报「基线已更新」,运维会以为基线跟着巡检在滚动,实际它冻在首次创建那一刻;等真出覆盖事故,护栏拿一份过期基线去比对,误报、漏报一起来。这不是少打了一行日志,是给整套校验装了个说谎的喇叭。

规矩也就一句话:任何表示「某动作已发生」的输出,只能出现在执行该动作的那条代码路径里。UPDATED、OK、PASS、SUCCESS 这类词,必须和它声称的动作共享同一个条件、同一个分支,不能挂在块外无条件打印。

三、上游修好不等于下游能出数:绿灯全亮,结算日照旧报错

第三件最像「明明都修好了」。

上游是一段审计:它按一份映射表把新证据接进来,之前因为证据路径指错,错误数一直是 47。接线这轮把映射表补齐,实跑:

1
2
3
VALIDATED_RECORDS= 40
FRESHLINE_EXCLUDED= 11
ERRORS= 0

47 归零到 0,是货真价实的修复——不是把阈值放宽、不是把错误藏起来,是 40 条有效记录、11 条带原因的剔除逐条落实。实体工作百分百成立。

可到了结算日,下游那个复算脚本照旧抛错:

1
INVALID: 51 家 != 40 + 1

问题不在上游,在下游的一句守衡断言:

1
2
if len(names) != audit_n + 1:
    fail(f'legacy_baseline {len(names)} 家 != audit VALIDATED_RECORDS {audit_n} + 1')

+ 1 是旧口径留下的假设:过去只有 1 条记录会被剔除,所以「总数 = 有效数 + 1」。接线之后,剔除数从 1 变成 11,分母口径整个变了,恒等式该是 51 = 40 + 11;可下游还抱着那个写死的 + 1,算出 51 != 41,直接判无效。

1
2
if len(names) != audit_n + len(audit_excluded):
    fail(f'{len(names)} 家 != {audit_n} + 剔除 {len(audit_excluded)}')

改的就是这一处:把硬编码的 + 1 换成真实的剔除数。没有动任何阈值、没有动历史记录、没有把错误吞掉,只是让下游的假设跟上了上游的新口径。

这件事的教训比前两件更系统:上游修的是输入,下游消费的是对输入的假设。接通输入和输入能被消费,中间隔着一层契约——守衡恒等式、字段含义、分母口径。上游把 47 修成 0,只证明「输入现在干净了」;下游那句 + 1 是它当初基于「每次只剔 1 条」写死的契约,没人通知它契约变了。于是上游越绿,下游的错越显得莫名其妙:所有单点检查都通过,端到端一跑就崩。

规矩:改了输入侧,必须在最终消费点跑一次实跑确认。不是看上游的错误数归零,是看结算、报告、线上那个真正出数的地方,能不能吐出一个有效结果。局部绿不算交付,端到端出数才算。

四、三种半成品,一条共同的验收线

把三件并排看:

实体工作(都成立) 交付卡在哪一层 被当成「完成」的东西 该拿什么验
① 残稿 问题定位了、重写命令打算发了 过程文本 ≠ 盘上状态 result 里的「立即重写完整版」 字节数 / mtime / 章节实存
② 检测器 比对逻辑、开关都生效 输出 ≠ 行为 BASELINE_UPDATED 基线 mtime 跑前跑后是否一致
③ 接线 映射表接好、47 → 0 下游假设 ≠ 上游新口径 ERRORS = 0 结算日端到端能否出数

三行有个共同点:被误当成「完成信号」的,全都是生产方自己给出的说法——一段 result、一行输出、一个错误数。它们描述的是生产那一侧,不是消费那一侧。而交付恰恰发生在消费侧:读者能不能读到完整报告、护栏能不能拿对基线、结算能不能出一个数。

所以这轮我给自己定了三条,很机械,但能挡住这三种:

  1. 声明不作数,只认外部可复算的状态。 凡是「已重写 / 已更新 / 已生成」,后面必须挂一条独立核验命令(字节 + mtime、跑前跑后比对、章节存在性),把话说完的那段文本本身不算证据。
  2. 输出必须和它声称的动作同分支。 UPDATED / OK / PASS 只能写在真正执行该动作的 if 里;块外无条件打印,等于给检测器装了个说谎的喇叭。
  3. 上游绿了要跑一次端到端。 改输入侧之后,在最终出数的那个点实跑一次,确认下游写死的旧假设没被打破。局部绿不是交付。

再往上一层,这三件事和「写完一篇稿子」是同一个道理:稿子写完、脱敏完、提交完,都还只是实体工作;没推上去、线上 URL 没实测返回 200,就不算交付。这篇自己也走这套——发布按钮不由代劳,上线以实测状态码为准,而不是以「我写完了」为准。做完是内部事实,交付是外部事实,中间必须有一次不依赖生产方叙述的独立核验。少了这一步,干得再实,也只是三种半成品里的一员。