`stream:camera_frames` and `channel:print_alerts` are the only interface between `continuum-backend` and `continuum-ai-worker`, and there was nothing specifying either. They had drifted completely: the backend wrote `farmId`, `printerId`, `r2ObjectKey` and `capturedAt`; the worker read a single field called `printer_id` and ignored the rest. Every task was delivered, acknowledged, and attributed to a printer called "unknown" - and nothing errored, because a stream entry is a flat string map and asking it for a field that is not there is not a failure. Same shape as the /ws/edge/v1 drift, found the same way. `schemas/continuum/v1/ai/` is now canonical, with a TypeBox mirror in ts-types and a Rust mirror in the worker. Two things the schemas have to say that JSON Schema does not naturally express, both of which caused real bugs: - A Valkey stream entry is a flat field/value map of *strings*. No nesting, no numbers, no null. So FrameTask has no numeric or nested properties, and a field with no value is omitted from the XADD rather than written as `""` - an empty string is a value, and the producer was writing one for `print_job_id`, which a consumer would carry into a lookup or a uuid column. - PrintAlert, by contrast, *is* a JSON document, because PUBLISH carries one opaque string. It is also fire-and-forget with no persistence, so it can only ever be a live-console feed and never the record of a detection. The README's layout section was stale (it still listed four DTOs that were removed in 2026-09) and did not mention `edge/` or `ai/` at all. It now names both, says why the inter-service protocols live here, and notes that their Rust mirrors sit in the single repo that consumes each rather than in `packages/rust-types`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
68 lines
3.2 KiB
Markdown
68 lines
3.2 KiB
Markdown
# continuum-schemas
|
|
|
|
Single source of truth for the Continuum data model. Everything here is
|
|
JSON Schema, compiled into two consumer packages:
|
|
|
|
- `packages/ts-types` - hand-written [TypeBox](https://github.com/sinclairzx81/typebox)
|
|
schemas under `src/schemas/` used for runtime validation in
|
|
`continuum-backend`'s ElysiaJS routes.
|
|
- `packages/rust-types` - a Rust crate (`continuum-rust-types`) with
|
|
hand-written, `serde`-derived structs for `continuum-proxy` and
|
|
`continuum-ai-worker`.
|
|
|
|
## Layout
|
|
|
|
```
|
|
schemas/continuum/v1/ Continuum's own model
|
|
ai/ continuum-backend <-> continuum-ai-worker: the
|
|
stream:camera_frames work queue and the
|
|
channel:print_alerts notification channel
|
|
edge/ continuum-backend <-> continuum-proxy: the
|
|
/ws/edge/v1 gateway uplink protocol
|
|
material/ printer/ domain objects
|
|
vendor.schema.json
|
|
schemas/bambulab/ Bambu Lab MQTT wire schemas (v1/v2, report + request)
|
|
packages/ts-types/ TypeBox schemas (hand-written, mirrors schemas/)
|
|
packages/rust-types/ serde structs (hand-written, mirrors schemas/)
|
|
```
|
|
|
|
The two inter-service protocols - `edge/` and `ai/` - are here for a
|
|
specific reason: each has two independent implementations, one per repo, and
|
|
when there was nothing written down between them they drifted completely
|
|
while every test passed. `/ws/edge/v1` had an `auth` handshake the gateway
|
|
never sent; `stream:camera_frames` had a producer writing `printerId` and a
|
|
consumer reading `printer_id`, so every frame was attributed to a printer
|
|
called "unknown". Neither failure was visible from either side alone, which
|
|
is what a canonical schema plus a contract test on each end is for.
|
|
|
|
`schemas/continuum/v1/*.schema.json` is the canonical shape for each
|
|
domain object; `packages/ts-types/src/schemas/` and
|
|
`packages/rust-types/src/lib.rs` both mirror it by hand. A mirror is added
|
|
per schema only once something actually consumes it, so not every file here
|
|
has one, and the two inter-service protocols keep their Rust mirror in the
|
|
single repo that consumes it rather than in `packages/rust-types`:
|
|
`edge/` in `continuum-proxy/src/uplink/protocol.rs`, `ai/` in
|
|
`continuum-ai-worker/src/task.rs`. The set is small and changes slowly,
|
|
which is why keeping hand-synced copies in sync is less overhead than a
|
|
codegen pipeline (`protoc`/`ts-proto`/`prost-build`) nothing actually
|
|
consumed - see git history for the
|
|
protobuf version this replaced, and why it was dropped: every service in
|
|
this system talks JSON (JSON Schema for the wire, TypeBox/serde for
|
|
runtime validation), so protobuf's actual value - a single schema compiled
|
|
into a compact **binary** wire format for both languages - never applied.
|
|
|
|
Whenever a domain schema changes, update all three by hand: the JSON
|
|
Schema file, the TypeBox schema, and the Rust struct.
|
|
|
|
## Build / verify
|
|
|
|
Requires `npm install` at the repo root, and a Rust toolchain, first.
|
|
|
|
```
|
|
npm install
|
|
npm run build:rust # cargo build, confirms the Rust structs compile
|
|
npm run typecheck # tsc -b, confirms the TypeBox schemas compile
|
|
```
|
|
|
|
There's no `build:ts` step - the TS side is hand-written, not generated.
|