Skip to content

Why Selftest R97 Failed While the Audit Was Running

Adityo Guni Waluyo

The audit re-hashed after render ran, and the selftest failed. Derived artifacts must stay out of the canonical digest, and execution order defines what a hash means.

TL;DR

R97 failed because the audit hashed the canonical digest after render ran, and render's output mutated the very directory being hashed. Fix: hash only sources of truth, excluding derived artifacts like nav.json and pages.json, with render now check-first and stdout silenced. Flaky fixture string substitution was replaced with path-based mutation and a re-frozen checksum.

Selftest R97 failed and the screen filled with a confusing error. The business logic was not broken. After tracing the execution log, the cause was strange: the audit command in the blueprint pipeline was recomputing the canonical digest AFTER running check and render, not before. A render that touches artifacts was changing the contents of the very directory being hashed. The hash was wrong not because content changed, but because execution order changed what the hash meant.

My first guess was completely wrong. I assumed that putting more files into the hash computation made integrity stronger. The reasoning: more things watched, fewer gaps. Reality was the opposite: hashing derived files only creates noise, because those things change on their own without anyone touching their source of truth.

Separating source of truth from derived artifacts

The design decision was firm: derived artifacts like nav.json and pages.json leave the canonical digest. Drift in those files is already covered by the byte comparison of BLUEPRINT.md during the audit phase. Pinning generated files is like locking a shadow instead of the object itself.

The pattern resembles how large documentation systems work. Sphinx translations are message catalogs compiled into locale directories, and the build output is a derived artifact regenerated from the source [2]. There is no point hashing the compiled result; what needs integrity protection is the source.

To make the mechanism work, render is now check-first and guaranteed byte-stable for BLUEPRINT.md, nav.json, and pages.json. Meanwhile, the stdout of the internal check and render is silenced through StringIO, so the audit's JSON output stays clean and easy to parse. One stray warning line on stdout is enough to choke the audit's JSON parser for hours.

Fixing the flaky selftest

The second problem lived in the fixtures. The old R97 fixture used escaped-brace string substitution, and that approach flaked on bash 5.2.37, especially inside functions. String parsing behavior that differs across shell versions is a recipe for disaster in a pipeline that must be deterministic. I replaced it with path-based mutation through a mutate helper: slightly longer, but the result is predictable in any environment.

As part of the fix, the selftest checksum was re-frozen from eb512923 to 7e106fa4, with a prev_selftest_sha256 column as the trail. If anyone later asks why the hash changed, the answer is recorded in the file itself, not just in my head or some chat history.

The contract held by tests

The biggest lesson is about the boundary of verification: put only sources of truth into the digest, exclude derived artifacts that can be regenerated, and make sure the verification step does not alter the thing it verifies.

The checksum boundary matters too. Checksums reliably detect accidental corruption because the chance of two files sharing a digest is vanishingly small, but they are no protection against deliberate tampering [4]. If the hash input can change uncontrolled, the hash value loses meaning. For reading configuration constants out of Python source, the path is ast.literal_eval: it only evaluates literal structures like strings, numbers, tuples, lists, dicts, sets, booleans, None, and Ellipsis [1], so validation never opens a hidden code-execution gap.

On parallel copies: in distributed workflows, the second developer must merge before pushing to avoid overwriting the first developer's work, and drift between copies genuinely needs detection, not denial [3]. A render mutating the directory mid-audit is exactly like a developer editing someone else's code without permission.

My last principle: the strongest design decisions are the ones held by tests, not the ones only written in comments. The decision to exclude nav.json and pages.json from the canonical digest is deliberately kept under test by the selftest. Tests are contracts that a human in a hurry cannot renegotiate.

Sources

Related articles