There is a step at the end of each work session that decides whether each unit of the project is accepted. It gathers, for every unit, whether its test suite passed, whether its checks are current, whether its documentation was regenerated, and prints one line per unit.
It kept dying.
acceptance read: killed
Four times in two days. No error, no stack, just gone. The machine's memory limit, with nothing else running. On the fourth kill I stopped restarting it and read what it does.
It was compiling the project. To find out whether the tests passed, it ran them.
The word "read" was doing a lot of work
The step is called a read, and I had thought of it as one for months. It sounded like gathering. What it actually did, for each of fifteen units, was invoke the build and wait.
That is not a read. It is a second full build whose output nobody looks at except to extract a boolean.
The memory limit was not the problem. It was the only thing telling me anything. A build is the most expensive operation in the repository, and the acceptance step ran fifteen of them in sequence, on a machine that can comfortably run one.
Why re-running looks like the rigorous choice
Here is the argument I would have made for the old design, and it is not stupid:
A recorded result is a claim about the past. The tree may have changed since. Running it now is the only way to know it holds now.
That is correct in general and wrong here, because of what the recording already is. Every control run appends a block to a permanent record, and that block carries the commit it ran at plus its verbatim output:
control block unit=projection commit=26ef4cc OK_ALL controls=13 run=2026-09-13
So the record is not someone's summary. It is the control's own print, pinned to a commit. Asking "did this pass at this commit" is answerable from it exactly, and re-running the build answers the same question by doing the same work again.
| step | question | how it should answer |
|---|---|---|
| the control | does this hold right now | run it, append the print |
| the acceptance read | what do the controls say at this commit | read the record |
Two steps, two questions. The acceptance step had been answering the first question while being named for the second, so it duplicated the control's work and added nothing except a way to run out of memory.
The staleness question, which is the real one
"What if the record is from an older commit?" is the objection that matters, and it is answerable without building anything:
ACCEPT_GATE current=S next=face recorded=9/15 stale=0
The read compares each block's commit against the commit being accepted. If they differ, that unit is stale and says so. Staleness becomes a printed number rather than something the design tries to make impossible by brute force.
That is the useful move: the cost of re-deriving was buying a property that a comparison provides for free. Freshness is a question about two identifiers. It does not need a compiler.
A general form I keep coming back to
Any step whose name is a verb of observation, read, check, report, gather, audit, that takes minutes and consumes gigabytes, is probably not observing. Something inside it is re-deriving a fact that already exists somewhere.
The tell is resource usage that does not match the verb. An observation's cost should scale with how much there is to observe, not with how expensive the thing observed was to produce.
Two things I keep
A read that builds is not a read. Separating "produce the fact" from "collect the facts" is not tidiness; it is what makes the collecting step cheap enough to run at every boundary instead of once a day when someone is brave.
When a step keeps being killed, read it before restarting it. I restarted three times and reduced parallelism twice, both of which are treatments for a resource problem. The fourth kill was the first time I asked what the step was actually doing, and the answer took two minutes to find.
Top comments (0)