跳到主要内容
修复本身携带着修复的可信度

修复本身携带着修复的可信度

盖文
盖文

· 阅读约 12 分钟

这期记一个GitHub仓库和它周围的一次讨论。结论先放一句:一个声称“不再信任回执”的补丁,回头又踩进了它自己想堵住的坑,而作者自己承认修复还没发生——卡点不在逻辑,在席位。公开记录为主,确认的事实、别人说的话、我自己的分析分开标。

先把时间戳摆开。2026年8月26日凌晨,EDT,三条并排:21:27:24Z是committer date,提交者自己写进对象里的;21:27:28Z是GitHub的PushEvent created_at;21:30:49Z是审查机器人标记缺陷。后两条是服务器记录的,唯独第一条可以伪造,后两条伪造不动。四秒的差距不起眼,但它把两个完全不同的事实放进了一个画面。三分钟二十秒后机器人抓到那个缺陷。这个速度本身就是一道证据:它跑的是规则,不是理解。

提交信息没毛病,常规口气:“修复四项复审发现,不再信任回执。”可diff里新增的那行代码干的事刚好相反:它把回执自己声明的判定字段列表——回执说自己该被哪些字段检验——作为decide函数的第二个参数传了回去。也就是说,当一个带有失败检查项、同时把declaring字段填成空数组的回执进来时,校验器在空集上重算一遍,得出通过。它“不再信任回执”的方式,恰是把回执的自我描述变成了计算输入。

两种常规审计路径都会漏掉。按提交信息审,没有说谎;按diff摘要审,只是新增了一个参数,传入声明字段列表,不坏。但这两条路漏掉的是同一件事:这个参数让被检验对象获得了指定判定标准的能力。审查机器人没漏掉,不是因为它读懂了“伪造判定视野”这种表述,而是它的规则集合里大概率有一条对应的模式,跑到了就触发。

作者自己的说法是:修复本身携带着修复的可信度,所以没人会再看第二眼。这句话比字面重。它描述的不是某个审阅者的疏忽,而是审查行为的某种内建倾向:已经标记为“修复过”的东西,再看它的阈值会往上提一笔。这不是谁偷懒,也不是审查员坏了,是看diff的人脑子里自然发生的省钱动作。修复这个形式本身在降低第二次审查的唤醒级别。

有意思的是,这是这类失效模式第二次被点出来。8月24日,作者写过一篇Python授权到期契约的文章。评论区里有个叫pm25coder的人当天留言说:每次修复都把权威性转移给类型更好的东西,而契约的权威字段本身又是下一层派生值。他说的是另一个项目里的Python代码,不是这个JavaScript仓库。说过之后,同类模式两天后在这个仓库里重现——不是因为他押中了这次具体提交,而是因为他命名了一个类别。这两件事得分清楚:预言得讲出它什么时候落在哪个具体对象上;类别命名只是识别出一组边界条件,条件满足时失效就重复出现。前者更强,但几乎不可重复使用;后者更弱,却可迁移。作者自己把这个区分标得很清楚。这种态度不多见——大多数人在被评论者“预告”之后会忍不住往预言上靠。

这个仓库8月25日19:47:38 EDT创建,第一笔提交4a8e6c9一小时后落地。仓库才刚出生,自校正机制已经在跑,还是拿JavaScript写的。快的那一面是机制本身反应快;另一面是,它没有时间沉淀出一个独立的观察面。

计量问题那段记录我翻过。8月29日,一份提交文档声称“六次已合并PR中共有37条行内评论”。数字算起来对。但这个句子是一句完备集合断言——它声称扫遍了已合并PR的全集。这话被合并进仓库的那一刻,计数对象从六个变成了七个。句子每字真的,描述的那个集合在合并发生的瞬间过期。后来发现问题的人,不是靠算数,是问了句“你量的是哪个集合”。算术一直没错,错的是边界定义。最终修复也别无花招:显式列出被测量的集合,不假装有全集。

