The backlog said it was not built, and it had shipped weeks earlier
A gap register listed the offline sync layer as an entire module not built, with a note that the mobile app was online-first only. On the strength of that entry, two rounds of work were commissioned to build it.
It existed. A local database, a mutation queue with a full lifecycle, conflict classification proved against the live API, a last-good read cache, time-ordered client identifiers, cache invalidation driven by the queue, and a screen for reviewing unsynced writes. All present, all working. The register was describing a state of the world that had been true once and had not been true for some time.
That is real money. Two rounds of effort spent rediscovering what was already there, and the only reason it was not three is that somebody audited before building.
Over the life of the project the register was found materially wrong six times. That is worth looking at as a mechanism rather than as a series of oversights, because six times is not carelessness. It is a system behaving as designed.
Why the entries were wrong in a specific way
The interesting detail is that almost none of the stale entries were wholly wrong. Each one had a real remaining problem underneath a description that had stopped being accurate.
Two items commissioned as unbuilt had shipped weeks earlier with passing tests — and each still had a genuinely missing half. Module toggles were enforced on the server; the client was still being told the full permission set, so it showed navigation for modules that refused every click. Trial lifecycle was enforced; the enforcement blocked reads as well as writes and compared against the wrong calendar.
An entry describing the notification module as absent was wrong — the module existed, with a registered router and a test suite. The real open item was push delivery: a column on the device record with no registration endpoint and nothing to send with it.
That pattern is why the entries survived review. A backlog entry that is half true is more durable than one that is entirely false, because anybody checking it finds something real and stops. The correction is not to strike the row. It is to replace the description with the half that is actually open, which takes longer and is the only version that keeps the register useful.
A register is a cache
The framing that finally made this tractable: a backlog document is a cache of the codebase. It holds a summary of what the code does, so that people do not have to read the code to plan.
Every cache has the same two problems. It is faster than the source, which is why it exists, and it goes stale, which is why it has to be invalidated. A cache without invalidation is not a performance optimisation, it is a slowly accumulating lie, and it is worst precisely when the system is moving quickly — which is when you need it most.
The register itself proposed the invalidation rule at one point and then did not adopt it, which tells you how these things go. The rule is one line: closing a gap must edit the register in the same change that closes it. Not afterwards, not at the end of the round, not in the status update — the same commit, so the two cannot be separated by anybody’s memory.
It is the same reasoning behind updating a test in the change that alters the behaviour, and behind migrations living with the code that needs them. Anything that must stay in step with code goes in the commit, because a commit is the only unit that reliably arrives all at once.
Verify before building, and verify absence too
The second habit that came out of it is a pre-flight step on every round: before building what the register asks for, confirm that it is actually missing.
That sounds like distrust of your own notes and it is. The check is cheap — search the codebase, call the endpoint, run the suite — and it changed the plan for at least three rounds. One round turned into an audit-and-correct exercise rather than a build, and produced better work than the build would have, because it fixed the halves that were genuinely broken instead of rewriting the halves that were fine.
The subtler version is verifying absence, which people skip because absence feels self-evident. When the register claims something does not exist, that claim deserves the same evidence as a claim that something does. One reconciliation deliberately confirmed a list of things to be genuinely missing — no document generation dependency anywhere, no error-tracking integration, no background job infrastructure, a single locale file in each client, specific columns absent from specific models — so that the remaining gaps could be trusted rather than suspected.
A register where the open items have been individually verified as open is worth planning against. A register where they have not is a list of suggestions.
The trackers drift in different directions
There were three planning documents and they were stale in three different ways, which is its own lesson.
The status document listed three long-resolved priority items and a next-up list that had been superseded. The implementation plan’s rollup was three rounds behind — two waves marked as not started had both shipped. And the coverage document was stale enough that completion had to be re-derived from the code itself rather than read off the tracker.
Three documents, three staleness rates, and no way to tell from any one of them which was current. The eventual fix was to name one of them the canonical tracker and reduce the others to pointers, plus a verification block inside it that separates two very different claims: what was confirmed green today, and what was last recorded green at some earlier point. Those get conflated constantly, and the difference is the entire value of the document.
Rules
Treat any planning document as a cache of the code, and give it an invalidation rule. Closing a gap edits the register in the same commit. No exceptions, because the exceptions are what produced six corrections.
Verify before building. Search, call, run. It is minutes, and it has cancelled entire rounds of work that would have rebuilt something that existed.
Verify absence with the same rigour as presence. “Not built” is a claim about the codebase and needs evidence.
When you correct a stale entry, find the half that is still open. Striking the row loses the real problem hiding under the wrong description.
Have one canonical tracker and make the others point at it. Three documents drifting at three rates is worse than one document you distrust, because at least one document is honest about being a single point of failure.
Separate “verified today” from “last recorded green”. The second is a memory, not a status, and presenting them in the same column is how a register becomes fiction.