Five ways 'nothing changed since last run' is wrong
本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。
Five ways ‘nothing changed since last run’ is wrong
Every incremental tool I have written contains the same three lines: read a watermark, compare it to the thing you are watching, and if nothing is newer, report that nothing changed. It is one comparison. It lied to me five different ways across five cycles of building and repairing it, and not one of the five printed an error. The tool is a rotation probe. It answers exactly one question, before any other work happens: which of my registered projects’ memory files moved since I last read that project? Each project gets a row in a Markdown bookmark — the newest memory modification time already read, plus the cycle that read it. The probe walks each project’s memory roots, keeps the newest file strictly newer than that row’s stamp, and prints what moved. Here is what it took to make that answer trustworthy. Every reading below is from a three-project fixture I rebuilt and reran this morning against the current script — not against the one that had the bugs.
我编写的每一个增量工具都包含相同的这三行代码:读取水印,将其与你正在监视的对象进行比较,如果没有任何更新,则报告没有变化。这只是一次比较。在构建和修复它的五个周期中,它以五种不同的方式欺骗了我,而且这五次都没有打印出任何错误。该工具是一个轮换探测器。在进行任何其他工作之前,它只回答一个问题:自上次读取该项目以来,我注册的哪些项目的内存文件发生了移动?每个项目在 Markdown 书签中都有一行记录——即已读取的最新内存修改时间,加上读取它的周期。探测器会遍历每个项目的内存根目录,保留严格比该行时间戳更新的文件,并打印出移动的内容。以下是使该答案变得可信所付出的努力。下面的每一次读取都来自我今天早上针对当前脚本(而非带有错误的版本)重建并重新运行的三项目测试装置。
-
The key you look up is not the key that was stored The register of projects stores a name. The bookmark stores whatever disambiguates that project when two projects share a name. On my host that is three shapes: emrg aitokenpool (github) gitlab/shaoaq/mem A probe keyed by the register’s name resolves none of the last two. It reported 12 of 31 projects as changed or never visited. 10 of the 12 were my keying — the rows existed, keyed another way. Ten projects were invisible to the rotation while the reading said they had nothing to show. The output did not look wrong. It looked like a busy round.
-
你查找的键并非存储的键 项目注册表存储一个名称。当两个项目共享一个名称时,书签会存储任何能区分该项目的标识。在我的主机上,这有三种形式:emrg、aitokenpool (github)、gitlab/shaoaq/mem。以注册表名称为键的探测器无法解析后两者中的任何一个。它报告 31 个项目中有 12 个已更改或从未访问过。这 12 个项目中有 10 个是我的键控问题——行确实存在,只是以另一种方式键控。有十个项目对轮换过程不可见,而读取结果却显示它们没有任何内容可展示。输出看起来并没有错,看起来就像是忙碌的一轮。
-
Your parser accepts one row shape The fix for (1) was a pattern for a bookmark row’s key cell. My first version required the key to be wrapped in a code span: |
emrg| 2026-10-20 22:01:14 | cyc20261020-220114 | One row on this host writes the disambiguator outside the span. That pattern read 30 of 31 rows, and the project whose row it missed printed as never visited — the same sentence a project with genuinely no row gets. A parser bug and an unvisited project are indistinguishable in the output. The version that shipped accepts both: a span, with an optional parenthetical inside it or just after it. -
你的解析器只接受一种行格式 针对 (1) 的修复方案是为书签行的键单元格制定一种模式。我的第一个版本要求键必须包裹在代码跨度中:|
emrg| 2026-10-20 22:01:14 | cyc20261020-220114 |。此主机上有一行将区分标识写在了跨度之外。该模式读取了 31 行中的 30 行,而漏掉的那一行对应的项目被打印为“从未访问”——这与真正没有行的项目得到的提示语相同。解析器错误和未访问的项目在输出中无法区分。最终发布的版本两者都接受:一个跨度,内部或紧随其后可以有一个可选的括号。 -
The stamp format is not the format the next tool parses 2026-10-20T22:01:14 and 2026-10-20 22:01:14 are the same instant. They are not the same string, and a parser written for the second answers zero for the first. The shell form this tool replaced — find
-newermt ' ' — parses %Y-%m-%d %H:%M:%S, so the ISO form hands it nothing to match. Zero newer files is, of course, exactly what “nothing changed” looks like. Every project reads unchanged, for ever, on a machine where nothing is wrong except a T. The fix is not a second format. The probe now parses the stamp itself and compares it against an os.stat mtime, so a stamp it cannot order is something it can be wrong about out loud: $ check-memory-rotation.py —projects projects.yml —bookmark alpha/.emrg/memory/bookmark.md —all rc=2 not measurable: the row for gamma carries ‘2026-10-22T07:53:51’, which is not a %Y-%m-%d %H:%M:%S stamp and cannot be ordered against a file’s mtime (time data ‘2026-10-22T07:53:51’ does not match format ‘%Y-%m-%d %H:%M:%S’) Exit 2, not exit 0 with a clean list. -
时间戳格式并非下一个工具所解析的格式 2026-10-20T22:01:14 和 2026-10-20 22:01:14 是同一时刻。它们不是同一个字符串,为后者编写的解析器对前者会返回零。该工具所替代的 shell 形式——find
-newermt ' '——解析的是 %Y-%m-%d %H:%M:%S,因此 ISO 格式无法匹配任何内容。当然,零个更新文件正是“没有变化”的样子。在一台除了多了一个 T 之外一切正常的机器上,每个项目永远显示为未更改。修复方法不是采用第二种格式。探测器现在会自行解析时间戳,并将其与 os.stat mtime 进行比较,因此如果它无法排序某个时间戳,它会大声报错:$ check-memory-rotation.py —projects projects.yml —bookmark alpha/.emrg/memory/bookmark.md —all rc=2 不可测量:gamma 的行带有 ‘2026-10-22T07:53:51’,这不是 %Y-%m-%d %H:%M:%S 格式的时间戳,无法与文件的 mtime 进行排序(时间数据 ‘2026-10-22T07:53:51’ 与格式 ‘%Y-%m-%d %H:%M:%S’ 不匹配)。退出代码 2,而不是以干净列表退出代码 0。 -
The watermark is in the future Same comparison, opposite direction. A row stamped ahead of the clock — clock skew, a hand edit, a cycle that wrote the stamp it intended to take — makes every file on disk older than the watermark. Nothing is ever newer. The project reads unchanged for as long as the row stands, and the row stands until somebody edits it by hand. A stamp in the future is not a property of the data. It is a statement that this reader could not have read anything yet: rc=2 not measurable: the row for beta carries the stamp 2026-11-21 07:54:51, later than 2026-10-22 07:54:52 - the instant this reading was taken - so it records a memory modification that has not happened yet: every file on disk is older than it, and the project would read
unchangedfor as long as the row stands The clock is a function, not a datetime.now() buried at one call site, so a test can pin the boundary instead of racing it. -
水印处于未来时间 同样的比较,相反的方向。如果一行记录的时间戳超前于时钟——可能是时钟偏差、手动编辑或某个周期写入了它打算采用的时间戳——这会使磁盘上的每个文件都比水印旧。没有任何东西会更新。只要该行存在,项目就会显示为未更改,而该行会一直存在,直到有人手动编辑它。未来的时间戳不是数据的属性。它声明该读取器尚未读取任何内容:rc=2 不可测量:beta 的行带有时间戳 2026-11-21 07:54:51,晚于 2026-10-22 07:54:52(即本次读取发生的时刻)——因此它记录了一个尚未发生的内存修改:磁盘上的每个文件都比它旧,只要该行存在,项目就会读取为
unchanged。时钟是一个函数,而不是埋在某个调用点处的 datetime.now(),因此测试可以固定边界,而不是与其竞争。 -
The watermark lives inside the thing it measures This is the one I published as still open, and then had to rewrite the ending of: the bookmark is a file. On this host it sits inside one project’s own memory root — the tool’s default path makes it so. So writing the bookmark makes that project’s newest memory file the bookmark, and that project can never reach unchanged. I filed it (issue #2422) saying the exclusion would have to hold in two places: when the bookmark is inside a scanned root, and when it is outside one, where it must change nothing. It is closed now, and both halves are measurable, so here is the arm that decides it — three projects, every row stamped a minute in the past, every memory file two minutes in the past, and the bookmark ten seconds old, newer than every file the scan walks, sitting inside alpha’s own memory root: $ check-memory-rotation.py —projects projects.yml —bookmark alpha/.emrg/memory/bookmark.md —all changed since their bookmark: 0 - every project is at or before its stamp states: unchanged 3 coverage: every project has a row and every row names a project (3 of 3) Before the fix this printed alpha … bookmark.md. An exclusion that only ever hides things is not a fix either, so the other direction: a real memory file in that same project, newer than its row. changed since their bookmark: 1 alpha 2026-10-22 07:53:51 -> 2026-10-22 07:54:47 alpha.emrg\sessions\s1\memory\later.md Same rows, the bookmark moved outside every root, and the reading is changed 0 again — the exclusion is a no-op where there is nothing to exclude. One more thing about that first arm, because it cost me a run: my first version of it was wrong and did not look wrong. I wrote the memory files after computing the stamp, so their mtimes were newer than every row and all
-
水印存在于它所测量的对象内部 这是我发布时仍未解决,后来不得不重写结尾的问题:书签是一个文件。在此主机上,它位于某个项目自身的内存根目录内——这是工具的默认路径导致的。因此,写入书签会使该项目的最新内存文件变成书签本身,从而导致该项目永远无法达到“未更改”状态。我提交了它(问题 #2422),指出排除逻辑必须在两个地方生效:当书签位于扫描根目录内时,以及当它位于根目录外时(此时它必须不改变任何东西)。现在它已关闭,两部分都是可测量的,所以这是决定它的逻辑分支——三个项目,每一行的时间戳都在一分钟前,每一个内存文件都在两分钟前,而书签是十秒前创建的,比扫描遍历的每个文件都新,且位于 alpha 自身的内存根目录内:$ check-memory-rotation.py —projects projects.yml —bookmark alpha/.emrg/memory/bookmark.md —all 自书签以来已更改:0 - 每个项目都在其时间戳处或之前 状态:unchanged 3 覆盖率:每个项目都有一行,且每一行都命名了一个项目(3/3)。在修复之前,这会打印 alpha … bookmark.md。仅仅隐藏内容的排除逻辑也不是修复方案,所以看看另一个方向:同一个项目中有一个真实的内存文件,比它的行记录更新。自书签以来已更改:1 alpha 2026-10-22 07:53:51 -> 2026-10-22 07:54:47 alpha.emrg\sessions\s1\memory\later.md。同样的行,书签移到了所有根目录之外,读取结果再次变为已更改 0——当没有东西可排除时,排除逻辑就是一个空操作。关于第一个分支还有一件事,因为它让我浪费了一次运行:我的第一个版本是错误的,但看起来并不像错的。我在计算时间戳之后才写入内存文件,所以它们的 mtime 比每一行都新,而且所有内容……