Conformance fixtures: what two implementations may not disagree about

Tasks 3.1 and 3.3 of novox/hq ADR 0116. One fixture directory, read by
every implementation's own runner rather than copied into each — a fixture
copied twice is two fixtures, and two fixtures drift.

The README settles what ADR 0074's byte-for-byte can and cannot mean. The
envelope is exact: the subject, the required headers, each name and format,
because those are what two implementations get wrong invisibly. The body is
not: it is the module's payload, and Go sorts a map's keys where JavaScript
keeps insertion order, so demanding identical bytes would commit every
implementation to a canonical JSON encoder to buy a property the mesh never
uses. Said plainly, because read strictly it would have sent somebody
writing one.
This commit is contained in:
2026-09-26 23:40:59 +02:00
parent debd703d4e
commit 8f5b786a6b
2 changed files with 76 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
# Conformance fixtures
What two implementations of the mesh's module protocol may not disagree about
([novox/hq ADR 0074](https://git.novox.be), design 19). A fixture is data, not prose: every
implementation produces it and consumes it, and an implementation that passes the fixtures for a
capability **claims** that capability.
## What "byte-for-byte" means, and what it cannot
ADR 0074 says the fixtures must match byte for byte. Taken literally that is more than the mesh
needs and more than two languages can give:
- **The envelope is exact.** The subject, the set of required headers, each header's name, and
the format of each value. This is what an implementation can get wrong in a way that makes two
meshes silently ignore each other, so this is what is pinned.
- **The body is not.** It is the module's own payload, opaque to the mesh, and its serialisation
is the language's. Go's `encoding/json` sorts a map's keys; JavaScript's `JSON.stringify` keeps
insertion order. Demanding identical bytes there would mean committing every implementation to
a canonical JSON encoder — a large promise, bought for a property the mesh never uses. The
fixture pins that the body **round-trips**: what goes in comes out equal.
So: exact where disagreement is invisible, semantic where it is not. Stated here because
"byte-for-byte" read strictly would have sent somebody implementing a canonical encoder.
## Running them
The TypeScript implementation: `node conformance/run.mjs` (against a NATS server; see the file).
The Go implementations read this directory by sibling path, the way the lab finds its siblings.
## Layout
events/ one emitted event, and what it must look like on the wire
+44
View File
@@ -0,0 +1,44 @@
{
"capability": "events",
"name": "a module emits an event",
"why": "The envelope is what two implementations can disagree about without either failing: a missing header, a header spelled differently, or a body nested where metadata belongs. None of those stop a mesh running; they stop it reacting.",
"given": {
"module": "shop",
"node": "one",
"key": "order.placed",
"body": { "id": "a1", "total": 12 },
"headers": {
"x-event-id": "0123456789abcdef0123456789abcdef",
"x-source": "shop",
"x-node": "one",
"x-time": "2026-09-26T12:00:00Z",
"content-type": "application/json"
}
},
"wire": {
"subject": "mesh.mod.shop.event.order.placed",
"requiredHeaders": ["x-event-id", "x-source", "x-node", "x-time", "content-type"],
"optionalHeaders": ["x-causation-id", "x-schema"],
"headerFormats": {
"x-time": "RFC3339",
"content-type": "application/json",
"x-event-id": "^[0-9a-f]{32}$"
},
"payloadIs": "the body alone, not the envelope",
"keyRecoveredFrom": "the subject, after the .event. token"
},
"refuses": [
{
"what": "an envelope nested in the payload",
"why": "an implementation that publishes the whole envelope as the body passes all of its own tests and is unreadable to every other"
},
{
"what": "a missing x-event-id",
"why": "delivery is at-least-once and only the emitter can say which of two messages is a redelivery"
},
{
"what": "an x-source that differs from the subject's module",
"why": "the bus enforces the namespace, so a disagreement means the envelope is lying about its origin"
}
]
}