Developer Tools

Version Comparison Has No Standard

SemVer defines no range syntax. Debian, PEP 440, npm, Maven, RPM and Go each order versions differently, and the cross-ecosystem spec is still unpublished.

TechLogHub Editorial
October 10, 2026
8 min read
0 views

Share Article

Dark cover reading Version Compare Has No Spec, with rows for SemVer, RPM and vers

Version Comparison Has No Standard

Quick answer: SemVer is the most cited specification in software and it defines neither range syntax nor cross-ecosystem comparison. Ask where version ordering is actually specified and you get a Debian policy section, a PEP marked historical, a Java class javadoc, a C function, and a 91-assertion test file. The one cross-ecosystem spec for it, vers, is listed as In Development with a publication date of TBD.

What SemVer specifies, and what it leaves out

Semantic Versioning 2.0.0 is self-published by Tom Preston-Werner under CC BY 3.0. It defines MAJOR.MINOR.PATCH, optional pre-release and build metadata labels, a BNF grammar, regular expressions for valid version strings, and precedence rules. That is a real specification and it does its job.

What it does not define is range syntax. There is no caret in SemVer. No tilde, no wildcard, no 1.2.x. The spec’s only gesture at ranges is a prose example describing a dependency as “greater than or equal to 3.1.0 but less than 4.0.0”. Every ^1.2.3 you have ever typed is a convention your package manager invented.

It is unambiguous about one rule that gets broken downstream: “Build metadata MUST be ignored when determining version precedence.” Hold on to that; Go breaks it on purpose.

npm’s reference for caret and tilde is an implementation

Open npm’s package.json documentation and the dependencies section lists the full range vocabulary: exact, comparators, tilde, caret, x-ranges, hyphen ranges, wildcard and || unions, plus non-semver specifiers for tarballs, git URLs, dist-tags and npm: aliases.

Now look at what it cites. Tilde is “Approximately equivalent to version”. Caret is “Compatible with version”. Both link to node-semver — the npm package — not to semver.org. The page says a version “must be parseable by node-semver, which is bundled with npm”. The semver.org link appears only over in the peerDependencies section.

So for the range syntax at the centre of the npm ecosystem, the normative reference is a JavaScript library. There is no document to read; there is code to match. That is not a criticism of npm — node-semver is honest about being the source of truth.

Where each ecosystem’s ordering rules actually live

Jack Nesbitt’s survey of package management RFCs puts it directly: “Every ecosystem has its own versioning rules, and version comparison is the least settled part of the whole business.” Here is what “least settled” looks like when you go and read the primaries:

EcosystemWhere ordering is specifiedKind of artifact
DebianDebian Policy §5.6.12Normative prose in a policy document
PythonPEP 440 (Status: Final, Created 2013)A PEP its own page calls a historical document
npmnode-semver, bundled with npmA library
MavenComparableVersion javadocAPI documentation for one Java class
RPMthe rpmvercmp() routineA C function, plus a test file
Gothe module reference, deferring to SemVerA spec that cites another spec, then diverges
Cross-ecosystemvers (In Development, TBD)Unpublished

Two of those seven are written to be read as rules. The rest are implementations, docs for implementations, or nothing yet.

RPM documents its algorithm with a test suite

This is the clearest case. rpm.org’s dependencies manual says the comparison algorithm “is in the routine rpmvercmp()” and calls it “just a segmented strcmp(3)”: segment boundaries from isdigit(3)/isalpha(3), alphabetic segments by lexicographical ASCII, numeric segments with leading zeros stripped. It notes the algorithm has no knowledge of decimal fractions, so perl-5.6 is older than perl-5.00503 — and tells developers not to rely on the details.

It does not mention the tilde. It does not mention the caret. Both are load-bearing in real RPM versioning, and their behaviour is specified in tests/rpmvercmp.at, an Autotest m4 file with 91 active assertions (six more commented out as arguably buggy) and no prose at all: 1.0~rc1 below 1.0, 1.0^ above 1.0, 1.0^20160101 below 1.0.1. To know how RPM orders versions, you read the tests.

Debian wrote it down; Go cites SemVer and then diverges

Debian Policy §5.6.12 is the counterexample, worth reading once even if you never touch a .deb. A version is [epoch:]upstream_version[-debian_revision], compared in that order. Within a part the algorithm alternates: the leading non-digit run compared lexically, then the leading digit run compared numerically, with an empty string counting as zero, “until a difference is found or both strings are exhausted.”

The lexical comparison is ASCII with two documented modifications: “all the letters sort earlier than all the non-letters” and “a tilde sorts before anything, even the end of a part”. Policy even gives the sorted example: ~~, ~~a, ~, the empty part, a. That is a specification. You could implement it from the text alone.

Go goes the other way. The module reference says every version “starts with the letter v, followed by a semantic version” and points at SemVer for how versions are “formatted, interpreted, and compared”. Then: “The build metadata suffix is ignored for the purpose of comparing versions. The go command accepts versions with build metadata and converts them to pseudo-versions to maintain the total ordering between versions.”

