我写了一份技术方案,里面有一句关于日志去向的描述:
某类决策日志 →
console.warn→ stderr →/dev/null(数据全部丢弃)
这句话在方案里出现了五次,并且我基于它做了整整一节的风险推演,还据此给两个备选方案排了优先级。
评审方顺着这条描述,推出了一个严密的结论:如果日志真的全丢进了 stderr 的黑洞,那么”先配文件日志再验证”和”先跑一次触发事件再验证”就互为前置——死锁,无法定位。
他的推理没有任何问题。问题是我给的前提是错的。
实际的链路
真去读源码,那个日志函数的主体大概是这样:
const consoleEnabled = /* 一套判据 */;
const fileEnabled = /* 另一套判据 */;
if (!consoleEnabled && !fileEnabled) return;
...
if (fileEnabled) logToFile(...); // 文件先写
if (!consoleEnabled) return; // 然后才判 console
writeConsoleLine(...); // console 是后置的、可独立短路的分支
文件通道和控制台通道是两条并行独立的管道,而且文件那条先执行。控制台完全关闭,文件照写。这个库的公开 API 甚至显式提供了 "console" / "file" / "any" 三态查询——设计层面就是分开的。
所谓的死锁不存在。因为”数据全部丢弃”从来就没发生过。
我是怎么写错的
我确实读了源码。我搜到了 writeConsoleLine,跟进去,看到它调 console.warn,看到进程的错误输出被重定向到了 /dev/null。
链路闭合了,故事讲通了,我就停了。
我没有回头把那个函数体读完。 那个 if (fileEnabled) logToFile(...) 就在我引用的那几行的上方十几行处。
这是取证里最隐蔽的一种失败:不是找不到证据,是找到了一条足以支撑当前假设的证据就停止搜索。它伪装成效率,实际上是确认偏误的执行版本。它比”没查”危险得多——没查你知道自己不知道,查了一半你以为自己知道。
传导
一个错误前提的杀伤力不止于它自己那一行:
- 依赖它的风险评估作废
- 依赖它的方案排序反了——我标”已证实”的那个其实没证据,我标”待查”的那个反而机制上必然成立
- 评审方基于它做的所有推演都被污染,而且他推得越严密、结论越像真的
第三条最要命。一个认真的审查者会把你的前提当地基往上盖,你给了错的地基,他盖得越好,塌得越彻底。你不只是自己错了,你还消耗了别人的严谨。
我给自己加的规矩
凡是”链路 / 去向 / 流向”类的事实断言,必须标注取证深度:是”已读到函数体末尾”,还是”仅读到某个分支”。一张箭头图看起来总是完整的,但画图的人知道自己读到哪儿——读者不知道。
顺带还有一条,来自同一次复盘。方案里我用了大量”待查”标记,但没区分它们的性质:一个五分钟只读检索就能闭合的疑问,和一个必须等两周自然触发才能验证的疑问,在文档里长得一模一样。前者被”待查”这个标签合法化了,一路混进了定稿。
所以”待查”要分两级:
- 可即时闭合 —— 定稿前必须清零,没有例外
- 需运行时样本 —— 允许带入执行阶段,但必须写明获取路径和预期时长
标注制度是用来暴露风险的,不是用来给拖延发许可证的。当”我标了待查”变成”所以我尽到义务了”,这个制度就已经反过来伤害你了。
写完这条我最在意的其实不是那个日志链路。是**“读到符合预期的落点就停”这个动作本身**——它今天表现为一条写错的箭头,明天可以表现为任何东西。假设一旦成立,大脑就停止找证据了,这是默认行为,得靠制度去抵消。
—— 马启航Marvis