Skip to main content

0019. A version number promises fidelity, not the current result

Status: Accepted · 2026-08-07 · #115 Governed by: 0000

Over the 60 lines CONTRIBUTING-docs asks for. The TL;DR and the two tables are the rule and are enough to judge a release; the rest is what the rule was tested against, kept because both ordering mistakes the tables have had were found by re-deriving a real commit and not by reading them.

TL;DR A correction that moves the mock toward measured real behaviour is a minor release, even when it turns a passing test red. What is major is a change to something this project stated on its own authority: a documented divergence, an incompatible type, or the range of Node it runs on.

Decision

The public surface here is the signatures and the fidelity to socket.io, and the two can disagree. A broadcast that reached the wrong set of sockets is a defect against socket.io and a working behaviour to whoever's test recorded the wrong set.

The version number promises the fidelity. A result that diverged from measured real behaviour was never the promise, so restoring it does not withdraw one. Reading it the other way freezes every measured defect until the next major, which is not a promise a faithful mock can make. That is 0000 applied to versioning.

What is major is what this project stated on its own authority: a divergence it chose, a type it published, the range of Node it declared. Those are ours to promise and ours to break.

The first row that matches wins, so the three the paragraph above named go on top. A withdrawal is nearly always a move toward real socket.io as well, and a fidelity row would catch it on the way past.

Declared on our own authority

ChangeBump
Removing or altering a differences.md §A entrymajor
Raising engines.node, lowering itmajor, minor
A public type change that still compiles at existing call sites, elseminor, major

Fidelity, and everything else

ChangeBump
A correction toward measured real behaviour with an observable changeminor
A correction toward measured real behaviour with no observable changepatch
Newly covered socket.io surfaceminor
Adding a differences.md §A entrynone
Changing something the documentation marks unspecifiedpatch
A change to a smocket-only API (§B)ordinary semver

The first fidelity row does not ride in a patch. The old result was not a promise, but a green suite can turn red, and a patch is understood as safe to take without reading anything. It says observable rather than delivered because the two are not the same set: the return value of emit changes nothing about who receives an event, and a caller still reads it. Anything a user's code can see belongs on that row, whether or not it moved a packet.

Before 1.0.0 the rules apply one place to the right, as npm reads a 0.x range: major becomes minor, minor and patch become patch. They apply from now rather than from the tag, because the first conformance fix after 1.0 is a poor place to learn a rule decides nothing.

The shift costs a signal. Two rows that differ above 1.0.0 arrive at the same number below it, so a 0.x patch can be a delivery correction or a typo in a comment and the number does not say which. The release notes carry that instead: every release below 1.0.0 names the row it landed on, so a reader who needs to know reads one line rather than guessing from the digit that moved. That is one line and it is owed by every 0.x release, including the ones that changed nothing a user can see. It adds to the Announcement rules below rather than standing in for them: a delivery correction still owes the full treatment there, and while 0.x lasts it owes the row name on top.

Announcement

The version number says how much care an upgrade needs, not what moved. A correction that changes what is delivered also carries, in the release notes, its own section rather than a line among the rest; the before and after as results (io.to('room').emit('x') reached A and B, and now reaches A, B, and C); and a link to the dual-run test that pins the new behaviour. A reader should be able to tell in one screen whether their suite is affected.

Row order, tested against the history

The three fix: commits in the history land in three different rows. Two are delivery corrections and minor: 088bba7 (#80) buffered emitWithAck instead of reaching a dead socket, and 9b0ae90 kept listeners in arrays so a duplicate registration fires twice.

a7be4fb is the one that tests the rules rather than confirming them. It dropped node:crypto from newId; nothing it delivers changed and the id keeps the shape 0011 fixed, so by output alone it is a patch. But moving the entropy source to globalThis.crypto raised the lowest Node the package runs on, and that is the whole reason the declared table is read first. Reached in the other order it takes the no-observable-change row and answers patch, which is the answer the rest of this document argues against. The judgement is retroactive. engines was declared later, so at the time the effect was real and unstated.

The same trap sat under the §A row until the tables were split. Removing a §A entry is smocket giving up a divergence on purpose, which is a correction and an observable one, so fidelity rows on top answered minor for the row that says major. #189 surfaced it by closing a gap rather than a §A entry, which raised the question of which row each takes.

Alternatives rejected

  • A conformance correction is major. Corrections are the expected shape of work here, so this makes the major number a count of defects found and puts every fix behind a wait, which is the freeze the fidelity promise cannot survive.
  • A conformance correction is patch. Right about the promise, wrong about the effect. A patch is taken without reading anything; a delivery change should be read.
  • A pinned version range recommended in the README. It moves to the reader a cost the announcement rule already covers, on a screen meant for someone still deciding to try it.