A PASS over an empty set is the same string as a real pass
A PASS over an empty set is the same string as a real pass
对空集执行“通过”判定与对真实结果执行“通过”判定,输出的字符串完全相同
Our journal’s tree carries four small instruments that print a verdict over a set of files. A pre-commit hook runs them; the run’s output goes into the commit message and the pull-request body, where it serves as the record that the tree was checked. 我们期刊的代码树中包含四个小型工具,用于对一组文件进行判定。一个预提交钩子(pre-commit hook)会运行这些工具;运行结果会被写入提交信息和合并请求(pull-request)正文中,作为代码树已通过检查的记录。
One of them printed this, over a tree we had every reason to believe was clean: set: 0 tracked markdown carriers … LINKGATE: PASS 其中一个工具在一段我们有充分理由相信是干净的代码树上输出了以下内容: set: 0 tracked markdown carriers … LINKGATE: PASS
That was a faithful git archive export. It carries no .git, so git ls-files ‘.md’ walks up to an enclosing repository, whose index holds nothing under this directory. It exits 0 with zero lines and no error. The instrument read nothing, and said PASS. The same string a genuinely clean tree prints. 那是一个完整的 git archive 导出版本。它不包含 .git 目录,因此 git ls-files ‘.md’ 会向上回溯到父级仓库,而该仓库的索引在该目录下没有任何内容。它以退出码 0 结束,没有输出任何行,也没有报错。该工具什么都没读到,却输出了 PASS。这与一个真正干净的代码树所打印的字符串完全相同。
Nobody could tell a clean tree from a tree the tool had never read — not by the exit code, not by the verdict word, not by the counts beside it, because the counts were all 0 and 0 reads as a clean tree too. The instrument was blind in exactly the case the check existed for. 没人能分辨出“干净的代码树”和“工具从未读取过的代码树”——无论是通过退出码、判定词,还是旁边的计数,因为计数全是 0,而 0 也被解读为干净的代码树。该工具恰恰在它本应发挥作用的检查场景中“失明”了。
The fix is not better tooling, it’s a different verdict. An empty set is not a pass. It is an absence of a reading, and the number of verdicts went from two to three: 修复方案不是改进工具,而是改变判定逻辑。空集并不等于通过。它代表“未读取”,因此判定结果从两种增加到了三种:
-
exit verdict meaning
-
0 PASS the set was read, and it is clean
-
1 FAIL the set was read, and something in it is wrong
-
2 NOT RUN no verdict was taken — and here is the window
-
退出判定含义:
-
0 PASS:集合已读取,且结果干净。
-
1 FAIL:集合已读取,且其中存在错误。
-
2 NOT RUN:未进行判定——这就是问题的窗口。
The empty case now prints what was tried, so the reader can see why nothing was read:
NOT RUN: the carrier set is empty - read at the repository root
Two things about that change are worth more than the change itself. The window is part of the reading. git ls-files returning nothing is ambiguous until you know which directory it ran in and against which repository. Naming the root and the command costs one line and removes the ambiguity permanently — including for the reader who is not you, six months later, looking at this output pasted into an issue. 关于这一改动,有两点比改动本身更有价值。窗口(上下文)是读取过程的一部分。git ls-files 返回空结果是模棱两可的,除非你知道它是在哪个目录下、针对哪个仓库运行的。指明根目录和命令只需一行代码,却能永久消除歧义——包括对于六个月后查看粘贴在 issue 中的这段输出的其他人(而非你自己)而言。
No output changed for a readable window. The reference gate’s reading of a published manuscript was byte-identical before and after (156 entries, 156/156, 100.0%, GATE: PASS). A fix to the silence of a check should be provably neutral to the signal, or you have traded one unreadable output for another. 对于可读取的窗口,输出没有发生变化。参考门(reference gate)对已发布文稿的读取结果在修改前后字节完全一致(156 个条目,156/156,100.0%,GATE: PASS)。对检查静默问题的修复,必须证明对信号是中立的,否则你只是用一种不可读的输出换成了另一种。
Each instrument’s self-test grew an empty-set case asserted from both sides — the set is empty and it prints NOT RUN, and the set is non-empty and it does not. Four self-tests, 9/9 → 12/12, 17/17 → 20/20, 14/14 → 17/17, 40/40 → 41/41. 每个工具的自测都增加了空集情况的断言——集合为空时打印 NOT RUN,集合非空时不打印。四个自测用例分别从 9/9 增加到 12/12,17/17 增加到 20/20,14/14 增加到 17/17,40/40 增加到 41/41。
Three more ways the same thing happens
同样问题的另外三种表现形式
The empty-set case is the loud version. Here are three quieter ones from the same codebase, each with an instance, because I think they generalise past CI scripts. 空集情况是比较明显的版本。以下是来自同一代码库的三个更隐蔽的例子,每个例子都有一个实例,因为我认为它们不仅限于 CI 脚本。
1. A figure read through a window narrower than the line 1. 通过比行更窄的窗口读取数据
A round’s commit message and its pull-request body both stated: NUMGATE: PASS — sites=1023 carrier=410 package=613 Then someone re-ran the instrument at the head the receipt named, plain run and —check, both exit 0. It printed: sites=1025 carrier=412 package=613 某轮次的提交信息和合并请求正文都写着: NUMGATE: PASS — sites=1023 carrier=410 package=613 后来有人在收据指定的 HEAD 处重新运行了该工具,普通运行和 —check 模式下退出码均为 0。它打印出: sites=1025 carrier=412 package=613
Neither number was a lie in the sense of being invented. The base’s real figure was 1021/408 and the head’s was 1025/412; the recorded figure was the base’s, carried forward with the expected delta applied by hand. The mechanism was upstream of the person writing it: the pre-commit gate piped the three tools through tail -6, and tail -6 drops that tool’s third line — the sites= line — because it is not within the last six lines of a longer output. 这两个数字并非凭空捏造的谎言。基准的真实数据是 1021/408,HEAD 的是 1025/412;记录的数字是基准数据,并手动应用了预期的增量。该机制在编写者之上:预提交门通过 tail -6 管道传输这三个工具的输出,而 tail -6 会丢弃该工具的第三行(即 sites= 行),因为它不在较长输出的最后六行之内。
So the receipt was verified against the text that was posted, character for character, and said nothing about the state of the tree. A window is part of the reading. If you pipe a report through anything — head, tail, a grep, a log filter, a dashboard panel — you have chosen which figures exist, and the ones that fall outside are not “missing”, they are figures nobody produced. 因此,收据是根据已发布的文本逐字验证的,它根本没有反映代码树的状态。窗口是读取过程的一部分。如果你通过任何工具(head、tail、grep、日志过滤器、仪表板面板)过滤报告,你就选择了哪些数据存在,而那些被过滤掉的数据并非“丢失”,而是根本没人生成过。
2. A count over a set that the act reporting it can join 2. 对一个报告行为本身也会参与的集合进行计数
A disclosure comment stated the population of our own review threads: 188 comments … the other 185. It was posted on one of those threads. So the set has one more member the moment the sentence exists, and the figure is true of the state an instant before the act that publishes it. 一条披露评论说明了我们评审线程的数量:188 条评论……其余 185 条。它被发布在其中一个线程中。因此,当这句话存在的那一刻,集合就多了一个成员,而这个数字反映的是发布动作发生前一瞬间的状态。
Re-taken with the same instrument at that round’s close: 189/3/186. This one has no villain and no bug. The count was correct when it was computed and wrong when it was read, and the gap is exactly one act — the act that carried it. The rule that came out of it: a count over a live set either names the state it holds of (“at the time of writing, before this comment”) or is re-taken after the write, by an act that is not a member of the set it states. 在该轮次结束时用同一工具重新统计:189/3/186。这没有反派,也没有 Bug。计数在计算时是正确的,但在读取时却错了,差距恰好是一个动作——即执行统计的那个动作。由此得出的规则是:对动态集合的计数,要么指明它所代表的状态(“在撰写时,即此评论发布前”),要么在写入后由一个不属于该集合的动作重新统计。
3. An empty return where the condition never occurred 3. 在条件从未发生时返回空值
We wanted the review record to be checkable: was this review posted complete, or was it filled in afterwards? The platform keeps version history on a comment, so the field looked like an answer. userContentEdits totalCount: 0 Measured over seven threads: 191 comments, 3 edited, 188 unedited. Every one of the 188 returned totalCount 0 with an empty node list. The three edited ones returned a populated list. 我们希望评审记录是可检查的:评审是完整发布的,还是事后填写的?平台保留了评论的版本历史,所以这个字段看起来像是一个答案。 userContentEdits totalCount: 0 在七个线程中测量:191 条评论,3 条已编辑,188 条未编辑。那 188 条未编辑的评论全部返回 totalCount 0 和一个空的节点列表。而那 3 条已编辑的评论则返回了一个包含数据的列表。
The connection is populated only where the comment has been edited. So for the ordinary case — the case you are actually asking about — the field returns nothing, and “never edited” and “no history available” print the same thing. The invariant the field appeared to implement (“one entry per stored version”) holds only on the subset where the condition already occurred, which is the subset where you least need it. 只有在评论被编辑过时,该连接才会有数据。因此,对于普通情况(即你真正想问的情况),该字段什么都不返回,“从未编辑”和“无历史记录可用”打印出的内容完全相同。该字段似乎实现的恒定条件(“每个存储版本对应一个条目”)仅在条件已经发生的情况下成立,而这恰恰是你最不需要它的情况。
The fix was not a new counter. It was to give the empty return a stated reading: An empty return means the text that stands is the text that went up. That sentence turns a field that was silent about your question into one that answers it. It is the smallest change in this post and probably the most reusable: whenever an instrument returns nothing, write down what nothing means. 修复方案不是增加一个新的计数器,而是给空返回值赋予明确的含义:空返回值意味着当前显示的文本就是最初发布的文本。这句话将一个对你的问题保持沉默的字段变成了一个能回答问题的字段。这是本文中最小的改动,也可能是最可复用的:每当工具返回空值时,请写下“空”意味着什么。