@myrobotaxi/contracts
v0.11.0
Published
Canonical wire-protocol schemas for the MyRoboTaxi platform. JSON Schema (draft-2020-12) + pre-generated TypeScript types. Consumed by SDKs (TS, Swift) and the Go telemetry server.
Readme
@myrobotaxi/contracts
Canonical wire-protocol schemas for the MyRoboTaxi platform.
- JSON Schema (draft-2020-12) under
schemas/— the language-neutral source of truth. - Pre-generated TypeScript types under
dist/(via the./typessubpath) — produced byjson-schema-to-typescriptand committed tosrc/generated/for diff review.
Consumed by @myrobotaxi/sdk, the Go telemetry server, and the react-frontend Next.js app.
Install
npm install @myrobotaxi/contractsUsage
Type-only import (no runtime payload, tree-shakes to zero bytes):
import type { VehicleState, WebSocketEnvelope } from '@myrobotaxi/contracts/types';Runtime schemas for Ajv / runtime validation:
import { schemas } from '@myrobotaxi/contracts';
// schemas.vehicleState, schemas.vehicleSummary, schemas.drivesList,
// schemas.driveDetail, schemas.driveRoute, schemas.rideRequest,
// schemas.wsMessages, schemas.wsEnvelopeRaw JSON for non-JS consumers (Go):
./node_modules/@myrobotaxi/contracts/schemas/vehicle-state.schema.json
./node_modules/@myrobotaxi/contracts/schemas/vehicle-summary.schema.json
./node_modules/@myrobotaxi/contracts/schemas/drives-list.schema.json
./node_modules/@myrobotaxi/contracts/schemas/drive-detail.schema.json
./node_modules/@myrobotaxi/contracts/schemas/drive-route.schema.json
./node_modules/@myrobotaxi/contracts/schemas/ride-request.schema.json
./node_modules/@myrobotaxi/contracts/schemas/ws-messages.schema.json
./node_modules/@myrobotaxi/contracts/schemas/ws-envelope.schema.jsonSwift
This repo is also a Swift package (MyRobotaxiContracts) with pre-generated Codable/Sendable structs under Sources/MyRobotaxiContracts/Generated/ — produced by scripts/codegen-swift.mjs and committed for diff review, exactly like the TypeScript types. Swift consumers resolve by git URL + semver tag (no registry publish).
In your Package.swift:
dependencies: [
.package(url: "https://github.com/myrobotaxi/contracts.git", from: "0.4.0")
],
targets: [
.target(
name: "MyApp",
dependencies: [.product(name: "MyRobotaxiContracts", package: "contracts")]
)
]Usage:
import MyRobotaxiContracts
let state = try JSONDecoder().decode(VehicleState.self, from: snapshotData)
let envelope = try JSONDecoder().decode(WebSocketEnvelope.self, from: frameData)
if envelope.type == .vehicleUpdate { /* decode VehicleUpdatePayload from envelope.payload */ }The same custom annotations are folded into the generated SwiftDoc comments, so grep '@classification "P1"' works inside the checked-out package too. Wire enums (status, chargeState, MessageType, error codes, …) generate as decode-tolerant Swift enums: a RawRepresentable enum with the known cases plus a catch-all case unrecognized(String) (MYR-195). Nullable / non-required schema fields are Swift optionals. The intentionally-open wire positions — WebSocketEnvelope.payload and VehicleUpdatePayload.fields — are typed via the hand-written JSONValue enum, which preserves explicit JSON nulls (required for atomic-group clears per NFR-3.9).
Decode-tolerant wire enums (forward-compat for installed clients)
A plain enum: String (Swift's synthesized Codable) throws on the first unrecognized value, which would make one new enum member shipped in a newer contracts version hard-fail the entire frame/snapshot decode on a not-yet-updated iOS app. To keep installed clients forward-compatible, each string wire enum is emitted as:
public enum Status: RawRepresentable, Codable, Hashable, Sendable {
case driving
case parked
// …known cases…
/// A wire value not known to this build of the contracts package.
case unrecognized(String)
// init(rawValue:) / rawValue / init(from:) / encode(to:) map every string,
// falling back to .unrecognized(raw) instead of throwing.
}- Unknown values never throw — they decode to
.unrecognized(raw), so the surroundingVehicleState/WebSocketEnvelopestill decodes. - Round-trips byte-identically — the raw string is preserved and re-encoded verbatim, so a relaying client never corrupts a value it doesn't understand.
- Ergonomic for consumers —
switch status { case .driving: …; case .unrecognized(let raw): … }stays exhaustive and compiler-checked;== .parkedand.rawValuework as before. The catch-all is namedunrecognized(notunknown) becauseChargeStatealready has a known member whose wire value is"Unknown".
Tool choice (Swift): custom generator
Like the TypeScript side, the Swift generator is a small custom script (scripts/codegen-swift.mjs, no quicktype): it keeps the annotation-folding grep affordance, emits exactly one type per root schema and per $defs entry (websocket-protocol.md §9.2), needs no npm install (zero dependencies), and is deterministic so CI can diff the committed output.
What's in the generated TypeScript
Custom JSON Schema annotations from the source schemas — x-classification, x-atomic-group, x-unit, x-encrypted, x-tesla-proto-field — are folded into the generated TSDoc comments at codegen time. This means consumers can grep '@classification "P1"' inside their node_modules to find every sensitive field without leaving their editor.
/**
* GPS latitude. Encrypted at rest (AES-256-GCM) per FR-11.1.
*
* @classification "P1"
* @atomic-group "gps"
* @encrypted true
*/
latitude: number;Tool choice: json-schema-to-typescript
We picked json-schema-to-typescript v15+ over quicktype for three reasons:
- Matches the telemetry repo's existing convention. The Go-side
vehicle-state-schema.md§6.1 already documentsjson-schema-to-typescriptas the TS-side tool. Picking the same one in the contracts package guarantees zero drift between the docs and the actual output. - Pure JS (no Rust binary). Easier to vendor into CI, no platform-specific install issues.
- TSDoc-preserving. The
descriptionfield on each JSON Schema property is copied verbatim into the generated TSDoc comment.quicktypeproduces leaner runtime helpers (parsers / serializers) but does not preserve descriptions as cleanly, and we don't need its runtime artifacts here.
The trade-off is that quicktype's discriminated-union handling for oneOf payloads is more idiomatic. The current schemas use a nullable-enum-with-explicit-null pattern instead of oneOf, so this doesn't bite us yet. If the schema set grows to need true discriminated unions, we may revisit.
Schema-authoring policy
Schemas in this repo are vendored from myrobotaxi/telemetry/docs/contracts/schemas/ — that repo is the current authoring home.
Until Phase 2 ships, schema changes require a paired PR to this repo (mirroring the change here, re-running codegen, bumping the version).
Phase 2 migration
A planned follow-up moves the schema authoring home from telemetry/docs/contracts/schemas/ into this repo, eliminating the paired-PR requirement. After Phase 2, the telemetry server's Go contract-tester will consume the published @myrobotaxi/contracts package directly.
Versioning policy
- Major version bump required for any wire-incompatible change (per NFR-3.37 in the telemetry docs).
- Minor version bump for additive, backwards-compatible additions (new optional fields, new message types, new enum members on append-only enums).
- Patch version bump for documentation / annotation changes that don't alter the generated TypeScript surface.
Development
npm install
npm run codegen # regenerate src/generated/ from schemas/
npm run codegen:swift # regenerate Sources/MyRobotaxiContracts/Generated/ from schemas/
npm run typecheck
npm run test
npm run build # tsup → dist/
swift build # Swift package
swift testCI runs both codegens and fails if src/generated/ or Sources/MyRobotaxiContracts/Generated/ drifts from the schemas — every schema bump must be paired with a regenerate-and-commit (both languages).
License
MIT