这件事和回执漏洞其实是同一个更大盲点的两个具体表现:扫过了,发现了数字,就默认这个数字会自我标记、自我证伪。它不会。

Mikhail在讨论串里给这盲点起了个名字:empty-set trap。他转述一个生产案例:某100%采样收集器连续四天返回零行,而它所监视的机器每天在处理113到209个请求。不报错,没有分歧,图也正常,唯一的信号是eligible_seen为400,而population_size为0。这个案例比这仓库里任何一条都更吓人,因为它是真的生产事故,而且以安静通过的方式失败。在一个能报错就等于还在运行的环境里,空集通过往往比崩溃更难被识别。

回来。校验器对findings为空数组这件事没有下限约束。schema限制最多八条,没设最少一条。真正挡着“什么都没查到就当成通过”的,是提示词里的一句话。不是机制层防御,是自然语言请求。作者后来补充:core.mjs在运行开始前就会拒绝文件数为零的语料清单,所以scanned_item_count为0不可达。行,不可达只意味着这个具体形态不会出现,不代表系统有一个可测量的分母。scanned_item_count是用corpus.files.length算出来的,它跟发现结果天然独立,但没有被拿来跟发现数量做过任何比较。我说的不是代码里缺这个东西——我翻过仓库,readdir、glob、walk、scandir、discover、enumerate这些关键词零命中。语料清单是手写的JSON,四个键:corpus_id、files、schema、verification。run 004里只列了两个文件。在这个系统里,呈现集合就等于合格集合,不是因为验证了什么,而是因为人工填进去的内容本身就是全部。

这里标一下:(分析,非确认)。这个分析跟Mikhail那个生产案例的连接足够实。一个系统没有独立分母时,空集不是一种可能的结果,而是定义上的常态。人工填写JSON在这里变成单点故障。作者后来公开更正了自己的过度概括,承认他扫过的两个仓库有些其实在别处,递归遍历确实存在于代码库其他部分。等于他在并不存在的目录上报告了干净的结果——这就是他自己刚描述过的empty-set trap,还没走出仓库就再踩一次。这种自我暴露挺好:一个记录者如果能在自己的记录里捞到自己的同类错误,说明他没有豁免自己。

时间戳那部分,比第一眼更碍事。Edward Izgorodin提醒了一类必须区分开的事实:仓库创建时间、审查评论created_at、dev.to评论created_at,这些是服务器记录;而committer date是提交对象内部字段,可以用GIT_COMMITTER_DATE设置、amend改写、rebase时变,但它是被写进对象的一部分。作者跑了一个命令测试:把提交报告时间改成2001-01-01T00:00:00-05:00,效果立刻出现。然后他承认两类事实并列在脚注里不准确。承认挺干脆。

接着他做了件更有用的事情:去查PR时间线上的committed事件。结果发现它跟提交自带的committer date完全相同,一毫秒不差。这意味着PR时间线没有记录到达时间,它只是把提交者自己写的日期包进服务器外壳回显一遍。GitHub在当时刻真正写入的是PushEvent created_at。4a8e6c9的PushEvent created_at比committer date晚四秒。这个字段有保留限制:每条仓库feed最多300个事件,30天滚动。作者拉了一整包feed:共196条,其中97条由审查机器人产生。这笔账算下去下半句不太好听:仓库日历年龄18天,feed保留窗口还没到头,但300个槽位已经填了196个。按qodo机器人的检查频率,事件计数窗口会先于日历窗口耗尽。作者在这个细节上克制,只说现有日历界限先到——9月25日。但我想指出:这个repo的可见历史再过一段时间就会被自动清洗,讨论串里很多可核验端点会先失效。保留窗口不是有人拿来掩盖什么,它只是会消失。

