diff --git a/02-DECISIONS/0074-the-wire-is-specified-not-the-types.md b/02-DECISIONS/0074-the-wire-is-specified-not-the-types.md new file mode 100644 index 0000000..21a9f6f --- /dev/null +++ b/02-DECISIONS/0074-the-wire-is-specified-not-the-types.md @@ -0,0 +1,117 @@ +--- +topic: the tiers +status: proposed +date: 2026-09-15 +deciders: jochen +reconstructed: false +extends: 0039-what-the-sdk-holds-and-refuses.md +--- + +# 74. The wire is specified; an SDK is whatever passes the conformance suite + +## Context + +[ADR 0039](0039-what-the-sdk-holds-and-refuses.md) says what the SDK holds: the tool-serving +harness, the messaging and event framework, the contracts, and core primitives. It settles what +belongs in *an* SDK. It does not say what happens when there is more than one. + +There is already more than one. **The contracts are expressed twice** — as Go types in the control +plane and the host, and as TypeScript types in the SDK — and nobody has felt it because both live +in one repository and one head. + +**They already disagree.** Not in some future where a second language is added; today: + +| | TypeScript | Go | +|---|---|---| +| the provision's field | `resource` | `Provision` | +| what `consumer` means | **the module** | **the node**; the module is `From` | +| event headers | six, including `x-causation-id` and `x-schema` | four — the other two are never written | + +So one word means two things in the two halves of one mesh, and the header that exists so a body's +shape can change without silent misreads is declared on one side and emitted by neither. + +A failure of this kind does not announce itself. Two implementations that disagree about an +envelope do not fail to compile — they ignore each other's messages, and a mesh where a module +stops reacting looks exactly like a mesh where nothing happened. + +## The question this settles + +A module may be written in any language the mesh can build +([`18-building-a-module`](../03-DESIGN/01-to-be/18-building-a-module.md)). Every language needs an +SDK. What is an SDK *of*? + +Two answers were available, and the obvious one is wrong. + +**Shared types, generated.** Write the shapes once — a schema, an IDL — and generate Go, TypeScript, +Rust. It is the familiar answer and it solves the smaller half of the problem. The shapes are not +where the difficulty is. + +**A specified wire, with a conformance suite.** The shapes are a consequence; what an SDK must get +right is *behaviour*. + +## Decision + +**The wire is specified, and an SDK is any implementation that passes the conformance suite.** + +What the specification covers is what two implementations can disagree about: + +- **the exchanges and queues** — which exchanges exist, that a consumer's queue is durable and + named `..events`, that a tool is served from a shared durable `serve.` +- **the envelope** — every header, which are required, what an unknown `x-` header means, and that + ignoring one is correct rather than lax +- **identity** — that a module's node and module name come from its sealed credential and not from + its environment, so what it emits matches what the mesh authorised +- **delivery** — at-least-once, and that dedup is on `x-event-id`, which only the emitter can make +- **the credential** — the sealed document's fields, and that a connection pins a certificate + fingerprint rather than trusting an authority +- **provisioning** — a grant in, a credential out, and what each carries +- **the vocabulary** — that `consumer` is one thing, named once + +**And the suite is executable, not prose.** A specification nobody can run is a document two +implementations drift from while both believe they conform. Conformance is a set of fixtures — an +emitted event, a served tool call, a grant and its answer — that every SDK must produce and consume +byte-for-byte. + +**The existing two implementations are the first two to be made to pass it.** Not a future language: +the drift above is present, and a suite that only new SDKs must satisfy would leave the disagreement +that already exists in place while certifying everything added afterwards against it. + +## Why not generated types + +Generation makes the shapes agree and leaves everything that matters unspecified. Two SDKs +generated from one schema can still name their queues differently, take identity from the +environment, dedup on the wrong field, or omit a header the other requires — and every one of those +is a mesh that runs and quietly does not work. + +It also makes the contract into whatever the generator supports, which is a decision nobody made +about a boundary everything else depends on. + +**The shapes are worth generating once the wire is specified.** That is a convenience, and it comes +second. + +## Consequences + +**A language is a commitment, and now a measurable one.** Adding one means implementing the wire +and passing the suite. That is more work than transliterating types, and it is the work that was +always there — the difference is that it can now be finished rather than believed. + +**Versioning becomes possible.** `x-schema` exists for it and is never written. A specified envelope +with a version on the body is what lets a mesh hold a module built against an older SDK, which is +the ordinary state of any mesh that has been running for a while. + +**The two current implementations will be found wrong.** They disagree, so at least one is. Fixing +that is the point rather than a cost, but it is not free: something is emitting or expecting +something it should not. + +**This does not make the mesh polyglot by itself**, and should not be reported as though it does. It +makes polyglot possible to do correctly. A Rust SDK is still a Rust SDK. + +## How this is checked + +| Rule | Checked by | +|---|---| +| One vocabulary | A word means one thing across implementations, checked by the fixtures using it. | +| The wire is what is specified | Both existing SDKs run the conformance suite in their own test suites, and a change to one that breaks a fixture fails there rather than in a mesh. | +| A new SDK is a passing SDK | A language is not listed as buildable until its SDK passes; the toolchain list and the conformance results name the same set. | +| An unknown header is ignored | A fixture carries one, and every implementation accepts it. | +| Identity comes from the credential | A fixture sets an environment that disagrees with the credential, and the emitted event carries the credential's. | diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 31bc462..ac152e9 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -106,6 +106,7 @@ python3 00-META/checks/index.py fail if stale - **0071** — [Genesis clones from a mesh, and checks what it got](0071-where-genesis-gets-its-source.md) - **0072** — [Two graphs, and a build chain that orders itself](0072-two-graphs-and-the-build-chain.md) - **0073** — [The installer carries a builder, and the registry stays where it is](0073-the-installer-carries-a-builder.md) +- **0074** — [The wire is specified; an SDK is whatever passes the conformance suite](0074-the-wire-is-specified-not-the-types.md) *(proposed)* ### What runs on them, and how it gets there