On September 24, three versions of one of our npm packages went up in the space of about two and a half hours. The publish commands exited zero. The tarballs were on the registry. Every version was installable by exact version number.
And anyone who ran npm i <package> got the build from five days earlier.
When you run npm publish, npm does two things. It uploads a tarball under a version number, and it moves a named pointer โ by default latest โ to that version. Consumers almost never name a version. They run npm i foo, which resolves foo@latest. So the artifact people receive is chosen by the pointer, not by the set of versions that exist.
Those two operations are separable, and one of them is easy to skip without noticing:
Both of those are correct behaviour, and holding latest back on purpose is a legitimate release practice โ you may well want npm i to keep handing people the stable line while you stage something newer. The dangerous case is not the policy. It is not knowing which regime you are in.
The uncomfortable part is that this package was covered. It had four separate checks pointed at it, all green, and every one of them was asking a real question:
| Check | What it measured | Why it stayed green |
|---|---|---|
| Published-surface sweep | Every version exists, every tarball downloads, links in the README resolve | All true. All four versions were on the registry. |
| File-count drift | Whether the newest version ships fewer files than an older one | No file was missing from any version. |
| Version-literal check | Whether a version's own metadata files agree with the version they shipped under | Each version's package.json named itself correctly. |
| Repo โ tarball parity | Whether the published tarball matches the source tree byte for byte | The repo and the tarball agreed with each other. |
Every one of those is a statement about a version: does it exist, is it complete, is it internally consistent, does it match the source. All of them can be true of every version on the registry while the pointer sits on the wrong one.
What was missing was a statement about a relationship:
dist-tags.latest -> which version?
exists a non-deprecated version whose publish time is
strictly later than the publish time of the version
that `latest` names
No check was making that comparison, because every check was comparing siblings โ versions to their own files, tarballs to source, counts to counts. A pointer's value is not a property of the thing it points at. You can verify every property of every artefact and still be shipping the wrong one, because "which artefact the user receives" is a fact about the pointer.
By a person, reading a column.
Not by a check that failed. Not by a report. By looking at the version table, noticing that the latest column said one thing and the publish-date column said another, and going "wait." That is worth saying plainly, because it is the ordinary way this class of bug gets found, and it is the reason the class survives: the failure is silent and the state looks tidy.
The shape of it, in publish times taken from the registry: 0.2.4 at 2026-09-24T15:32:49Z, 0.2.5 at 15:33:35Z, 0.2.6 at 18:09:22Z โ and latest still naming 0.2.3, published the morning of September 19. A registry listing shows several versions, all recent, all complete. Nothing on that page looks broken unless you compare two specific columns against each other.
I am not going to put a number on how many installs resolved to the older build. I cannot measure it, and inventing a figure to make the story land would be exactly the failure this post is about. The package is small. The point is not the blast radius โ it is that nothing in the pipeline would have said a word, indefinitely.
Writing the check turned out to be the easy part. Deciding what to flag took longer, because the obvious formulation is wrong in two directions.
One of our other packages has a 4.0.x line that is semver-above the 1.2.x line we actually maintain, and it is deprecated. If retired versions counted, that pointer would be "behind" forever โ and a guard that is permanently red is a guard nobody reads. The retired line is already dispositioned; its relationship to latest is not a defect to chase. So: deprecated versions are excluded as a candidate and as a target.
The tempting predicate is "is latest the highest version?" That is wrong. Holding latest back is legitimate, as above, and a check that forbids it would be a check that forbids a policy rather than a bug. The predicate that survives is about time: is there a non-deprecated version published after the one the tag names? Publish time is evidence; semver is opinion. A version number does not tell you whether a human meant to point at it.
If the whole package is retired there is no live pointer to be behind, so the check reports and judges nothing โ the same convention the neighbouring guards use, so that "clean" means the same thing across all of them.
Everything the predicate needs is in the packument: dist-tags gives the pointer, time gives every version's publish timestamp, and the deprecation marker is right there. No tarballs are downloaded. For our 23 packages it is 23 metadata fetches per run.
If you maintain a package and want to check it yourself, this is the whole measurement:
npm view <pkg> dist-tags time --json
Read dist-tags.latest, look up that version's time, and compare it against the times of every other version that is not deprecated. If something is later, the pointer is behind, and the fix is one command:
npm dist-tag add <pkg>@<version> latest
Note what that command is: it moves the pointer. No republish, no new version, no tarball. Which is the other half of the lesson โ the thing that was wrong was never in the package.
A check like this is easy to test wrongly, and I did, first try.
The positive control reconstructs the actual incident: take the live packument for that package, pin latest back to the historical value it was stuck on, and assert that the check โ the deployed function, not a copy of its logic โ reports the versions that came after. That much is right. What was wrong was the assertion: the first version asserted the exact set ['0.2.4', '0.2.5'] and the function returned four versions, because 0.2.6 and 0.2.7 were also published after the pinned tag. The function was correct; the assertion was stale the moment the third version shipped.
Which is its own small instance of the same theme: a fixed answer compared against a moving world will fail for the wrong reason and then get "fixed" by loosening the thing that was actually working. The control now asserts containment plus ordering โ the incident's versions are present, and they come back sorted โ so it keeps meaning the same thing as the package evolves.
This is not an npm story. It is a story about every place a published artefact and a published pointer to an artefact live in different fields:
The generalisation: if a system has an indirection in it, at least one check has to dereference that indirection and compare the result. Checks that only compare siblings will agree with each other all the way to the incident.
Two artefacts can be mutually consistent and both wrong. That is the same shape as a specification thread where everyone said yes and the normative text never changed โ a related post, Agreed Is Not Merged, is about the same gap one layer up. The conversation updates; the artefact does not.
Here the conversation was a publish that exited zero, and the artefact that did not update was a three-word pointer.
Notes from running an x402 pay-per-call API marketplace and its published tooling. The check described here is published-dist-tag-lag.py; it is one of four guards on the published surface, each of which exists because a real defect got past the other three.