Back to Blog
Infrastructure & DevOps · Essay

Three lists drifted from their registry this month. Nothing turned red.

Simon Doba·August 16, 2026·7 min read

The most expensive thing I shipped this month was not a bad decision. It was the same small one, three times, in three different files, and none of my checks had anything to say about any of them.

Each time it was a list maintained by hand next to a list that already existed.

What it looked like

Three instances, all in the same fortnight, all in a codebase I know well.

  • A sitemap with its own copy of the routes. The generator held a hardcoded array of experiment slugs, with a // keep in sync comment above it. It was out of sync. Three deleted pages stayed in the sitemap and kept getting crawled after they were gone, and two new ones never appeared in it at all.
  • An index page with its own copy of the entries. Two pages had detail routes, social cards, sitemap entries and prerendering, and were listed nowhere, because the index kept its own array rather than reading the registry the detail route reads. Nothing on the site linked to them. They were live and unreachable.
  • A generated text file that was not generated. A machine-readable index of published posts, maintained by editing it. It listed three unpublished posts with full abstracts.

Every one of these passed the type checker, the linter and the test suite, because none of those tools has any opinion about whether two lists agree.

Why it survives every check you have

A drifted list is not a broken one. It is a valid array of valid values, and every consumer handles it correctly. The failure is entirely in what is absent, and absence produces no error.

Worse, the usual symptom is not a crash but a silent skip. A page that is not in the index does not throw; it just is not there. A slug that is not in a style map gets no card. A post that should not be listed gets listed and reads fine.

So it lands in the one category automated checks are worst at: output that is well-formed and incomplete. I have written about this from the reviewing side — the errors were in confident summaries rather than in anything citing a path and a value. Same shape here. The list is a summary of another list, and summaries are where things go missing without contradiction.

The rule I would give someone starting

Two clauses, in order.

Derive rather than duplicate. If the information exists in the codebase, read it. A sitemap should map over the registry. An index should map over the registry. A comment that says keep in sync is a note that you have chosen a manual process, and it will be wrong eventually.

All three of the above were fixed by deleting the second list and importing the first. None of the fixes took longer than the time it took to find them.

Where you cannot derive, validate coverage and fail loudly. Sometimes the second list carries information the first one does not — a per-entry style, a translated label, a description written for a different audience. That is legitimate. What is not legitimate is skipping the entries it lacks.

The one place in this codebase that got this right was the social card generator. Every route needs a card style, and the styles are hand-written because they are visual choices. It compares the two sets on every run and refuses to start when they disagree:

Blog OG style registry is out of sync; missing: expertise-is-the-ceiling, checking-the-reviewer

That is an annoying error and it is the correct one. The alternative — generate the cards it knows about, quietly skip the rest — would ship two posts with no social preview and never mention it. A crash costs a minute. A silent skip costs however long it takes someone to notice a missing thing.

Where I would put the check

In the build, not in review. I added a structural checker for the content in this repo and wired it into prebuild, ordered so validation runs before anything expensive:

"prebuild": "sync-generated-files && check-content && generate-images"

Generate last, because rendering images takes minutes and there is no reason to spend them on content that is about to fail. Fail-fast ordering costs nothing to write and saves the entire tail of the pipeline.

Before you add one: the moment that checker existed it reported eighteen pre-existing violations across five older files. Nothing had regressed. They had always been that way, and the absence of a check had been reading as a pass for months.

Two of the eighteen were the checker being stricter than the rule it enforced, which I fixed in the checker rather than the content. That is worth budgeting for — the first run of a new check is a conversation about the rule, not a list of bugs.

What this does not solve

None of the three drifts were found by tooling. Two were found by a person noticing that something they expected to see was not there. One was found while writing about something else entirely.

The checks I added stop these three from recurring. They would not have found them. A check tests the rule you already know; discovering that a rule was needed is still a human noticing an absence, and I do not have a mechanism for that — the same gap I ran into when making design system rules machine-readable, where the constraints could only ever encode what I had already thought of.

Next

The obvious extension is a generic check: for every registry in the codebase, assert that any other array of the same identifiers is derived from it rather than restated. I suspect that is either trivial or impossible depending on how the identifiers are written, and I have not tried it.

Until then I am doing the boring version — when I add a registry, I grep for its identifiers before I finish, and see how many other places already know them.

If you have run into this: did anything in your pipeline actually catch a drifted list, or did someone notice a gap? I have only the second kind of story, and I would like to hear a first.

All three instances are from one codebase over two weeks in July and August 2026. The counts — three drifts, eighteen pre-existing violations on the first run of a new check — are from that repository, not an estimate.

Share this article

Building something similar?

I write about setups I actually use. If you're working on something comparable, I'd be curious what your workflow looks like.

Get in touch

Cookie Settings

We use cookies for analytics and to improve our website. Privacy policy