#!/usr/bin/env node import path from "node:path"; import { readdir, readFile, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; const skillRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const schemaDir = path.join(skillRoot, "schemas"); const outputPath = path.join(skillRoot, "scripts", "lib", "extraction-validators.generated.mjs"); const files = (await readdir(schemaDir)).filter((name) => name.endsWith(".schema.json") && name !== "common.schema.json").sort(); const schemas = {}; for (const file of files) schemas[file.replace(/\.schema\.json$/, "")] = JSON.parse(await readFile(path.join(schemaDir, file), "utf8")); const runtime = `// Generated by scripts/generate-extraction-validators.mjs. Do not hand-edit.\nconst SCHEMAS = ${JSON.stringify(schemas, null, 2)};\n\nexport const GENERATED_SCHEMA_VERSION = "0083.1";\nexport const GENERATED_SCHEMA_KINDS = Object.freeze(Object.keys(SCHEMAS).sort());\n\nexport function validateGeneratedArtifact(value, kind) {\n const schema = SCHEMAS[kind];\n if (!schema) return { ok: false, errors: [\`unsupported generated schema: \${kind}\`] };\n const errors = [];\n validate(value, schema, "$", errors, false);\n return { ok: errors.length === 0, errors };\n}\n\nfunction validate(value, schema, at, errors, conditionOnly) {\n if (!schema || typeof schema !== "object") return true;\n const before = errors.length;\n if (schema.const !== undefined && !same(value, schema.const)) errors.push(\`\${at}: must equal \${JSON.stringify(schema.const)}\`);\n if (schema.enum && !schema.enum.some((entry) => same(value, entry))) errors.push(\`\${at}: must be one of \${schema.enum.map(String).join(", ")}\`);\n if (schema.type && !matchesType(value, schema.type)) errors.push(\`\${at}: must be type \${Array.isArray(schema.type) ? schema.type.join("|") : schema.type}\`);\n if (typeof value === "string") {\n if (schema.minLength !== undefined && value.length < schema.minLength) errors.push(\`\${at}: must have length >= \${schema.minLength}\`);\n if (schema.pattern && !new RegExp(schema.pattern, "u").test(value)) errors.push(\`\${at}: must match \${schema.pattern}\`);\n if (schema.format === "uri") { try { new URL(value); } catch { errors.push(\`\${at}: must be a URI\`); } }\n }\n if (typeof value === "number") {\n if (schema.minimum !== undefined && value < schema.minimum) errors.push(\`\${at}: must be >= \${schema.minimum}\`);\n if (schema.maximum !== undefined && value > schema.maximum) errors.push(\`\${at}: must be <= \${schema.maximum}\`);\n }\n if (Array.isArray(value)) {\n if (schema.minItems !== undefined && value.length < schema.minItems) errors.push(\`\${at}: must contain >= \${schema.minItems} items\`);\n if (schema.maxItems !== undefined && value.length > schema.maxItems) errors.push(\`\${at}: must contain <= \${schema.maxItems} items\`);\n if (schema.uniqueItems && new Set(value.map((entry) => JSON.stringify(entry))).size !== value.length) errors.push(\`\${at}: items must be unique\`);\n if (schema.items) value.forEach((entry, index) => validate(entry, schema.items, \`\${at}[\${index}]\`, errors, conditionOnly));\n }\n if (value && typeof value === "object" && !Array.isArray(value)) {\n for (const required of schema.required || []) if (!(required in value)) errors.push(\`\${at}.\${required}: is required\`);\n const properties = schema.properties || {};\n for (const [key, entry] of Object.entries(value)) {\n if (properties[key]) validate(entry, properties[key], \`\${at}.\${key}\`, errors, conditionOnly);\n else if (schema.additionalProperties === false) errors.push(\`\${at}.\${key}: additional property is not allowed\`);\n else if (schema.additionalProperties && typeof schema.additionalProperties === "object") validate(entry, schema.additionalProperties, \`\${at}.\${key}\`, errors, conditionOnly);\n if (schema.propertyNames?.pattern && !new RegExp(schema.propertyNames.pattern, "u").test(key)) errors.push(\`\${at}.\${key}: property name must match \${schema.propertyNames.pattern}\`);\n }\n }\n for (const child of schema.allOf || []) validate(value, child, at, errors, conditionOnly);\n if (schema.if) {\n const probe = [];\n validate(value, schema.if, at, probe, true);\n if (!probe.length && schema.then) validate(value, schema.then, at, errors, conditionOnly);\n }\n if (conditionOnly && errors.length > before) return false;\n return errors.length === before;\n}\n\nfunction matchesType(value, expected) {\n const types = Array.isArray(expected) ? expected : [expected];\n return types.some((type) => type === "null" ? value === null\n : type === "array" ? Array.isArray(value)\n : type === "object" ? Boolean(value) && typeof value === "object" && !Array.isArray(value)\n : type === "integer" ? Number.isInteger(value)\n : type === "number" ? typeof value === "number" && Number.isFinite(value)\n : typeof value === type);\n}\n\nfunction same(left, right) { return JSON.stringify(left) === JSON.stringify(right); }\n`; if (process.argv.includes("--check")) { let existing = ""; try { existing = await readFile(outputPath, "utf8"); } catch {} if (existing !== runtime) { console.error("Generated extraction validators are stale. Run node skills/wix-headless-replatform/scripts/generate-extraction-validators.mjs"); process.exit(1); } } else { await writeFile(outputPath, runtime); console.log(outputPath); }