From debd703d4eeffb4ddcbfc4701bfd92e2ea3a57e5 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 23:33:34 +0200 Subject: [PATCH 1/2] The sdk names no transport Task 3.7 of novox/hq ADR 0116, and the whole of the sdk's diff for the bus change. Three comments said AMQP where they meant 'message headers' and 'a broker client'; the code never spoke it, which is why no module is rebuilt for any of this (ADR 0039). --- src/contracts/index.ts | 2 +- src/events/index.ts | 2 +- src/messaging/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contracts/index.ts b/src/contracts/index.ts index d57ae11..e52264e 100644 --- a/src/contracts/index.ts +++ b/src/contracts/index.ts @@ -19,7 +19,7 @@ export interface ToolDefinition { } /** - * The metadata that rides an event as AMQP headers (novox/hq ADR 0042). An event's identity and + * The metadata that rides an event as message headers (novox/hq ADR 0042). An event's identity and * provenance live here, not in the body, so a consumer — or the broker, or an audit tool — reads * who/when/what without parsing the payload. An unknown `x-` header is ignored, not refused: an * event is observed by parties that need not all understand every header. diff --git a/src/events/index.ts b/src/events/index.ts index b99aa26..42e902a 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -4,7 +4,7 @@ // credential — just the broker's topic routing). Declared as emits/consumes on the manifest so the // mesh knows the event graph. // -// Identity and provenance ride as AMQP headers, not in the body (novox/hq ADR 0042): a consumer — +// Identity and provenance ride as message headers, not in the body (novox/hq ADR 0042): a consumer — // or the broker, or an audit tool — reads who/when/what without parsing the payload, and the body // is only the domain payload. This surface hides the header/exchange/queue mechanics; a module // names a type and a body and never sees the wire. diff --git a/src/messaging/index.ts b/src/messaging/index.ts index ce71883..d717458 100644 --- a/src/messaging/index.ts +++ b/src/messaging/index.ts @@ -25,7 +25,7 @@ export interface Broker { /** * How a module obtains its broker. The hosting runtime sets this once; module code calls broker() * without knowing the concrete binding. Keeping the binding out of the sdk is deliberate — the sdk - * carries the contract, not a specific AMQP client build. + * carries the contract, not a specific broker client build. */ let binding: (() => Broker) | undefined; -- 2.54.0 From 8f5b786a6b101c7c31f2ff582310f4bf226c4c3c Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 23:40:59 +0200 Subject: [PATCH 2/2] Conformance fixtures: what two implementations may not disagree about MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- conformance/README.md | 32 ++++++++++++++++++++ conformance/events/module-event.json | 44 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 conformance/README.md create mode 100644 conformance/events/module-event.json diff --git a/conformance/README.md b/conformance/README.md new file mode 100644 index 0000000..5b32dd6 --- /dev/null +++ b/conformance/README.md @@ -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 diff --git a/conformance/events/module-event.json b/conformance/events/module-event.json new file mode 100644 index 0000000..8de1169 --- /dev/null +++ b/conformance/events/module-event.json @@ -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" + } + ] +} -- 2.54.0