feat(semver): a strict Semantic Versioning 2.0.0 parser, replacing x/mod's shorthand #17

Merged
mikluko merged 2 commits from mikluko/octant/1723-semver-parser into mikluko/octant/1721-unreadable-version 2026-09-06 14:14:24 +00:00
mikluko commented 2026-09-06 11:28:09 +00:00 (Migrated from github.com)

Octant: T1723, on map D1726.

golang.org/x/mod/semver is not a SemVer implementation and says so in its own
package doc: it requires a leading v, and it recognises vMAJOR and
vMAJOR.MINOR as alternatives to the three-component form. parseHeading gated
on semver.IsValid(v) && semver.Canonical(v) == v, where the second comparison
existed solely to undo the shorthand the library documents as a feature, and
carried a second policy nobody chose along with it.

What changes for a user

Two headings change verdict:

heading before after
[v1.2.3] a version no version — the leading v is x/mod's, not the specification's
[1.2.3+build.1] no version a version — build metadata is valid SemVer

And a rejection now names the rule it hit. One message covered every rejection,
so the author of ## [1.3.0+build.1] was told that a version states all three of
major, minor and patch, which it does.

The corpus

The specification ships no reference corpus in any language — semver.org lists
no implementations and no test suite. It does ship a suggested regular
expression, which sits entirely inside RE2, so Go compiles it as written.

It is the differential oracle in the tests and not the implementation: it
returns a boolean, and the whole reason for a parser is that a rejection says
which rule broke. 54 valid forms grouped by production and 42 invalid forms each
with the rule it breaks are asserted against both, and for the valid ones the
parse is checked against the oracle's own capture groups.

Three forms are pinned as departures: the specification sets no upper bound
on a numeric component and this package reads them into uint64.

What this does not do

Comparison still runs on x/mod. Whether the in-house parser replaces it
there is the third of the three open decisions on D1726 and is where sections 10
and 11 live — build metadata ignored for precedence, and ASCII ordering of
alphanumeric identifiers with numeric ones ranked below them, compared
numerically with no upper bound. Draft until that is settled.

Base

Branched from mikluko/octant/1721-unreadable-version (#14) rather than main,
because #14 touches changelog.go, check.go and lint.go — the same gate this
rewrites. It does not presume the answer to whether #16's ceremony grows to carry
all of 1.2.0.

Octant: [T1723](http://127.0.0.1:9876/T1723), on map [D1726](http://127.0.0.1:9876/D1726). `golang.org/x/mod/semver` is not a SemVer implementation and says so in its own package doc: it requires a leading `v`, and it recognises `vMAJOR` and `vMAJOR.MINOR` as alternatives to the three-component form. `parseHeading` gated on `semver.IsValid(v) && semver.Canonical(v) == v`, where the second comparison existed solely to undo the shorthand the library documents as a feature, and carried a second policy nobody chose along with it. ## What changes for a user Two headings change verdict: | heading | before | after | |---|---|---| | `[v1.2.3]` | a version | no version — the leading `v` is `x/mod`'s, not the specification's | | `[1.2.3+build.1]` | no version | a version — build metadata is valid SemVer | And a rejection now names the rule it hit. One message covered every rejection, so the author of `## [1.3.0+build.1]` was told that a version states all three of major, minor and patch, which it does. ## The corpus The specification ships no reference corpus in any language — `semver.org` lists no implementations and no test suite. It does ship a suggested regular expression, which sits entirely inside RE2, so Go compiles it as written. It is the **differential oracle in the tests and not the implementation**: it returns a boolean, and the whole reason for a parser is that a rejection says which rule broke. 54 valid forms grouped by production and 42 invalid forms each with the rule it breaks are asserted against both, and for the valid ones the parse is checked against the oracle's own capture groups. Three forms are pinned as **departures**: the specification sets no upper bound on a numeric component and this package reads them into `uint64`. ## What this does not do **Comparison still runs on `x/mod`.** Whether the in-house parser replaces it there is the third of the three open decisions on D1726 and is where sections 10 and 11 live — build metadata ignored for precedence, and ASCII ordering of alphanumeric identifiers with numeric ones ranked below them, compared numerically with no upper bound. Draft until that is settled. ## Base Branched from `mikluko/octant/1721-unreadable-version` (#14) rather than `main`, because #14 touches `changelog.go`, `check.go` and `lint.go` — the same gate this rewrites. It does not presume the answer to whether #16's ceremony grows to carry all of 1.2.0.
Sign in to join this conversation.
No description provided.