Stop committing tsc output, and pin TypeBox exactly

Two cleanups, one cause each.

The root tsconfig.json carried the compilerOptions directly with no `include`,
which made `tsc -b` treat the whole repository as one project: it ignored each
package's `outDir` and emitted .js/.d.ts next to every source file, which then
got committed. So every typecheck dirtied the tree and the build output was in
git. The root is now a solution file that only references the real projects,
shared options moved to tsconfig.base.json, and the six emitted files are
removed from the index. Output goes to packages/ts-types/dist, which was
already gitignored.

@sinclair/typebox is now pinned exactly rather than by caret range. It is
load-bearing in a way a range should not be allowed near: continuum-backend's
Elysia requires it at runtime without declaring it as a dependency at all, and
@continuum/ts-types resolves its own copy from here, so two different versions
mean two structurally incompatible TSchema types and nothing from this package
validates anything there.

Worth recording what that looked like, because the error pointed elsewhere.
Elysia accepts `TSchema | StandardSchemaV1Like`; when the TSchema branch failed
on the version skew, TypeScript reported the *other* branch's failure,
`'~standard' is missing`. That reads as "Elysia wants a Standard Schema
validator, TypeBox is wrong here", which is not true - Elysia is TypeBox-native
and Standard Schema is just the alternative for Zod/Valibot users. The fix was
only ever to make the versions match.

