commit af7c34c3c944660cd902009b395e9ab6d560b05c
parent baa444159ba09c567401f2df981ba35d1cd8f714
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 23 Sep 2026 19:23:17 -0400
settings: zod seams for workers, channelPriority and autoQueue, kept out of client bundles
one-core phase 3 slice 4a, commit 2. `workersSchema`, `channelPrioritySchema`
and `autoQueueSchema` wrap the existing sanitizers (`sanitizeWorkers`,
`sanitizeChannelPriority(value: unknown)`, `sanitizeAutoQueue`), which keep
their homes, names and signatures.
The seams live in lib/settingsFieldSchemas.ts rather than beside each
sanitizer: workers.ts, channelPriority.ts and (via jobs/autoQueuePolicy)
autoQueueSchema.ts are value-imported by "use client" forms, and a zod
import there would ship zod to the browser. Commit 1's seam in
autoQueueSchema.ts moves here for the same reason.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 63 insertions(+), 14 deletions(-)
diff --git a/common/jobs/autoQueuePolicy.ts b/common/jobs/autoQueuePolicy.ts
@@ -40,7 +40,6 @@ export {
AUTO_QUEUE_MODES,
AUTO_QUEUE_ORDERS,
AUTO_QUEUE_MAX_WORKERS_MAX,
- autoQueueSchema,
defaultAutoQueue,
defaultAutoQueuePolicy,
sanitizeAutoQueue,
diff --git a/common/lib/autoQueueSchema.ts b/common/lib/autoQueueSchema.ts
@@ -16,8 +16,13 @@
//
// It never throws. Every function is total over `unknown`, because its input is
// a file an operator may have hand-edited and a settings read may not fail.
+//
+// NO ZOD HERE, deliberately. Four `"use client"` forms import constants from
+// this module through `jobs/autoQueuePolicy` (LadderRung reads
+// AUTO_QUEUE_MODES), so anything this file imports can land in a browser
+// bundle. The zod field seam that wraps `sanitizeAutoQueue` lives in
+// `lib/settingsFieldSchemas.ts`, which only server code imports.
-import { z } from "zod";
import {
type AutoQueueGroup,
type AutoQueueKind,
@@ -277,15 +282,3 @@ export function sanitizeAutoQueue(value: unknown): AutoQueueSettings {
) as AutoQueueSettings;
}
-// THE LANE POLICIES AS ONE SCHEMA FIELD.
-//
-// `sanitizeAutoQueue` above IS the parser — it is total, it never throws, and it
-// is what fifteen hundred lines of tests pin. The schema is the seam that lets
-// `lib/settingsSchema.ts` compose it with the other thirty fields without
-// knowing any of that: zod supplies the plumbing (a key, a strip, a `.catch`),
-// the sanitizer supplies the arithmetic. Same shape as `workersSchema` and
-// `channelPrioritySchema`.
-export const autoQueueSchema = z
- .unknown()
- .catch(undefined)
- .transform((value): AutoQueueSettings => sanitizeAutoQueue(value));
diff --git a/common/lib/settingsFieldSchemas.ts b/common/lib/settingsFieldSchemas.ts
@@ -0,0 +1,57 @@
+// THE ZOD SEAMS FOR THE THREE SANITIZERS THAT LIVE OUTSIDE settings.ts.
+//
+// `settings.json` has three blocks whose parsers have homes of their own and
+// callers of their own: `workers` (lib/workers.ts — the pool and the settings
+// form), `channelPriority` (lib/channelPriority.ts — storageWatch and the
+// channels actions call `sanitizeChannelPriority(value: unknown)` directly) and
+// `autoQueue` (lib/autoQueueSchema.ts — the picker's tests pin it). Each keeps
+// its home and its signature. What this file adds is ONE schema per block, so
+// `lib/settingsSchema.ts` can compose them with the other twenty-eight fields
+// the same way it composes everything: zod supplies the plumbing (the key, the
+// strip of unknown siblings, a `.catch` that makes a field total), the existing
+// sanitizer supplies the arithmetic. Nothing was re-implemented, so nothing
+// could drift.
+//
+// WHY A SEPARATE FILE, and not a `workersSchema` export beside each sanitizer:
+// all three homes are imported AS VALUES by `"use client"` forms
+// (WorkersConfigForm, ChannelTierSelect, LadderRung, …). A module-level
+// `import { z } from "zod"` there would put zod in a browser bundle for no
+// reason. Only server code imports this file — lib/settingsSchema.ts, which
+// itself is only reached through lib/settings.ts (node:fs at module scope).
+
+import { z } from "zod";
+import { sanitizeWorkers, type Worker } from "./workers";
+import {
+ sanitizeChannelPriority,
+ type ChannelPriority,
+} from "./channelPriority";
+import { sanitizeAutoQueue } from "./autoQueueSchema";
+import type { AutoQueueSettings } from "./autoQueueTypes";
+
+// A settings FIELD: any JSON value in, a legal value out, never a throw.
+//
+// `.catch(undefined)` is today's try-and-continue: whatever zod could object
+// to becomes `undefined`, which every coercion below treats as "absent → the
+// default". `z.unknown()` accepts a missing key, and zod 4 still runs the
+// transform for it and emits the key — so an absent field is DEFAULTED, not
+// dropped, exactly as `defaults()` used to fill it.
+//
+// There is deliberately no `.default()` anywhere: a default applies only to
+// `undefined`, and every coercion here already decides that case itself — the
+// place "a zero limit is a hold" lives is inside the clamp, not in a fallback
+// zod would apply around it.
+export function settingsField<T>(coerce: (value: unknown) => T) {
+ return z.unknown().catch(undefined).transform((value): T => coerce(value));
+}
+
+export const workersSchema = settingsField(
+ (value): Worker[] => sanitizeWorkers(value),
+);
+
+export const channelPrioritySchema = settingsField(
+ (value): ChannelPriority => sanitizeChannelPriority(value),
+);
+
+export const autoQueueSchema = settingsField(
+ (value): AutoQueueSettings => sanitizeAutoQueue(value),
+);