What a MeshBench version number promises, what it does not, and how you find out when something has moved under you.
MeshBench is 0.x, deliberately. There is no 1.0 scheduled, and there will not be one until the list at the end of this page is worked through. The decision is the owner's and it is the honest one: 1.0 is a claim about stability, and this project is still learning what it is.
The usual reading of a leading zero is that anything may break at any time. That is a licence, and taken literally it would make this page unnecessary. It also would not be true: three client libraries are published to two package indexes, a control socket is documented verb by verb, and files people save are opened again months later. Somebody is depending on all of it, and saying "0.x, so caveat emptor" while quietly expecting them not to is dishonest in the other direction.
So the promise is narrower than 1.0 and wider than nothing:
Three things are checked at runtime, and a mismatch is refused rather than guessed at. The control protocol number, the client and workbench pairing, and the fixture format. Each is described below. What they have in common is that the failure they exist to prevent is not a crash: it is a run that completes and answers a question about something other than what you asked. This simulator's worst failure mode is a plausible wrong number, and every one of these three rules is bought to avoid one.
Everything else may change between any two releases, including the verb set,
the shape of a result, the command-line flags, the panels, the file layout of an
installation, and the numbers a study produces when the model improves. The
CHANGELOG.md says what changed. It is the only notice you get, and while the
major version is 0 there is no deprecation period behind it.
A release is a plain X.Y.Z, spelled that way everywhere. The git tag
carries a leading v, the linker stamp carries the tag, and PyPI and npm carry
the number without it. Nothing else counts as a release: a build from a working
copy stamps nothing at all, on purpose, because "not a release" is a thing the
pairing rule below needs to be able to tell.
control.Protocol is the wire version, and it is about frames, not
features. It moves when a client written against the previous number would
misread this one: a verb changing what it means, a field changing type, the
framing changing. It does not move when a verb is added, or when a result grows
a field, because a client reads the fields it knows and ignores the rest.
It has been 1 since the socket existed, and has never moved.
A client declares the number it speaks on the frame it was already sending: the token line on loopback TCP, the first request on a unix socket. There is no handshake of its own, because adding one would have broken every script written against this socket before it existed, which is the wire the check exists to protect.
A number this build does not speak is refused before any verb runs, with an error carrying both numbers and which end to change. The rule is an exact match in both directions, not "a newer workbench serves an older client": the number moves only when something an older client relied on has changed, so any difference at all is that break by construction.
A client that declares nothing is served. Every script written against this
socket before the field existed is one of those, and it finds out from
session.hello, which is what the shipped clients have always read.
The refusal carries the code protocol_mismatch. docs/scripting-api.md has
the wire detail and what each client raises.
The protocol number answers whether two ends can understand each other's frames. It cannot answer the question a script actually has, which is whether the client in the virtualenv is the one that came with the workbench on the PATH. Two releases apart with no protocol bump between them will connect happily and then disagree about a verb's parameters, and that failure surfaces forty calls later looking like the simulation misbehaving.
So the release travels on the wire beside the protocol number, and a released
client driving a workbench from a different release is refused at connect,
with both release numbers and the instruction to make them the same one. The
code is version_mismatch.
Refused in the workbench rather than in the clients, because a third-party script speaking the raw socket is entitled to the same answer as one using a client we ship.
A pair where either end is not a release is allowed through, and this is
what keeps the tree usable by the people working on it. A build from a working
copy has no release stamped in it, and neither has a client run out of the same
checkout, so a rule of "they must be equal" would refuse every pair a developer
has, every run, for a disagreement that does not exist. Where one end is
unstamped there is no second version to disagree with. The skipped check is
logged rather than silent: MESHBENCH_LOG=control says which end was not a
release.
A development build has development clients. A build from the development
channel is 0.0.11-dev.1, and its clients are published beside it as
pre-releases, which the package indexes hand out only when asked for: PyPI
spells it 0.0.11.dev1 and pip needs the exact pin or --pre; npm keeps the
tag's spelling under the dev dist-tag, so a plain npm install keeps
resolving to the stable release; Go takes the tag as it is.
pip install meshbench==0.0.11.dev1
npm install @meshbench/client@0.0.11-dev.1
go get github.com/MeshBench/meshbench/pkg/client-go/meshbench@v0.0.11-dev.1
The Python client says the tag's spelling on the wire, so the workbench
compares 0.0.11-dev.1 with 0.0.11-dev.1 and not with PyPI's rendering of
it.
The practical consequence for a script: pin the client to the workbench.
pip install meshbench==0.1.0 beside MeshBench 0.1.0, and the same for
@meshbench/client. Upgrading one without the other is caught at connect rather
than in the middle of a run, which is the whole point.
A fixture is the input to a simulation: the nodes, the boundary, the traffic and
the assertions. It carries a format number, and this build reads up to
fixture.Format.
Older is read. Newer is refused. A file written by a later MeshBench is refused by name, with both format numbers and the instruction to install the release that wrote it. Nothing migrates it and nothing reads the parts it recognises.
Refusing is the right answer while the format is still moving. Reading three quarters of a file does not fail: it produces a complete, plausible result about a network nobody described, and there is no point in the run at which anybody would be told. Refusing costs one upgrade. Migration machinery would be the other answer, and it is not built, because building it now would be guessing at migrations for changes nobody has designed.
A file written before the number existed reads as format 0 and is still read. The shape did not change on the day it started being declared, and backward compatibility is the half of this rule that has to keep working: the fixtures this release ships are the example everybody opens first.
format moves on the same rule the protocol number does: when a file this build
writes would be misread by the build before it. A new field that an older
build ignores costs it nothing and does not move the number.
The version a build reports is a linker stamp, and the linker stamp only ever reaches a binary the release pipeline builds. Three platforms are built by three different jobs, so the only thing keeping the three in agreement is that somebody remembered, and once they disagree the difference is invisible until a release is already out: each build works perfectly and says a different thing about itself. That has happened once, and the two platforms that disagreed with the tag were the two that moved.
All three now stamp internal/app/version.Version=v$VERSION, where $VERSION
is the tag without its v, or the dispatched version, or
0.0.0-dev-<short sha> for a build from something that is not a tag. So a
release binary reports its tag, v0.1.0, and a build from anything else reports
a version that is visibly not a release and is treated as one by the pairing
rule above.
internal/app/version's own test reads the three build commands out of
.github/workflows/package.yml and packaging/macos-app.sh and fails if any of
them builds cmd/meshbench without that stamp, or drops the v, or if a fourth
build path appears without one. All three also refuse a version that is not a
plain X.Y.Z, because a workbench stamped vv0.1.0 is not a release as far as
the pairing rule can tell, and would pair silently with every client of every
version.
Not aspirations. Each of these is something a person could check off, and each is a reason a 1.0 today would be a claim the project cannot support.
- A predicted margin has been compared with a real reception. The validation harness exists, counts its exclusions and refuses to treat a silent receiver as a negative observation. What it has never had is data. Run it against a real CoreScope or Beacon export and publish the bias and the spread. Until then every propagation number is correct according to the textbook, which is not the same as true on the hill.
- The board in the reader's hand works. Ten boards are described and, as last measured, one passes every capability the board check asks of it. Three nRF52 boards advert once and go quiet, two ESP32 boards assert inside ESP-IDF startup, and two have never been attempted. A 1.0 whose odds against an arbitrary board are seven in ten is a version number doing marketing.
- A downloaded build runs without the operating system objecting. macOS is ad-hoc signed and not notarised; Windows is unsigned and SmartScreen warns. Both need a signing identity, which needs an account, which is the actual task.
- Every platform can fetch what it needs. Windows still cannot fetch an emulator, because the resource downloader reads ELF and Mach-O headers and not PE.
- A release has been rebuilt from its own source archive, by somebody who did not build it, on a machine that has never seen this repository. The archive is published with every release and the obligation is met on paper; nobody has demonstrated it.
- The verb surface has gone two consecutive releases without a breaking
change.
docs/verb-reference.mdis generated, so the diff between two releases is the evidence: additions are fine, and a verb changing what it means or what it takes is not. A wire that has never been stable for two releases cannot be promised to be stable for a major version. - The fixture format has a way forward, or a stated reason it will not need one. Refusing a later file is the right answer while the format is moving. At 1.0 it stops being an answer, because the whole promise of a major version is that the file you saved keeps opening.
When those are done, 1.0 means: the control protocol and the fixture format do not break within the major version, the clients and the workbench keep their pairing rule, and anything that would break either gets a 2.0. Not before.
MeshBench is GPL-3.0-or-later and the repository is still private, so every
release carries a meshbench-source.tar.gz beside the binaries. That
is section 6's obligation met directly rather than by an offer.
An archive is only worth what a recipient can build from it, and this tree
carries a replace directive pointing at MeshBench/gio, our Gio fork with
Wayland layer-shell windows. That fork is public. A rebuilder can fetch it
with no credentials, no GOPRIVATE and no insteadOf, which is what makes the
archive a real remedy rather than a gesture. Every other fork this project
carries is public too, and docs/repositories.md states each one's status in a
table for exactly this reason.
MeshCore itself is not linked into the binary. It is built separately in
MeshBench/meshcore-native and downloaded at runtime, which is what freed the
licence choice; do not reintroduce a direct dependency on it without an ADR.
The Nordic SoftDevice is not ours and cannot be redistributed. Anyone running published nRF52 firmware supplies their own copy.