Contract v1 · Canonical bytes · MIT

One contract.
Every runtime.

Keel is the transport-neutral application contract shared by Racket desktop runtimes: one value model, one canonical encoding, one error shape — so native and WebView apps speak the same bytes.

Transport-neutral Canonical bytes Bounded resources
CANONICAL VALUE

keel/v1

✓ canonical
08 · Map
2 entries, keys sorted
04 · String
"task" → "review"
07 · Float64
"progress" → 0.5
06 · List
tags → "racket", "desktop"
64 MiB max encoded value
RVT1byte-identical
GLZ1same bytes

BELOW THE PROTOCOL, ABOVE THE BYTES

Envelopes differ. Meaning shouldn't.

RVT1 frames a native RPC; GLZ1 frames a WebView invoke. Frames, request ids, and backpressure are transport business — but the payload they carry kept being redefined by each runtime.

Keel owns what travels inside: values, structured errors, lifecycle names, and a schema vocabulary. It sits above a byte stream and below an application protocol, so Rivet and Glaze embed one contract instead of maintaining two.

01

Canonical by construction

One value encodes to one byte sequence. Map keys are ordered by their UTF-8 bytes, non-finite floats are rejected, and decoders refuse anything non-canonical.

02

Transport-neutral

A schema describes records, commands, events, and states without owning an envelope: Rivet maps a command to an RVT1 RPC, Glaze to a GLZ1 invoke. Names like ready? stay exact.

03

Bounded resources

Every implementation enforces the same budgets — 64 MiB, depth 64, 262,144 nodes — and rejects the same malformed inputs. No runtime is more forgiving than another.

TEN TAGS, NO AMBIGUITY

The whole value union fits in one nibble.

Every value is one tag byte followed by a fixed body. Integers are little-endian; lengths are unsigned 32-bit.

Tag Value Body
00Nullnone
01Bool · falsenone
02Bool · truenone
03Int64signed two's-complement, little-endian
04Stringlength + well-formed UTF-8
05Byteslength + uninterpreted bytes
06Listitem count + encoded values
07Float64finite IEEE-754 binary64 bits
08Mapentry count + String pairs, keys sorted
09Errorcode + message + data value

Error codes are kebab-case and deliberately not a closed list — RVT1, GLZ1, and future transports each publish and negotiate their own taxonomy.

64 MiBmax encoded value
64max nesting depth
262,144max value nodes

ONE VOCABULARY, TERMINAL ENDS

Every runtime says the same words.

startingbootstrapping
readyaccepting work
runningsteady state
stoppingdraining
stoppedterminal

Early shutdown may jump from starting or ready straight to stopping. Success, failure, cancellation, and timeout are mutually exclusive outcomes — cancellation races completion, and the winner owns the single terminal result.

RACKET REFERENCE IMPLEMENTATION

From value to bytes.

Install the package, encode a value, get the canonical bytes — the same bytes every conforming runtime produces.

Read the normative Contract v1 →
keel — Racket
> (require keel)

> (define wire
    (encode-app-value
     (hash "task" "review" "progress" 0.5)))

> (decode-app-value wire)
'#hash(("progress" . 0.5) ("task" . "review"))

> (make-app-error "invalid-args"
                  "Expected a non-empty title"
                  (hash "field" "title"))

COMPATIBILITY DISCIPLINE

Golden bytes never change.

tests/golden-v1.txt is the normative byte corpus: changing an existing line is a compatibility break. New union members need a written rationale, new cross-runtime golden vectors, decoder budget tests, and a version decision.

A DELIBERATE BOUNDARY

A contract, not a transport.

Keel owns no frame header, request identifier, streaming or backpressure, renderer, permission model, or installer. Rivet keeps RVT1 and its native hosts; Glaze keeps GLZ1 and its WebView adapters; other transports may embed this codec but negotiate their own versions and error taxonomies.