0.34.52 rather than 0.33.x because Elysia 1.4 calls `t.Module`, which 0.34
introduced. continuum-backend pins the identical version and
tests/dependency_identity.test.ts there fails if the two ever drift again.
This commit is contained in:
2026-10-03 12:16:35 +00:00
parent bb5a2680af
commit 88330ba9d3
11 changed files with 29 additions and 306 deletions
+1 -1
View File
@@ -10,6 +10,6 @@
".": "./src/index.ts"
},
"dependencies": {
"@sinclair/typebox": "^0.33.12"
"@sinclair/typebox": "0.34.52"
}
}
-1
View File
@@ -1 +0,0 @@
export * from "./schemas";
-1
View File
@@ -1 +0,0 @@
export * from "./schemas";
-152
View File
@@ -1,152 +0,0 @@
import { type Static } from "@sinclair/typebox";
/**
* The /ws/edge/v1 uplink protocol, mirroring
* `schemas/continuum/v1/edge/*.schema.json` by hand - the same convention
* every other schema in this package follows. The JSON Schema files are
* canonical; change them first, then this, then the Rust mirror (which for
* this one protocol lives in `continuum-proxy/src/uplink/protocol.rs` rather
* than in `packages/rust-types`, since the gateway is its only Rust
* consumer).
*
* Field names are snake_case, unlike the backend's HTTP routes. That is not
* an oversight: the other end of this wire is serde-derived Rust, and
* snake_case is what those structs produce without per-field rename
* attributes. It is also what the rest of this repository uses.
*/
export declare const PrinterTelemetrySchema: import("@sinclair/typebox").TObject<{
/**
* The *gateway's* own id from its printers.toml - not a vendor serial and
* not `printers.id`. The backend resolves it through
* `printers.gateway_printer_id`, scoped to the authenticated farm.
*/
printer_id: import("@sinclair/typebox").TString;
name: import("@sinclair/typebox").TString;
/**
* The printer's own state string rather than an enum, so an unrecognised
* state survives the trip instead of being dropped at the edge. Bambu
* emits IDLE, RUNNING, PAUSE, FINISH, FAILED.
*/
state: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
percent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
layer: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
total_layers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
remaining_minutes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
job_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
nozzle_temp: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
nozzle_target: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
bed_temp: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
bed_target: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
/** Vendor error code; non-zero means the printer is reporting a fault. */
print_error: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
/** Count of active Health-Management-System entries, not decoded messages. */
active_alerts: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
}>;
export type PrinterTelemetry = Static<typeof PrinterTelemetrySchema>;
/**
* Deliberately small, and deliberately missing "start a print" - see the
* JSON Schema for why that variant is absent until the gateway's
* start-an-uploaded-file path has been run against real hardware by hand.
*/
export declare const PrinterCommandSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
action: import("@sinclair/typebox").TLiteral<"set_fan_speed">;
/**
* A percentage on this wire whatever the vendor wants. A Bambu turns it
* into an M106 PWM value and reports it back as one of 16 gears, so what
* is set here does not read back identically.
*/
percent: import("@sinclair/typebox").TInteger;
}>, import("@sinclair/typebox").TObject<{
action: import("@sinclair/typebox").TLiteral<"send_gcode_line">;
/** The most powerful variant by far; be careful about exposing it to a UI. */
line: import("@sinclair/typebox").TString;
}>]>;
export type PrinterCommand = Static<typeof PrinterCommandSchema>;
export declare const EdgeClientMessageSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
type: import("@sinclair/typebox").TLiteral<"hello">;
gateway_id: import("@sinclair/typebox").TString;
farm_id: import("@sinclair/typebox").TString;
token: import("@sinclair/typebox").TString;
}>, import("@sinclair/typebox").TObject<{
type: import("@sinclair/typebox").TLiteral<"heartbeat">;
/** Monotonic per connection, restarting at 1 on reconnect. */
seq: import("@sinclair/typebox").TInteger;
}>, import("@sinclair/typebox").TObject<{
type: import("@sinclair/typebox").TLiteral<"telemetry">;
/**
* A batch, not a message per printer: a farm has one uplink and many
* printers, and a coherent picture of the farm at an instant is worth
* more here than the lowest possible per-machine latency.
*/
printers: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
/**
* The *gateway's* own id from its printers.toml - not a vendor serial and
* not `printers.id`. The backend resolves it through
* `printers.gateway_printer_id`, scoped to the authenticated farm.
*/
printer_id: import("@sinclair/typebox").TString;
name: import("@sinclair/typebox").TString;
/**
* The printer's own state string rather than an enum, so an unrecognised
* state survives the trip instead of being dropped at the edge. Bambu
* emits IDLE, RUNNING, PAUSE, FINISH, FAILED.
*/
state: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
percent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
layer: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
total_layers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
remaining_minutes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
job_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
nozzle_temp: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
nozzle_target: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
bed_temp: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
bed_target: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
/** Vendor error code; non-zero means the printer is reporting a fault. */
print_error: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
/** Count of active Health-Management-System entries, not decoded messages. */
active_alerts: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
}>>;
}>, import("@sinclair/typebox").TObject<{
type: import("@sinclair/typebox").TLiteral<"command_result">;
command_id: import("@sinclair/typebox").TString;
ok: import("@sinclair/typebox").TBoolean;
/** Only when `ok` is false. A message for an operator, not a code. */
error: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
}>, import("@sinclair/typebox").TObject<{
type: import("@sinclair/typebox").TLiteral<"frame_batch">;
printer_id: import("@sinclair/typebox").TString;
print_job_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
frames: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
object_key: import("@sinclair/typebox").TString;
captured_at: import("@sinclair/typebox").TString;
}>>;
}>]>;
export type EdgeClientMessage = Static<typeof EdgeClientMessageSchema>;
export declare const EdgeServerMessageSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
type: import("@sinclair/typebox").TLiteral<"hello_ack">;
session_id: import("@sinclair/typebox").TString;
}>, import("@sinclair/typebox").TObject<{
type: import("@sinclair/typebox").TLiteral<"hello_error">;
message: import("@sinclair/typebox").TString;
}>, import("@sinclair/typebox").TObject<{
type: import("@sinclair/typebox").TLiteral<"heartbeat_ack">;
seq: import("@sinclair/typebox").TInteger;
}>, import("@sinclair/typebox").TObject<{
type: import("@sinclair/typebox").TLiteral<"command">;
command_id: import("@sinclair/typebox").TString;
/** The gateway's own printer id - the backend translates on the way out. */
printer_id: import("@sinclair/typebox").TString;
command: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
action: import("@sinclair/typebox").TLiteral<"set_fan_speed">;
/**
* A percentage on this wire whatever the vendor wants. A Bambu turns it
* into an M106 PWM value and reports it back as one of 16 gears, so what
* is set here does not read back identically.
*/
percent: import("@sinclair/typebox").TInteger;
}>, import("@sinclair/typebox").TObject<{
action: import("@sinclair/typebox").TLiteral<"send_gcode_line">;
/** The most powerful variant by far; be careful about exposing it to a UI. */
line: import("@sinclair/typebox").TString;
}>]>;
}>]>;
export type EdgeServerMessage = Static<typeof EdgeServerMessageSchema>;
-125
View File
@@ -1,125 +0,0 @@
import { Type } from "@sinclair/typebox";
/**
* Optional *and* nullable: the key may be missing, or present as `null`.
*
* Strict in what we send, liberal in what we accept. A gateway omits fields
* it has no value for, which is what the JSON Schema describes - but a
* serde-derived Rust struct emits `null` by default unless every field is
* annotated, and one such field reaching here would otherwise fail
* validation and make Elysia drop an entire telemetry batch over a printer
* that merely had no job name. Not worth risking a farm's telemetry on.
*/
const Maybe = (schema) => Type.Optional(Type.Union([schema, Type.Null()]));
/**
* The /ws/edge/v1 uplink protocol, mirroring
* `schemas/continuum/v1/edge/*.schema.json` by hand - the same convention
* every other schema in this package follows. The JSON Schema files are
* canonical; change them first, then this, then the Rust mirror (which for
* this one protocol lives in `continuum-proxy/src/uplink/protocol.rs` rather
* than in `packages/rust-types`, since the gateway is its only Rust
* consumer).
*
* Field names are snake_case, unlike the backend's HTTP routes. That is not
* an oversight: the other end of this wire is serde-derived Rust, and
* snake_case is what those structs produce without per-field rename
* attributes. It is also what the rest of this repository uses.
*/
export const PrinterTelemetrySchema = Type.Object({
/**
* The *gateway's* own id from its printers.toml - not a vendor serial and
* not `printers.id`. The backend resolves it through
* `printers.gateway_printer_id`, scoped to the authenticated farm.
*/
printer_id: Type.String(),
name: Type.String(),
/**
* The printer's own state string rather than an enum, so an unrecognised
* state survives the trip instead of being dropped at the edge. Bambu
* emits IDLE, RUNNING, PAUSE, FINISH, FAILED.
*/
state: Maybe(Type.String()),
percent: Maybe(Type.Integer({ minimum: 0, maximum: 100 })),
layer: Maybe(Type.Integer({ minimum: 0 })),
total_layers: Maybe(Type.Integer({ minimum: 0 })),
remaining_minutes: Maybe(Type.Integer({ minimum: 0 })),
job_name: Maybe(Type.String()),
nozzle_temp: Maybe(Type.Number()),
nozzle_target: Maybe(Type.Number()),
bed_temp: Maybe(Type.Number()),
bed_target: Maybe(Type.Number()),
/** Vendor error code; non-zero means the printer is reporting a fault. */
print_error: Maybe(Type.Integer()),
/** Count of active Health-Management-System entries, not decoded messages. */
active_alerts: Maybe(Type.Integer({ minimum: 0 })),
});
/**
* Deliberately small, and deliberately missing "start a print" - see the
* JSON Schema for why that variant is absent until the gateway's
* start-an-uploaded-file path has been run against real hardware by hand.
*/
export const PrinterCommandSchema = Type.Union([
Type.Object({
action: Type.Literal("set_fan_speed"),
/**
* A percentage on this wire whatever the vendor wants. A Bambu turns it
* into an M106 PWM value and reports it back as one of 16 gears, so what
* is set here does not read back identically.
*/
percent: Type.Integer({ minimum: 0, maximum: 100 }),
}),
Type.Object({
action: Type.Literal("send_gcode_line"),
/** The most powerful variant by far; be careful about exposing it to a UI. */
line: Type.String(),
}),
]);
export const EdgeClientMessageSchema = Type.Union([
Type.Object({
type: Type.Literal("hello"),
gateway_id: Type.String(),
farm_id: Type.String(),
token: Type.String(),
}),
Type.Object({
type: Type.Literal("heartbeat"),
/** Monotonic per connection, restarting at 1 on reconnect. */
seq: Type.Integer({ minimum: 0 }),
}),
Type.Object({
type: Type.Literal("telemetry"),
/**
* A batch, not a message per printer: a farm has one uplink and many
* printers, and a coherent picture of the farm at an instant is worth
* more here than the lowest possible per-machine latency.
*/
printers: Type.Array(PrinterTelemetrySchema),
}),
Type.Object({
type: Type.Literal("command_result"),
command_id: Type.String(),
ok: Type.Boolean(),
/** Only when `ok` is false. A message for an operator, not a code. */
error: Maybe(Type.String()),
}),
Type.Object({
type: Type.Literal("frame_batch"),
printer_id: Type.String(),
print_job_id: Maybe(Type.String({ format: "uuid" })),
frames: Type.Array(Type.Object({
object_key: Type.String(),
captured_at: Type.String({ format: "date-time" }),
})),
}),
]);
export const EdgeServerMessageSchema = Type.Union([
Type.Object({ type: Type.Literal("hello_ack"), session_id: Type.String() }),
Type.Object({ type: Type.Literal("hello_error"), message: Type.String() }),
Type.Object({ type: Type.Literal("heartbeat_ack"), seq: Type.Integer({ minimum: 0 }) }),
Type.Object({
type: Type.Literal("command"),
command_id: Type.String(),
/** The gateway's own printer id - the backend translates on the way out. */
printer_id: Type.String(),
command: PrinterCommandSchema,
}),
]);
-1
View File
@@ -1 +0,0 @@
export * from "./edge";
-10
View File
@@ -1,10 +0,0 @@
// The old Material/PrintParameters/PrintJob/Template mirrors were removed
// (2026-09) when schemas/continuum/v1/ was restructured into
// material/printer/vendor - see that directory and its README. Add a
// hand-written TypeBox schema here per file under schemas/continuum/v1/
// once something actually consumes it (continuum-backend's validators,
// most likely), same convention as before.
// The /ws/edge/v1 gateway uplink protocol - the first thing to actually need
// a TypeBox mirror here, consumed by continuum-backend's edge WebSocket
// route. Mirrors schemas/continuum/v1/edge/.
export * from "./edge";
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"