Write down the backend <-> ai-worker contract

`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`.
This commit is contained in:
2026-10-11 16:58:57 +00:00
parent 88330ba9d3
commit 362c8222c1
5 changed files with 217 additions and 6 deletions
+26 -6
View File
@@ -13,19 +13,39 @@ JSON Schema, compiled into two consumer packages:
## Layout
```
schemas/continuum/v1/ Continuum's own domain model (PrintJob, Template, ...)
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. There are only
four small, stable DTOs here (`PrintJob`, `MaterialProfile`,
`PrintParameters`, `Template`), so keeping two hand-synced copies in sync
is less overhead than running a codegen pipeline (`protoc`/`ts-proto`/
`prost-build`) nothing actually consumed - see git history for the
`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