docs: record the compatibility-window work in build-status

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 11:43:02 +02:00
co-authored by Claude Fable 5
parent 33a6acb0bf
commit 8166611af1
+39
View File
@@ -621,3 +621,42 @@ finding code and the metrics survive, then deleted it.
- Accounts/OIDC on the server, which is what `uploads=account` is waiting for.
- Nothing in this entry has been exercised on a phone yet — all of it was verified from the PC
against the live server. On-device verification is the next step.
### SemVer compatibility windows between app and server (server-v0.5.0 … v0.5.2, 2026-08-01)
Both artifacts are SemVer, and each now declares — and enforces — which peer versions it will talk
to. Spec: `docs/probe-protocol.md` §8.
**Two axes, deliberately not conflated.** Release versions are a *proxy* for what actually has to
match, so the real thing is checked first:
- `protocol_version` — **can** these builds talk. Advertised in the profile; a peer in a different
breaking series is refused whatever its release version says. Below 1.0.0 the **minor** is the
breaking axis (SemVer §4).
- release-version window — **may** they, per policy. `[min, max)`, min inclusive, max exclusive,
because the useful bound is always "the version that broke it".
Bounds sit at breaking boundaries, not at releases, so shipping a patch never requires editing a
range. The app requires server `>= 0.4.2` for a stated reason, not caution: earlier multi-homed
servers mis-addressed granted sends and the client measured 100 % downstream loss that never
happened. Operators override the server side with `ECHOLOT_MIN_APP_VERSION` /
`ECHOLOT_MAX_APP_VERSION`; a malformed bound is fatal at startup rather than ignored, so a typo
cannot silently disable a restriction.
Three rules that shaped the implementation:
1. **`GET /v1/profile` is never gated.** It is where a refused client learns which version it needs;
gating it leaves the user with a network error instead of an answer.
2. **An unparseable or absent version is `unknown`, and is allowed.** Dev builds report `dev`, and a
client too old to send the header cannot be identified anyway.
3. **Refusal is 426 with a body naming both versions and the window**, surfaced client-side as a
distinct `VersionRefused` rather than folded into "network error".
The app's `versionCode` is now derived from its SemVer (`major*1e6 + minor*1e4 + patch*10`) instead
of being a second number to remember.
Verified live against fmr (`LiveCompatTest`): profile advertises the window and stays readable for a
refused version; 0.1.0 and 99.0.0 are both refused with actionable messages; 0.2.0 and a missing
header are both served.
One user-visible bug caught in the process: Go's JSON encoder HTML-escapes `<`, `>` and `&` by
default, so the refusal reached the client as `needs \u003e= 0.2.0`. Disabled at the encoder (this
is an API, not a page), and the client now *parses* the error field instead of pattern-matching it,
so it survives whatever a future encoder decides to escape.