Every unit of this project carries a record of its last verification, and each record pins the configuration it was checked against by hashing it. If the configuration changes, the pin no longer reproduces and the record goes stale, which is exactly what you want: a verification result that quietly outlives its inputs is worse than no result.
One afternoon I added a six-line block to that configuration file. It described a new component and touched nothing else.
Fifteen units went stale. Every one of them owed a full re-run of its checks and a fresh record appended afterwards, which is most of an hour of machine time and a boundary ceremony, for six lines that concerned one of them.
Nothing was broken. The check did precisely what it promised.
The pin was over the whole file
pin = sha256(entire configuration file)
Any byte anywhere changes that hash. So the pin does not mean "the configuration this unit depends on is unchanged". It means "nobody has edited this file at all", which is a far stronger statement and one that becomes false constantly in a file every component writes to.
The gap between what a pin means and what everyone reads it to mean is the whole bug, and it is invisible while the file is small.
| what the pin says | what everyone reads it as |
|---|---|
| this file is byte-identical | my inputs are unchanged |
| any edit invalidates me | edits to my section invalidate me |
The failure mode is people, not machines
An hour of machine time is affordable. What is not affordable is what it does to behaviour.
When correcting a six-line block costs fifteen re-runs, three things start happening. I watched all three:
- edits get batched until there are enough to justify the ceremony, so the configuration is wrong for longer
- the staleness line gets read past, because it is almost always red for reasons unrelated to what you are doing
- someone proposes re-pinning without re-running, which is the point where the check stops meaning anything
A check whose cost is out of proportion to the change gets routed around. It does not get fixed, because it is not broken.
The repair is a declared partition
The pin became the hash of the unit's own block plus the sections genuinely shared between units, rather than the whole file:
pin = sha256(block for this unit) + sha256(shared sections)
Two properties matter more than the saving. The partition is declared, as rows in the file itself rather than inferred by a parser guessing at section boundaries, so it is readable and arguable. And it is recomputable by hand, since the recipe is printed next to the hash and anyone can rerun it with sha256sum.
Now an edit to one unit's block stales one unit. An edit to a shared section stales everything, and that is correct, because everything does depend on it.
What I would check earlier next time
There is a cheap diagnostic I did not run for months. Take the last N edits to the file and ask, for each, how many units the pin invalidated and how many actually depended on the change.
edits sampled 20
units invalidated 15 each time
units actually affected 1, 1, 1, 2, 1, 15, 1, ...
One row genuinely needed all fifteen. The rest were a partition problem visible in five minutes of arithmetic over the git log, and the ratio is the argument for fixing it.
Two things I keep
Hash granularity is an interface, not an implementation detail. Choosing the whole file is choosing "any edit invalidates every dependant", and that choice deserves the same scrutiny as any other contract, especially in a file that many components share.
"Correct but disproportionate" is a defect class of its own. It never shows up as a failing check, so nothing surfaces it. It shows up as people batching their edits, skimming past a red line, and eventually proposing to weaken the check, and by then the conversation is about the check rather than about its granularity.
Top comments (0)