@myrobotaxi/contracts 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTRIBUTING.md CHANGED
@@ -11,8 +11,8 @@ This means any schema change requires:
11
11
  1. Open a PR in [`myrobotaxi/telemetry`](https://github.com/myrobotaxi/telemetry) that edits the source `.schema.json` file under `docs/contracts/schemas/`. Get it reviewed and merged.
12
12
  2. Open a paired PR in this repo that:
13
13
  - Copies the new schema file from the telemetry repo into `schemas/`
14
- - Runs `npm run codegen` to regenerate `src/generated/`
15
- - Commits the regenerated `.ts` files
14
+ - Runs `npm run codegen` to regenerate `src/generated/` and `npm run codegen:swift` to regenerate `Sources/MyRobotaxiContracts/Generated/`
15
+ - Commits the regenerated `.ts` and `.swift` files
16
16
  - Bumps the version per the [versioning policy](#versioning-policy)
17
17
 
18
18
  Until [Phase 2](#phase-2-migration-authoring-home) ships, this paired-PR convention is enforced by review (no automation).
@@ -40,9 +40,23 @@ The MyRoboTaxi schemas use these non-standard keywords:
40
40
  | `x-encrypted` / `x-encrypted-at-rest` | AES-256-GCM at rest |
41
41
  | `x-tesla-proto-field` | Source Tesla protobuf field number |
42
42
 
43
- The codegen pipeline (`scripts/codegen.mjs`) **folds these into TSDoc comments** at generation time, so they survive into the consumer's editor as `@classification "P0"` annotations.
43
+ The codegen pipeline (`scripts/codegen.mjs`) **folds these into TSDoc comments** at generation time, so they survive into the consumer's editor as `@classification "P0"` annotations. The Swift pipeline (`scripts/codegen-swift.mjs`) does the same folding into SwiftDoc (`///`) comments.
44
44
 
45
- If you add a new `x-*` keyword to a schema, add it to the `ANNOTATIONS` array in `scripts/codegen.mjs` and re-run `npm run codegen`.
45
+ If you add a new `x-*` keyword to a schema, add it to the `ANNOTATIONS` array in **both** `scripts/codegen.mjs` and `scripts/codegen-swift.mjs`, then re-run `npm run codegen` and `npm run codegen:swift`.
46
+
47
+ ## Swift codegen pipeline
48
+
49
+ The repo doubles as a Swift package (`Package.swift` at the root, product `MyRobotaxiContracts`). Swift types are generated from the same `schemas/*.json` source of truth:
50
+
51
+ - **Generator**: `scripts/codegen-swift.mjs` — a small custom Node script (zero npm dependencies; runs without `npm ci`). One `Codable`/`Equatable`/`Sendable` struct per root schema object and per `$defs` entry (websocket-protocol.md §9.2); string enums become **decode-tolerant** Swift enums — a `RawRepresentable` enum with the known cases plus a catch-all `case unrecognized(String)` (MYR-195). Unknown wire values decode to `.unrecognized(raw)` instead of throwing (so a newer contracts version adding an enum member never hard-fails an older installed client's whole-frame decode) and re-encode byte-identically. The generator emits a small suite of unit tests (`src/codegen-swift.test.ts`) alongside the Swift `swift test` round-trip tests.
52
+ - **Loud codegen errors**: to prevent silent wire drift, `scripts/codegen-swift.mjs` hard-errors (naming the schema path) rather than degrading when it hits a construct it can't faithfully map:
53
+ - a property enum whose members match a `$defs` enum but in a **different order** (would otherwise silently mint a near-duplicate nested type) — reorder the property enum to match the `$defs` order, or replace it with a `$ref`;
54
+ - schema composition (`oneOf` / `anyOf` / `allOf`) and **non-string enums** (would otherwise silently degrade to `JSONValue` / `Int`) — model the position as a concrete typed object, a string enum, or an explicit open `JSONValue`.
55
+ - **Output**: `Sources/MyRobotaxiContracts/Generated/*.swift` — **committed**, never hand-edited (same rule as `src/generated/`). Regeneration is deterministic: emission order follows the schema JSON, so re-running on an unchanged schema is a no-op diff.
56
+ - **Hand-written support code**: `Sources/MyRobotaxiContracts/JSONValue.swift` (arbitrary-JSON enum used for `WebSocketEnvelope.payload` and `VehicleUpdatePayload.fields`) is NOT generated and may be edited.
57
+ - **Regenerate**: `npm run codegen:swift` after any schema change, and commit the result alongside the regenerated TypeScript.
58
+ - **CI**: the `swift` job on `macos-latest` runs `swift build`, `swift test`, then `npm run codegen:swift` and fails on drift in `Sources/MyRobotaxiContracts/Generated/` — mirroring the TypeScript `codegen-drift` job.
59
+ - **Consumption**: Swift consumers resolve the package by git URL + tag (`from: "0.x.y"`); there is no Swift registry publish. The `v0.x.y` git tags pushed for npm releases are also what SwiftPM resolves against, so the versioning policy above applies to the Swift surface too.
46
60
 
47
61
  ## Release process
48
62
 
package/README.md CHANGED
@@ -28,7 +28,7 @@ import { schemas } from '@myrobotaxi/contracts';
28
28
  // schemas.vehicleState, schemas.wsMessages, schemas.wsEnvelope
29
29
  ```
30
30
 
31
- Raw JSON for non-JS consumers (Go, Swift):
31
+ Raw JSON for non-JS consumers (Go):
32
32
 
33
33
  ```
34
34
  ./node_modules/@myrobotaxi/contracts/schemas/vehicle-state.schema.json
@@ -36,6 +36,60 @@ Raw JSON for non-JS consumers (Go, Swift):
36
36
  ./node_modules/@myrobotaxi/contracts/schemas/ws-envelope.schema.json
37
37
  ```
38
38
 
39
+ ## Swift
40
+
41
+ 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).
42
+
43
+ In your `Package.swift`:
44
+
45
+ ```swift
46
+ dependencies: [
47
+ .package(url: "https://github.com/myrobotaxi/contracts.git", from: "0.4.0")
48
+ ],
49
+ targets: [
50
+ .target(
51
+ name: "MyApp",
52
+ dependencies: [.product(name: "MyRobotaxiContracts", package: "contracts")]
53
+ )
54
+ ]
55
+ ```
56
+
57
+ Usage:
58
+
59
+ ```swift
60
+ import MyRobotaxiContracts
61
+
62
+ let state = try JSONDecoder().decode(VehicleState.self, from: snapshotData)
63
+ let envelope = try JSONDecoder().decode(WebSocketEnvelope.self, from: frameData)
64
+ if envelope.type == .vehicleUpdate { /* decode VehicleUpdatePayload from envelope.payload */ }
65
+ ```
66
+
67
+ 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 `null`s (required for atomic-group clears per NFR-3.9).
68
+
69
+ ### Decode-tolerant wire enums (forward-compat for installed clients)
70
+
71
+ 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:
72
+
73
+ ```swift
74
+ public enum Status: RawRepresentable, Codable, Hashable, Sendable {
75
+ case driving
76
+ case parked
77
+ // …known cases…
78
+ /// A wire value not known to this build of the contracts package.
79
+ case unrecognized(String)
80
+ // init(rawValue:) / rawValue / init(from:) / encode(to:) map every string,
81
+ // falling back to .unrecognized(raw) instead of throwing.
82
+ }
83
+ ```
84
+
85
+ - **Unknown values never throw** — they decode to `.unrecognized(raw)`, so the surrounding `VehicleState` / `WebSocketEnvelope` still decodes.
86
+ - **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.
87
+ - **Ergonomic for consumers** — `switch status { case .driving: …; case .unrecognized(let raw): … }` stays exhaustive and compiler-checked; `== .parked` and `.rawValue` work as before. The catch-all is named `unrecognized` (not `unknown`) because `ChargeState` already has a known member whose wire value is `"Unknown"`.
88
+
89
+ ## Tool choice (Swift): custom generator
90
+
91
+ 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.
92
+
39
93
  ## What's in the generated TypeScript
40
94
 
41
95
  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.
@@ -82,12 +136,16 @@ A planned follow-up moves the schema authoring home from `telemetry/docs/contrac
82
136
  ```bash
83
137
  npm install
84
138
  npm run codegen # regenerate src/generated/ from schemas/
139
+ npm run codegen:swift # regenerate Sources/MyRobotaxiContracts/Generated/ from schemas/
85
140
  npm run typecheck
86
141
  npm run test
87
142
  npm run build # tsup → dist/
143
+
144
+ swift build # Swift package
145
+ swift test
88
146
  ```
89
147
 
90
- CI runs `codegen` and fails if `src/generated/` drifts from the schemas — every schema bump must be paired with a regenerate-and-commit.
148
+ CI 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).
91
149
 
92
150
  ## License
93
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myrobotaxi/contracts",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "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.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,6 +52,7 @@
52
52
  ],
53
53
  "scripts": {
54
54
  "codegen": "node scripts/codegen.mjs",
55
+ "codegen:swift": "node scripts/codegen-swift.mjs",
55
56
  "build": "tsup",
56
57
  "clean": "rm -rf dist coverage",
57
58
  "lint": "eslint .",