Wire contract
The .proto files every Ranvier implementation is generated from, and the
Rust generated from them. Sample, StreamInfo, Frame, Request — what a
node announces, what it publishes, and how both are framed.
proto/ranvier/message/v1/ what travels on the wire
proto/ranvier/example/v1/ an illustrative schema for docs and tests
src/generated/ the committed Rust, and the descriptor set
What is here, and what is not
Here: the envelope. A Sample carries values, a sequence number, and two
timestamps — one for when Ranvier published the reading, one for when the
instrument measured it. A StreamInfo announces what a stream is: its
identity, its authorisation, the schema its payloads conform to, its settings,
and how it is produced. Frame and Request are the transport around them.
Not here: what a measurement means. Gaze, body pose, inertial measurement,
scalp potentials and the vectors they are built from are a canonical
vocabulary, and this repository does not define one — see
decision 0012
for how the split ended up here. A node announces a schema identifier from
whatever vocabulary it publishes, inside a StreamInfo that this contract
defines; the identifier is the vocabulary's and the envelope is Ranvier's, and
neither side needs the other to build.
That division is the whole reason this directory is not its own repository.
It was one — ranvier-schemas, from 2026-08-07 — for as long as two contracts
with different audiences and different change rates lived together and several
languages generated from both. One contract with one implementation buys a
coordination step and nothing else.
Field numbers are permanent
From the first recording anyone intends to keep. A recording decodes by
number, so a file in which field 3 meant values fixes 3 as values for as
long as that file must be readable. Names may change; numbers may not.
That means additive-only evolution: new fields take new numbers, retired ones are reserved rather than reused, and a genuinely incompatible contract is a new version alongside the old rather than an edit to this one. The rule outlived the decision that introduced it — 0007 is superseded on the question of who defines the recording format, and keeps this paragraph.
Regenerating
cargo run --manifest-path tools/regen-proto/Cargo.toml
Run from the repository root, after any change under proto/. It writes
src/generated/ and you commit the result, so an ordinary cargo build needs
neither a code generator nor protoc.
The generated Rust is committed on purpose, and committing it is only worth
what checks it. A contract edited without regenerating leaves types that read
the schema as it stood before the edit, and nothing inside this crate would
disagree with itself about it. Continuous integration runs the generator and
fails on any diff — the contract job in
.github/workflows/ci.yml. protoc
is pinned by protoc-bin-vendored rather than taken from the machine, because
two versions can format their output differently and a check that fails for
that reason teaches people to ignore it.
Consumers
| How it reads this | |
|---|---|
crates/ | Depends on the ranvier-message crate directly |
crates/abi | The same, and exports the result as C functions rather than as types |
bindings/python, bindings/javascript | Neither reads it. They call the ABI, which has already decoded |
Nothing generates a second copy of these types any more. The JavaScript
binding was ranvier-sdk-js, a separate repository holding its own copy of
proto/ranvier/message/v1/ kept in step by a script that reached a sibling
checkout. The script pointed at a repository the contract had already left, so
syncing copied a file that had stopped moving and the package fell 85 lines
behind with every check green. Moving both clients into this repository fixed
the staleness by having them generate from the root proto/ directly; deleting
them on 2026-08-23 removed the generated runtimes altogether, because a binding
over the C ABI is handed decoded values and never sees a protobuf message. The
Python binding ships no runtime dependency at all, protobuf included:
grep '^dependencies' bindings/python/pyproject.toml # dependencies = []
What is not checked
No lint gate and no breaking-change gate. buf lint and buf breaking — the
latter under the FILE category rather than WIRE, since wire compatibility
accepts a field rename that stops generated code compiling in every
downstream language — are recorded as deferred in
deferred.md. What runs today is
protoc accepting these files and the generated Rust matching them.