SemVer says two versions differing only in build metadata have the same precedence. Go needs a total order, so it rewrites them until they differ. Its pseudo-versions exist precisely so that, in Go’s words, “a pseudo-version compares higher than its base version, but lower than the next tagged version.” Sound engineering; not the spec Go cites.

Python diverges explicitly rather than quietly. PEP 440 calls SemVer’s major.minor.micro clauses compatible, but bars SemVer’s hyphen pre-releases and plus build metadata from a public version, adds epochs, and mandates its own suffix order: .devN, aN, bN, rcN, no suffix, .postN. Its epoch example — 1!1.0 sorts after 2014.04 — is exactly what breaks a naive comparator.

Maven tokenises on both separators and on every digit-to-letter transition: 1.0alpha1 becomes [1, 0, alpha, 1]. It recognises seven well-known qualifiers (alpha, beta, milestone, rc/cr, snapshot, ga/final, sp) and “unknown qualifiers are considered after known qualifiers, with lexical order”. The javadoc’s own example is 1.0.RC2 < 1.0-RC3 < 1.0.1 — a dot and a hyphen ordering the same qualifier differently.

vers is the cross-ecosystem fix, and it is not published

There is an effort to fix this. The Vers Version Range Specification, out of the Package URL project, “defines a standard way to express version ranges for software packages” across ecosystems. An npm caret range becomes vers:npm/>=1.0.2|<2.0.0: the range is normalised, and the ecosystem is named in the URI because the comparison rules are the ecosystem’s, not the spec’s.

Its own page lists Current Version: In Development and Publication Date: TBD, developed by the PURL community and Ecma International under TC54 task group TG2 and, per Nesbitt’s survey, due to be proposed to Ecma later in 2026. Its sibling, Package URL, made it: the same survey dates ECMA-427’s first edition to 10 December 2025, with a second edition headed for the December 2026 General Assembly.

What to do if your tool spans ecosystems

Do not write your own comparator. In a dependency dashboard, an SBOM differ, a vulnerability matcher or an upgrade bot, the version-comparison layer is several incompatible algorithms wearing one function signature. Nesbitt points at the univers library, which reimplements these rules behind a single interface; reaching for it beats discovering Debian’s tilde rule in production.

Record which scheme a version came from. That is the real lesson of the vers URI design: a version string without its ecosystem is not comparable. If your schema stores versions as bare strings, the information you need is already gone.

Test the boundaries, not the happy path. Epoch resets, local version labels, tildes, carets, +incompatible, and Maven’s dot-versus-hyphen qualifier difference. If you are writing the parser by hand, our regex tester checks a version pattern against real-world strings before it ships.

Expect resolver behaviour to shift under you. Ordering rules are stable; resolvers are not. RubyGems swapped its resolver wholesale (the PubGrub swap) and pnpm 12 became the default install while most teams were not watching. Advisory data has the same problem from the other end: when npm retired its legacy audit endpoints, clients had to compute patched ranges themselves — version comparison by another name.

Evaluating tools in this space: the developer tools directory and open-source listings are the starting point, and how to compare developer tools is the rubric.

FAQ

Does SemVer define caret and tilde ranges?

No. Semantic Versioning 2.0.0 defines version syntax and precedence only. It contains no caret, tilde, wildcard or x-range operator; its only mention of ranges is a prose example. Range grammars are per-package-manager inventions.

What is the authoritative reference for npm version ranges?

node-semver, the library bundled with npm. npm's package.json documentation links caret and tilde to node-semver rather than semver.org, and says a version must be parseable by it. There is no specification document for the grammar.

How does Go's version comparison differ from SemVer?

SemVer says two versions differing only in build metadata have the same precedence. Go needs a total ordering, so it converts versions carrying build metadata into pseudo-versions, constructed to sort above their base version and below the next tagged version.

Where are RPM's tilde and caret ordering rules documented?

In the test suite. rpm.org's dependencies manual describes rpmvercmp() as a segmented strcmp and never mentions either character. Their behaviour is pinned in tests/rpmvercmp.at, an Autotest m4 file with 91 active assertions and no prose.

Is vers a published standard yet?

No. The Vers Version Range Specification lists its current version as In Development with a publication date of TBD, developed by the PURL community and Ecma International under TC54 task group TG2. Package URL itself did land as ECMA-427, first edition December 2025.

Can I compare two version strings without knowing the ecosystem?

Not reliably. Epochs, local labels, tilde and caret characters and qualifier handling all differ by scheme, so the same string can order differently in two ecosystems. Store the scheme alongside the version, which is why the vers URI names the ecosystem explicitly.


Everyone says they follow SemVer. What they follow is their package manager’s comparator, and no two of those agree.

Stay Updated

Get the next deep dive in your inbox

Subscribe for product analysis, engineering explainers, and practical guides published on TechLogHub.

See what launched this week

One email a week: new and trending developer tools, fresh comparisons, and what shipped. Unsubscribe in one click.