// Forms preserve the JSON schema's values and defaults. Complex unions and free-form // objects use a JSON editor instead of guessing which action variant the user meant. import { esc } from "./util.js"; const LONG = /(prompt|text|code|content|description|instruction|message|answer|transcript|latex|query|body|script|reasoning|patch)/i; const own = (o, k) => Object.prototype.hasOwnProperty.call(o || {}, k); const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v); export function resolveSchema(schema, root = schema, seen = new Set()) { if (!object(schema)) return {}; if (!schema.$ref) return schema; const ref = schema.$ref; if (typeof ref !== "string" || !ref.startsWith("#/") || seen.has(ref) || seen.size > 20) return {}; let target = root; for (const part of ref.slice(2).split("/").map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"))) { if (!own(target, part)) return {}; target = target[part]; } const { $ref, ...rest } = schema; return { ...resolveSchema(target, root, new Set([...seen, ref])), ...rest }; } function kind(raw, root) { const p = resolveSchema(raw, root); if (own(p, "const")) return { t: "const", p }; if (Array.isArray(p.enum)) return { t: "enum", p }; const alternatives = p.anyOf || p.oneOf; if (Array.isArray(alternatives)) { const choices = alternatives.map((x) => resolveSchema(x, root)).filter((x) => x.type !== "null"); if (choices.length === 1 && choices.length < alternatives.length) { const { anyOf, oneOf, ...rest } = p; return { ...kind({ ...choices[0], ...rest }, root), nullable: true }; } return { t: "json", p }; } if (p.allOf) return { t: "json", p }; const ts = Array.isArray(p.type) ? p.type.filter((x) => x !== "null") : [p.type]; const t = ts.length === 1 ? ts[0] : undefined; return { t: t === "string" ? (LONG.test(p.title || "") ? "text" : "str") : t === "integer" ? "int" : t === "number" ? "num" : t === "boolean" ? "bool" : "json", p, nullable: Array.isArray(p.type) && p.type.includes("null") }; } // Deliberately a bounded, conservative subset of JSON Schema: server validation // remains authoritative for formats, patterns and custom framework constraints. export function validateValue(value, schema, path = "Input", root = schema, depth = 0) { if (depth > 24) return; if (schema === false) throw new Error(`${path} is not allowed`); const p = resolveSchema(schema, root); const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b); if (own(p, "const") && !equal(value, p.const)) throw new Error(`${path} must be ${JSON.stringify(p.const)}`); if (p.enum && !p.enum.some((x) => equal(x, value))) throw new Error(`${path} must be one of the listed choices`); for (const option of p.allOf || []) validateValue(value, option, path, root, depth + 1); const alternatives = p.anyOf || p.oneOf; if (alternatives) { const matches = alternatives.filter((x) => { try { validateValue(value, x, path, root, depth + 1); return true; } catch { return false; } }); if (!matches.length || (p.oneOf && matches.length !== 1)) throw new Error(`${path} must match ${p.oneOf ? "exactly one" : "one"} of the allowed schemas`); } const types = Array.isArray(p.type) ? p.type : p.type ? [p.type] : []; const is = (t) => t === "null" ? value === null : t === "object" ? object(value) : t === "array" ? Array.isArray(value) : t === "integer" ? Number.isSafeInteger(value) : t === "number" ? typeof value === "number" && Number.isFinite(value) : typeof value === t; if (types.length && !types.some(is)) throw new Error(`${path} must be ${types.join(" or ")}`); if (typeof value === "number") { if (!Number.isFinite(value)) throw new Error(`${path} must be a finite number`); for (const [key, bad, word] of [["minimum", (n) => value < n, "at least"], ["maximum", (n) => value > n, "at most"], ["exclusiveMinimum", (n) => value <= n, "greater than"], ["exclusiveMaximum", (n) => value >= n, "less than"]]) { if (typeof p[key] === "number" && bad(p[key])) throw new Error(`${path} must be ${word} ${p[key]}`); } } if (typeof value === "string") { const length = [...value].length; if (p.minLength != null && length < p.minLength) throw new Error(`${path} needs at least ${p.minLength} characters`); if (p.maxLength != null && length > p.maxLength) throw new Error(`${path} allows at most ${p.maxLength} characters`); } if (Array.isArray(value)) { if (p.minItems != null && value.length < p.minItems) throw new Error(`${path} needs at least ${p.minItems} items`); if (p.maxItems != null && value.length > p.maxItems) throw new Error(`${path} allows at most ${p.maxItems} items`); if (p.items) value.forEach((v, i) => validateValue(v, p.items, `${path}[${i}]`, root, depth + 1)); } else if (object(value)) { for (const key of p.required || []) if (!own(value, key)) throw new Error(`${path}.${key} is required`); for (const [key, v] of Object.entries(value)) { if (own(p.properties, key)) validateValue(v, p.properties[key], `${path}.${key}`, root, depth + 1); else if (p.additionalProperties === false) throw new Error(`${path}.${key} is not an allowed field`); else if (object(p.additionalProperties)) validateValue(v, p.additionalProperties, `${path}.${key}`, root, depth + 1); } } } export function schemaForm(schema, { prefix = "sf", values = {}, skip = ["metadata"] } = {}) { const root = schema || {}, s = resolveSchema(root, root); const required = new Set(s.required || []); const props = Object.entries(s.properties || {}).filter(([k]) => !skip.includes(k) || required.has(k)); const idBase = esc(prefix); if (s.anyOf || s.oneOf || s.allOf || !props.length && s.additionalProperties !== false) { const initial = Object.keys(values).length ? values : s.default; return ``; } if (!props.length) return '
No inputs.
'; return `