feat(frame-wire): amend the ratified wire contract to 1.3.0 #7
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/frame-wire-1-3-0"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Forge wish: 82197cdc (slice T1a of anchor wish 4160ffa9). Blocks T1b and S9.
Companion: Toilville/ritual-engine PR on the same branch name — these land together.
Amends the ratified wire contract 1.2.0 → 1.3.0. Five amendments, all found by
diffing the two implementations against each other and by designing a real
TypeScript consumer.
Scope
1 ·
valueis typed explicitly.FieldSetDelta.value,ElementAppendDelta.valueandToolCall.resulthad notypekeyword at all— only a description. Under
json-schema-to-typescriptwithadditionalProperties: falsethat renders as an object map, sofield_set("/progress_pct", 42)and anynullfailed typecheck, though thecontract's own motivating examples for the op are scalars. Now a
$defs/JsonValueoneOfover the six JSON types, referenced from all three sites.Permissiveness is unchanged — verified, not asserted: a Draft 2020-12
validator was run over
null,true,false,0,42,-1,3.5,"","str",[],[1,{"a":1}],{}and{"a":{"b":[null]}}; every one valid. Thebranches are mutually disjoint, so
oneOf's exactly-one rule admits everything amissing
typeadmitted.2 · A failed turn can say why.
FrameTurnisadditionalProperties: falseand had no member for an error, so both folds dropped
ErrorEvent'smessageand
codeand a client could not carry them even if it wanted to. Addsoptional
error_messageanderror_code.3 · The fold returns the partial turn on abort. It previously returned an
error alone, losing the turn the user was mid-render on. The discard/abort
classification is unchanged — this is about what the caller receives, not
which codes abort.
4 · The unspecified is specified. A second
block_startfor an open index isidempotent last-write-wins (both implementations already did this; neither
document said so). RFC 6901 leading zeros in array indices are rejected — the
grammar is
0 / (%x31-39 *DIGIT)and both implementations previously acceptedthem, agreeing with each other and disagreeing with the RFC.
BlockPointergainsa receiver rule against
__proto__,constructorandprototype: "everycontainer on the path MUST already exist" is satisfied by an inherited member in
JavaScript, and a TypeScript client is queued behind this contract. The
block_id-mismatch silent discard is recorded as deliberate so it stops beingrediscovered as a bug.
5 · The
EventSourcejustification is corrected. It claimed data-only frameskeep browser clients on
EventSource's defaultonmessage— but this endpoint isa POST with an auth header and
EventSourceis GET-only and cannot set headers,so no browser client can use it regardless. The format choice stands; only the
reason was wrong.
openapi.yamlmirrors every schema change;schemas/index.jsonregistersframe_wire.Testing
This repo runs no tests today (only
pr-policy-check.yml), so verification wasdone by hand and in the companion PR:
frame_wire.jsonandopenapi.yamlbothparse (JSON and YAML, OpenAPI 3.1.0, 113 schemas) and were diffed property by
property across all nine amended definitions. The only differences are
pre-existing translation conventions — schema
const→ openapienum, andexternal
./forge_block.json$ref→#/components/schemas/ForgeBlock—confirmed pre-existing by re-running the same comparison against
HEAD~1.Wish
ebda71cfgives this repo a Rust contract-test crate that actually runs;until it lands, the behavioural proof lives in the companion PR's 767 passing
ritual-coretests.A correction in the second commit, worth reading
The first commit stated the caller receives "the turn as folded up to the
offending event." That is true for
block_delta_unknown_index, raised by anevent — and false for
block_start_index_gap, which is a property of thecompleted turn and is therefore reached only once the whole stream has folded.
That partial turn carries every event in it,
doneincluded. The branch's owntest asserts
finish_reason == "done"on a gap abort, so the shipped testscontradicted the shipped prose.
A second thing a renderer has to expect went unsaid:
blocksholds opened blocksin index order and a gap has no representation in a JSON array, so the positions
close up — a block the sender opened at index 2 arrives at
blocks[0], andthe real indices survive only in the
ErrorEvent'smessage.This matters exactly as much as amendment 3 does: its whole purpose is that
clients render the partial turn rather than discard it, so an implementer reading
the ratified spec would have mis-rendered the gap case. Corrected in every
location that states it.
Known limits
test_frame_wire.pyis untouched — wishebda71cfreplaces it wholesale, sorepairing it would be wasted effort. Consequence worth stating: the Python
reference fold is now behind this contract on two amendments — it accepts
leading zeros and does not implement the
__proto__rule. The Rust folddocuments both as deliberate 1.3.0 divergences rather than defects. This is
another reason that wish exists.
Impact Statement
what a fold returns on abort and what a turn can carry.
frame_wire.json1.2.0 → 1.3.0. Amendments 1, 2and 4 are additive or clarifying; 3 changes the fold's Rust signature, which is
internal to ritual-engine and handled in the companion PR.
FrameTurngains two optional members; threevaluesites gainan explicit type union via a new
JsonValuedefinition;BlockPointergains areceiver rule.
ritual-core/schemas/is re-syncedbyte-identically in the companion PR.
contract; both were blocked on it.
ErrorEvent.message/codenow map ontoFrameTurn.error_message/error_code.currently-valid payload becomes invalid — confirmed by the 13-value
permissiveness check above.
Five amendments to an already-ratified, already-implemented contract. No payload valid against 1.2.0 is invalid against 1.3.0. 1. TYPE `value` EXPLICITLY. FieldSetDelta.value, ElementAppendDelta.value and ToolCall.result carried a description and no `type` keyword at all. Under json-schema-to-typescript a member with no `type` inside an additionalProperties:false object renders as `{ [k: string]: unknown }` — an object map — so field_set("/progress_pct", 42), field_set("/text/markdown", true) and any null failed typecheck in a generated client, though the contract's own motivating examples for the op are scalars. All three now $ref a new $defs/JsonValue, a oneOf over the six JSON types. Permissiveness is unchanged; the omission becomes a statement every generator reads alike. 2. A FAILED TURN CAN SAY WHY. FrameTurn is additionalProperties:false and had no member for an error, so both folds dropped ErrorEvent's message and code and a client could not carry them even if it wanted to. Adds optional error_message and error_code. 3. AN ABORT RETURNS THE PARTIAL TURN. Abort means "no further event may be applied", not "discard what was already applied": a rendering client must stop folding AND keep what it drew. The discard/abort classification is untouched — this is about what the caller receives, not which codes abort. 4. SPECIFIES FOUR UNSPECIFIED BEHAVIOURS: a second block_start for an open index is idempotent last-write-wins (and does not reset seq); RFC 6901 leading zeros in array indices are rejected per 0 / (%x31-39 *DIGIT); a pointer must not address or traverse __proto__/constructor/prototype, because "every container on the path MUST already exist" is satisfied by an inherited member in JavaScript; the block_id-mismatch silent discard is recorded as deliberate rather than a missing code. 5. Engine-side only: the SSE framing rationale correction lands in ritual-engine. Nothing here changes for it. openapi.yaml mirrors every schema change. schemas/index.json registers frame_wire at v1.1; the index is bumped to 1.4.