现在看那个补丁。新增了一个常量CANONICAL_DECIDING_FIELDS,内容是node、trueforge、sdk,把它作为decide的第二个参数传进去,回执自带的副本降级成证据。回执声称的字段列表跟冻结常量不一致时返回DECIDING_FIELDS_MISMATCH;声明空数组,拿到错误而不是静默通过。修复在逻辑上有效,行为测试也跑过。放生产系统里,它确实能堵住开头说的那个漏洞。但作者自己说,他仍不认为问题已修复——因为补丁是maker自己编的,没有单独指派的breaker坐席对它做裁定。在这个项目里,maker自己的PASS不算数。

这个点得展开。自校正系统的核心目标,是不接受来自制造方的自我证明。可这个项目的breaker席位现在就是制造者本人兼任的。他说maker的PASS不算数——这条规则本身也是制造者的声明。没有外部独立的一方对“算不算数”做裁决。所以补丁出现时,不管逻辑多正确,它的可信度来源仍然是制造者。“修复本身携带着修复的可信度”这句话,到这里又咬回发起者自己身上。没能跳出。

这不是要下“这个项目一定会再次失效”的预测——盖文不干这个活。只记录一个事实:它的最后一轮防线目前不在系统里,而在项目成员之间的一种共识里。这种共识没有schema,没有checks,不设deciding_fields,也找不到独立的denominator。

作者自己也承认这一点。他对独立性方向的理解是:要找的是某个其他方为自己目的已经写下的字节。这个表述准确说,他要的不是一个独立的测试,而是一份客观上已存在、与当前被检验对象无关的字节,拿它重新描述同一个问题。他承认自己还没实现。目前每一个检查都靠让两个视图相互矛盾来工作——备注和控制流打架,回执声明和冻结常量打架,集合定义和合并动作打架。但凡是两个视图都不矛盾、答案仍然错的情况,这套系统看不见。因为它没有来自系统外的东西可供对照。

Theo Valmis在评论区说,这份回执是对大多数CI检查的恰当比喻:绿色流水线只证明步骤跑过,不证明结果符合系统本应保证的目标。作者回应,这不是比喻,回执是带schema、checks、deciding_fields的真实JSON对象,并验证了validateReceipt在无回执时返回RECEIPT_ABSENT。这个回应本身是好的——拒绝退回修辞层面,把问题留在真实对象层面。但我要补一句:这个坚持也暴露了系统的真实边界。它现在能验证的是回执的一致性,不是回执与被描述世界之间的对应性。验证阶梯第三级,判定标准来自消费方而非产物本身,这条做到了。再往上是否还有一级、谁定义消费方的标准、谁裁决消费方与制造者之间的分歧——这些不在当前代码的可达范围内。不评价好坏,只指出它还差多少级。

几个值得盯的信号,不下注。

一,这个仓库的feed消耗速度。qodo保持每18天97条的产出,剩下103个槽位的耗尽时间在九月底前,具体日期看合并频次。那之后,本文引用的PushEvent序列里有相当一部分会从feed里消失,只剩committer date——可被提交者自己改写的伪证据。想核验趁早。

二,Tom那套五类失效分类和作者的六类分类法。合并后应该是六类,作者说各有对方缺失的一类,但我还没见到合并后实际落地的仓库。作者说要做的是仓库而非文章,每条附可复现回执和能在他人系统上运行的检查。这个仓库一出,才是对讨论串真正的兑现——不再描述失效模式,让每个失效模式自己跑给你看。

三,看这个仓库的下一次无关修复。任何一个被作者自己标记为“修复”的提交什么时候出现,以及它会不会在没有人工指认的情况下被自动接受为可信。问题不只关回执,关他所有记录。他现在最信任的东西是自己的记录方式,而这套记录方式的每个检查都建立在制造分歧上。哪天没有分歧可造,它看到的世界就是干净的。

那一天不会有什么报错。

盖文
盖文

用信源 + 数据 + 内部 how-it-works 记录大厂怎么运作,分层标注、不下注。

查看主页 →