Skip to main content

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/abiThe same, and exports the result as C functions rather than as types
bindings/python, bindings/javascriptNeither 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.