- tools/mutation-check.mjs with tools/mutants.json: thirteen mutants against the Module 12 TypeScript reference (the eight of Module 13's first table, plus Armor before redirect, conditions at parse time, no defender priority, and the summon seam's noun check); all killed. - tools/run-scenario.mjs with tools/scenario/run.ts: a designer's cards, words and seam words as data, played on the Module 10 TypeScript reference. - course/10-spellgrid/designer-capstone.md and designer-example/ (output.txt is checked against the tool's output in the tools tests). - CI: diff-tracks (300 seeds), mutation-check, verify-levels. - Module 13: the mutation table is the tool's output; differential testing, undo and the solver sections describe the repository as it is. Lesson 10 says what the level proofs prove for levels 6, 8 and 9. - schema.mjs: a value that matches no oneOf branch is reported against the branch its const fields pick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
42 lines
2.5 KiB
JavaScript
42 lines
2.5 KiB
JavaScript
// Minimal JSON-schema subset validator (node built-ins only).
|
|
// Supports: type, required, enum, const, properties, items, minimum, oneOf, anyOf. A value that
|
|
// matches no oneOf branch is reported against the branch its const fields pick, if exactly one.
|
|
const typeOk = (t, v) =>
|
|
t === "array" ? Array.isArray(v)
|
|
: t === "null" ? v === null
|
|
: t === "integer" ? Number.isInteger(v)
|
|
: t === "object" ? v !== null && typeof v === "object" && !Array.isArray(v)
|
|
: typeof v === t;
|
|
|
|
export function validate(schema, value, path = "$") {
|
|
const errs = [];
|
|
if (schema.type) {
|
|
const types = [].concat(schema.type);
|
|
if (!types.some((t) => typeOk(t, value))) return [`${path}: expected ${types.join("|")}`];
|
|
}
|
|
if (schema.enum && !schema.enum.includes(value)) errs.push(`${path}: not in enum ${JSON.stringify(schema.enum)}`);
|
|
if ("const" in schema && value !== schema.const) errs.push(`${path}: expected ${JSON.stringify(schema.const)}`);
|
|
if (schema.minimum !== undefined && typeof value === "number" && value < schema.minimum) errs.push(`${path}: below minimum ${schema.minimum}`);
|
|
if (typeOk("object", value)) {
|
|
for (const k of schema.required || []) if (!(k in value)) errs.push(`${path}: missing "${k}"`);
|
|
for (const [k, s] of Object.entries(schema.properties || {})) if (k in value) errs.push(...validate(s, value[k], `${path}.${k}`));
|
|
}
|
|
if (Array.isArray(value) && schema.items) value.forEach((v, i) => errs.push(...validate(schema.items, v, `${path}[${i}]`)));
|
|
if (schema.anyOf && !schema.anyOf.some((s) => validate(s, value, path).length === 0)) errs.push(`${path}: matches no anyOf branch`);
|
|
if (schema.oneOf) {
|
|
const n = schema.oneOf.filter((s) => validate(s, value, path).length === 0).length;
|
|
// No branch matches: if the value's `const` fields pick out one branch (a clause's "kind",
|
|
// an effect's "type"), say what is wrong with it; that is what the author meant.
|
|
const meant = n === 0 ? schema.oneOf.filter((s) => discriminates(s, value)) : [];
|
|
if (meant.length === 1) errs.push(...validate(meant[0], value, path));
|
|
else if (n !== 1) errs.push(`${path}: matches ${n} oneOf branches (need 1)`);
|
|
}
|
|
return errs;
|
|
}
|
|
|
|
/** Does `value` have every `const` field of the object schema `s`, and does `s` have one? */
|
|
function discriminates(s, value) {
|
|
const consts = Object.entries(s.properties || {}).filter(([, p]) => "const" in p);
|
|
return typeOk("object", value) && consts.length > 0 && consts.every(([k, p]) => value[k] === p.const);
|
|
}
|