Publish an artifact that specifies the body of each request frame — field names, types, which are required — alongside the existing fixture, which specifies the envelope. A client author must be able to check their encoder against something other than a sample, so that a wrong field name fails their build instead of passing it.
This has already happened once, and it was invisible. The fixture is published as the conformance specification other languages build against, so a defect in it travels outward into every client written from it — and it travels while every test on both sides is green. That is a broken promise on a public surface, and the reason it is 1.0.0 work is that the fixture becomes the contract at the tag: after that, the wrong field name is not a bug, it is the protocol.
Measured 2026-09-20.fixtures/frames.jsonspecifies the envelope precisely — aheaderofu8 version | u8 type | u32be id | u32be length | payload[length],headerBytes: 10, the thirteen type numbers, and for each case apayloadHexand aframeHex. It does not specify the body at all: the body appears only as one samplejsonobject per case.
The invoke case carried{"op": "orders.list", …}while the server's struct field has always been taggedjson:"command". Both sides' byte-level tests stayed green, because both sides were decoding the same wrong file — encode and decode round-tripped perfectly against each other. A client written only from the fixture would have sentop, been ignored by the server, and passed its own conformance suite.
Both halves of the fix are in: the fixture now reads"command": "orders.list"(fixtures/frames.json:35), andinternal/server/fixture_decode_test.go(112 lines) now decodes every request fixture into the struct that actually serves it, withDisallowUnknownFieldsat:92and a table that fails by name when a request frame has no decoder. That guard is server-side. Nothing equivalent exists for a client in another language, which is the half this ticket is.
Coverage is also thinner than it looks: the fixture holds 10 cases over 8 distinct frame types. Types 1 (hello), 2 (welcome), 4 (pong), 8 (subscribe) and 10 (goodbye) have no case at all.
Measured 2026-09-20, separately from the body defect above. An operation's arguments
are fully described by its declaration — Input gives each one a name, a type and whether it
is required, and a generator can always emit a typed argument list from it. Its returned
rows are a different story: they are describable only if the operation declares a
projection. packages/ecosy-sapedb's generator
(src/types/index.ts) is honest about this rather than guessing — verified in the generated
output itself, tests/typecheck/ledger.d.ts: every operation without a projection emits
row: Record<string, unknown>, with the comment
"The schema does not describe documents, so the shape of this row is genuinely not known here."
right next to it.
The project's own worked fixture teaches the untyped path exclusively.
fixtures/ledger.schema.json declares five operations —
orders.place, orders.pay, orders.get,
payments.of_order, entries.of_account — and a count of
"projection" across the file returns zero. projection is a
real, implemented field (it appears in src/types/index.ts,
src/commander/index.ts and src/client/index.ts); nothing about the fixture is
missing a feature, it simply never uses the one field that would make a read's row typed. This is
ISS-19.
The rule this ticket's artifact has to publish, in one sentence: an operation does not carry a
shape, but it decides which shape a client can derive — the argument shape unconditionally, the
row shape only when the operation declares a projection. Without that sentence on the
page, a bundle author reads the worked example, copies its silence on projection, and
publishes an external operation whose surface is callable but not bindable — exactly the gap
SAPE-12's worked module has to avoid.
op/command defect is a regression test: a body using the old field name
is rejected when validated against the artifact. Run as a case, so the specific mistake cannot
return.fixture_decode_test.go already asserts its table: a frame type with no entry fails by
name, rather than being quietly absent.projection to fixtures/ledger.schema.json's operations. That is the
fixture's own fix, tracked separately as ISS-19, deferred because it changes
an example rather than a promise.