Skip to content

npm: the dev channel's versions don't order by recency, so a range resolves to a two-month-old build #475

Description

@EricAndrechek

Area: infra / npm publishing — correctness of the published version graph · related to #268 (which describes the content-addressed dev channel as designed)

Expected: installing from the dev channel gets you a recent build, and keeps getting you recent builds.

Actual: it can silently pin a build from two months ago, because the dev channel's version numbers do not order by recency.

Every dev publish is 0.0.0-dev.h<dist-hash> (#268: "every push to main builds the SDK and publishes 0.0.0-dev.h<dist-hash> to the dev dist-tag"). A content hash is deliberate and reasonable for identity — but semver compares alphanumeric prerelease identifiers lexically, so a hash makes the ordering arbitrary with respect to time.

Today's published set, in publish order:

2026-06-04   0.0.0-dev.0f8826c          ← also the `latest` dist-tag
2026-06-04   0.0.0-dev.h7d02b5f9a5e4
2026-06-05   0.0.0-dev.h825a5d7fa405
2026-06-05   0.0.0-dev.h97a11a9f5a0e
2026-06-08   0.0.0-dev.hab0797ca5a74
2026-06-08   0.0.0-dev.hff4a2a48d0da   ← SEMVER-HIGHEST of all ten
2026-06-08   0.0.0-dev.h514be5522d89
2026-06-12   0.0.0-dev.h8ea15a023aa2
2026-08-11   0.0.0-dev.hb0c3a577b2f8
2026-08-12   0.0.0-dev.h04a85f20bb96   ← current `dev` dist-tag

The newest build sorts sixth. A June 8 build is the highest version in the package. hff… > h04… lexically, and that is the whole story.

Why that bites in practice

Installing from the tag resolves correctly at that moment — but what gets recorded in a consumer's package.json is a range, not the tag. ^0.0.0-dev.h04a85f20bb96 means >=0.0.0-dev.h04a85f20bb96 <0.0.1, and 0.0.0-dev.hff4a2a48d0da satisfies it. The next resolution takes the highest match, which is the June 8 build. The dist-tag pointer is correct and gets thrown away one step later.

Observed exactly that: an @dev install landed on hff4a2a48d0da (June 8), which predates both #448 (baseURL path prefix) and #456 (options.headers / fetchOptions / fetch) by two months.

Why it's worse than an ordinary stale dependency

  • It fails silently and late. The install succeeds, types resolve, the build passes. What you get is a client missing whatever landed since — surfacing as "this documented option doesn't exist" or as wrong runtime behaviour, with a version string that looks like a recent dev build.
  • The lockfile then makes it sticky. Once resolved, it's pinned, so the staleness persists across installs and CI until someone notices and digs into why.
  • It's on the documented path. npm publishing: post-launch follow-ups (provenance, release path, latest tag, docs) #268 presents the dev channel as how you consume unreleased changes, so a consumer following that advice is the one exposed.
  • It gets worse as the channel grows. Every new publish is a coin flip on whether it sorts above hff4a2a48d0da; roughly half of future builds will be invisible to a range that has already resolved past them.

Adjacent, and already noted in #268: latest points at 0.0.0-dev.0f8826c, the very first publish — which also happens to sort below every h-prefixed version, since 0 < h. So bare npm install @wavehouse/sdk and a range-based install can disagree about what "newest" means in opposite directions.

Not proposing a change here

Flagging the property and its consequences — whether the ordering matters enough to change the scheme, and what that would cost against the content-addressed identity #268 chose on purpose, is a judgement for whoever owns the release path.


Found while pinning the SDK in a downstream app, 2026-08-13.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/infraCI, build, deploy, Docker, release

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions