// M75: a minimal schema builder, no dependencies (the daemon, the hub and the hosts import the // protocol and run without pi, so without pi's libraries). A schema is a plain JSON-Schema-shaped // object (type, properties, required, anyOf, const, enum, items, additionalProperties, // minLength, maxLength, pattern, minimum, maximum, description, title) that the doc generator // reads and pi's tools can hand to pi's schema type; a phantom type gives `Infer`. // `message` (not JSON Schema) is the exact error text a hand check says today, for `check`. export interface Meta { description?: string; title?: string; /** What `check` says when this value is wrong. */ message?: string; } /** A JSON-Schema-shaped object; `T` is the TypeScript type of the values it accepts (never present at run time). */ export interface Schema extends Meta { readonly "~type": T; type?: "string" | "number" | "integer" | "boolean" | "null" | "array" | "object"; const?: string | number | boolean; enum?: string[]; anyOf?: Schema[]; items?: Schema; properties?: Record; required?: string[]; additionalProperties?: Schema; minLength?: number; maxLength?: number; pattern?: string; minimum?: number; maximum?: number; minItems?: number; maxItems?: number; /** M76: `check` passes any value (no check today: a bad one is dropped where it is read). */ "x-loose"?: true; /** M76: the limits apply to the value trimmed (the daemon trims it first). */ "x-trim"?: true; /** M76: maxLength is not refused here: the daemon cuts the value ("cut"), or the op refuses it with its own code ("op"). */ "x-max"?: "cut" | "op"; /** M76: a union picked by this field alone; another value fails with the union's message. */ "x-discriminant"?: string; /** M77: `check` asks only that the value be an object (an array too); its fields are the op's to check. */ "x-shallow"?: true; } export type Infer = S extends Schema ? T : never; const OPTIONAL = Symbol("optional"); /** A field that may be left out (see `optional`). */ export interface OptionalField { readonly [OPTIONAL]: true; readonly schema: S; } type Field = Schema | OptionalField; type Fields = Record; type Plain = F extends OptionalField ? S : F; type Flat = { [K in keyof T]: T[K] } & {}; type ObjectOf

= Flat< { [K in keyof P as P[K] extends OptionalField ? never : K]: Infer> } & { [K in keyof P as P[K] extends OptionalField ? K : never]?: Infer> } >; const make = (json: Omit, "~type">, meta: Meta = {}): Schema => { const out: Record = { ...json }; for (const k of ["title", "description", "message"] as const) if (meta[k] !== undefined) out[k] = meta[k]; return out as unknown as Schema; }; export const str = (o: Meta & { min?: number; max?: number; pattern?: string; trim?: boolean; maxBy?: "cut" | "op" } = {}): Schema => make( { type: "string", ...(o.min !== undefined ? { minLength: o.min } : {}), ...(o.max !== undefined ? { maxLength: o.max } : {}), ...(o.pattern !== undefined ? { pattern: o.pattern } : {}), ...(o.trim ? { "x-trim": true as const } : {}), ...(o.maxBy ? { "x-max": o.maxBy } : {}), }, o, ); export const num = (o: Meta & { int?: boolean; min?: number; max?: number } = {}): Schema => make({ type: o.int ? "integer" : "number", ...(o.min !== undefined ? { minimum: o.min } : {}), ...(o.max !== undefined ? { maximum: o.max } : {}) }, o); export const bool = (o: Meta = {}): Schema => make({ type: "boolean" }, o); export const nul = (o: Meta = {}): Schema => make({ type: "null" }, o); export const literal = (v: V, o: Meta = {}): Schema => make({ const: v }, o); /** A string that is one of these. */ export const oneOf = (values: readonly V[], o: Meta = {}): Schema => make({ type: "string", enum: [...values] }, o); /** Plain, or discriminated by a literal field (`check` then reports the matching member's error). */ export const union = (members: M, o: Meta & { discriminant?: string } = {}): Schema> => make({ anyOf: [...members], ...(o.discriminant ? { "x-discriminant": o.discriminant } : {}) }, o); /** M76: the same schema (its type, its doc), not checked: a bad value is dropped where the daemon reads it. */ export const loose = (s: S): S => { const out = { ...s, "x-loose": true } as S; looseOf.set(out, s); return out; }; /** The schema a loose (or shallow, or relaxed) one was made from (the doc names a shared shape by it). */ export const looseOf = new WeakMap(); /** M77: the same schema, checked only for being an object (an array passes, as `typeof` let it), in these words. */ export const shallow = (s: S, o: Meta = {}): S => { const out = { ...s, "x-shallow": true, ...(o.message ? { message: o.message } : {}) } as S; looseOf.set(out, looseOf.get(s) ?? s); return out; }; /** M77: an object schema whose fields are loose, but a literal one (a union's discriminant) and those given in `keep` (in their place). */ export const relax = (s: S, keep: Record = {}, o: Meta = {}): S => { const properties: Record = {}; for (const [k, p] of Object.entries(s.properties ?? {})) properties[k] = keep[k] ?? (p.const !== undefined ? p : loose(p)); const out = { ...s, properties, ...(o.message ? { message: o.message } : {}) } as S; looseOf.set(out, looseOf.get(s) ?? s); return out; }; /** M77: a schema as plain JSON Schema for a tool's parameters: without `message` and the `x-` marks; a maximum the daemon cuts is left out. */ export function jsonSchemaOf(s: Schema): Record { const out: Record = {}; for (const [k, v] of Object.entries(s)) { if (k === "message" || k.startsWith("x-") || k === "~type") continue; if (k === "maxLength" && s["x-max"] === "cut") continue; if (k === "properties") out[k] = Object.fromEntries(Object.entries(v as Record).map(([n, p]) => [n, jsonSchemaOf(p)])); else if (k === "anyOf") out[k] = (v as Schema[]).map(jsonSchemaOf); else if (k === "items" || k === "additionalProperties") out[k] = jsonSchemaOf(v as Schema); else out[k] = v; } return out; } export const nullable = (s: S, o: Meta = {}): Schema | null> => union([s, nul()], o); export const arr = (item: S, o: Meta & { min?: number; max?: number } = {}): Schema[]> => make({ type: "array", items: item, ...(o.min !== undefined ? { minItems: o.min } : {}), ...(o.max !== undefined ? { maxItems: o.max } : {}) }, o); /** A string-keyed record. */ export const rec = (value: S, o: Meta = {}): Schema>> => make({ type: "object", additionalProperties: value }, o); export const optional = (schema: S): OptionalField => ({ [OPTIONAL]: true, schema }); export const obj =

(fields: P, o: Meta = {}): Schema> => { const properties: Record = {}; const required: string[] = []; for (const [k, f] of Object.entries(fields)) { const isOpt = OPTIONAL in f; properties[k] = isOpt ? (f as OptionalField).schema : (f as Schema); if (!isOpt) required.push(k); } return make({ type: "object", properties, ...(required.length ? { required } : {}) }, o); }; /** An object with no fields (`Record`); like `obj({})` to `check`: other keys are ignored. */ export const empty = (o: Meta = {}): Schema> => make({ type: "object", properties: {} }, o); /** A type another module owns (its interface stays there): checked only as an object. */ export const opaque = (title: string, description: string): Schema => make({ type: "object", title }, { description }); /** Anything. */ export const unknown = (o: Meta = {}): Schema => make({}, o); export type CheckResult = { ok: true } | { ok: false; path: string; message: string }; const fail = (s: Schema, path: string, fallback: string): CheckResult => ({ ok: false, path, message: s.message ?? fallback }); const join = (path: string, key: string) => (path ? `${path}.${key}` : key); /** Whether `value` is what `schema` says; the first failure's path ("launch.env", "ids[2]") and message. Unknown fields are ignored. */ export function check(schema: Schema, value: unknown, path = ""): CheckResult { const s = schema; if (s["x-loose"]) return { ok: true }; if (s["x-shallow"]) return typeof value === "object" && value !== null ? { ok: true } : fail(s, path, "must be an object"); if (s.anyOf) return checkAnyOf(s, value, path); if (s.const !== undefined) return value === s.const ? { ok: true } : fail(s, path, `must be ${JSON.stringify(s.const)}`); if (s.enum) return typeof value === "string" && s.enum.includes(value) ? { ok: true } : fail(s, path, `must be one of ${s.enum.join(", ")}`); switch (s.type) { case "string": { if (typeof value !== "string") return fail(s, path, "must be a string"); const v = s["x-trim"] ? value.trim() : value; if (s.minLength !== undefined && v.length < s.minLength) return fail(s, path, `must be at least ${s.minLength} characters`); if (s.maxLength !== undefined && !s["x-max"] && v.length > s.maxLength) return fail(s, path, `must be at most ${s.maxLength} characters`); if (s.pattern !== undefined && !new RegExp(s.pattern).test(v)) return fail(s, path, `must match ${s.pattern}`); return { ok: true }; } case "number": case "integer": { if (typeof value !== "number" || !Number.isFinite(value)) return fail(s, path, "must be a number"); if (s.type === "integer" && !Number.isInteger(value)) return fail(s, path, "must be an integer"); if (s.minimum !== undefined && value < s.minimum) return fail(s, path, `must be at least ${s.minimum}`); if (s.maximum !== undefined && value > s.maximum) return fail(s, path, `must be at most ${s.maximum}`); return { ok: true }; } case "boolean": return typeof value === "boolean" ? { ok: true } : fail(s, path, "must be a boolean"); case "null": return value === null ? { ok: true } : fail(s, path, "must be null"); case "array": { if (!Array.isArray(value)) return fail(s, path, "must be an array"); if (s.minItems !== undefined && value.length < s.minItems) return fail(s, path, `must have at least ${s.minItems} items`); if (s.maxItems !== undefined && value.length > s.maxItems) return fail(s, path, `must have at most ${s.maxItems} items`); for (let i = 0; i < value.length; i++) { const r = check(s.items!, value[i], `${path}[${i}]`); if (!r.ok) return r; } return { ok: true }; } case "object": { if (typeof value !== "object" || value === null || Array.isArray(value)) return fail(s, path, "must be an object"); const o = value as Record; for (const [k, p] of Object.entries(s.properties ?? {})) { if (o[k] === undefined) { if (s.required?.includes(k) && !p["x-loose"]) return fail(p, join(path, k), "is required"); continue; } const r = check(p, o[k], join(path, k)); if (!r.ok) return r; } if (s.additionalProperties) { for (const [k, v] of Object.entries(o)) { const r = check(s.additionalProperties, v, join(path, k)); if (!r.ok) return r; } } return { ok: true }; } default: return { ok: true }; } } function checkAnyOf(s: Schema, value: unknown, path: string): CheckResult { const members = s.anyOf!; const key = s["x-discriminant"]; if (key) { // picked by the discriminant alone (not by any other literal field that happens to match) const v = typeof value === "object" && value !== null ? (value as Record)[key] : undefined; const m = members.find((m) => { const p = m.properties?.[key]; return p !== undefined && v !== undefined && check(p, v).ok; }); return m ? check(m, value, path) : fail(s, path ? `${path}.${key}` : key, `must be one of ${discriminantValues(s, key).join(", ")}`); } const results = members.map((m) => check(m, value, path)); if (results.some((r) => r.ok)) return { ok: true }; // a discriminated member (its literal fields match the value) says what is wrong with it if (typeof value === "object" && value !== null) { const o = value as Record; const i = members.findIndex((m) => m.properties && Object.entries(m.properties).some(([k, p]) => (p.const !== undefined || p.enum) && check(p, o[k]).ok && o[k] !== undefined)); if (i >= 0) return results[i]; } const real = members.filter((m) => m.type !== "null"); if (real.length === 1) return results[members.indexOf(real[0])]; return fail(s, path, "does not match any of the allowed forms"); } /** The values a discriminated union's members take on `key`, in their order. */ export function discriminantValues(union: Schema, key: string): string[] { return (union.anyOf ?? []).flatMap((m) => { const p = m.properties?.[key]; return p?.const !== undefined ? [String(p.const)] : (p?.enum ?? []); }); }