#!/usr/bin/env node /** * Validate a question JSON file against the bundled schema. * * Usage: * node validate.mjs * echo '' | node validate.mjs - * * Exit codes: 0 = valid, 1 = errors found, 2 = usage error. */ import { readFileSync, existsSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const SCHEMA_PATH = resolve(__dirname, "../schema/question.schema.json"); const VALID_TYPES = [ "single_choice", "multiple_choice", "true_false", "fill_blank", "short_answer", "code_reading", "scenario", ]; const ID_RE = /^(sc|mc|tf|fb|sa|cr|sn)-\d{3}$/; function loadJson(path) { const raw = path === "-" ? readFileSync(0, "utf-8") : readFileSync(path, "utf-8"); return JSON.parse(raw); } function validate(data) { const errors = []; const requiredTop = ["topic", "type", "schema_version", "questions"]; for (const field of requiredTop) { if (!(field in data)) errors.push(`Missing required field: ${field}`); } if (!VALID_TYPES.includes(data.type)) errors.push(`Invalid type: ${data.type}`); if (data.schema_version !== "1.0.0") errors.push(`schema_version must be '1.0.0', got '${data.schema_version}'`); for (const [i, q] of (data.questions || []).entries()) { const p = `questions[${i}]`; for (const field of ["id", "type", "difficulty", "tags", "question", "explanation"]) { if (!(field in q)) errors.push(`${p}: missing '${field}'`); } if (!VALID_TYPES.includes(q.type)) errors.push(`${p}: invalid type '${q.type}'`); if (q.difficulty !== undefined && !(q.difficulty >= 1 && q.difficulty <= 5)) errors.push(`${p}: difficulty must be 1-5`); if (q.id !== undefined && !ID_RE.test(q.id)) errors.push(`${p}: id '${q.id}' doesn't match pattern {type}-{000}`); } return errors; } function main() { if (process.argv.length !== 3) { console.error(`Usage: node ${process.argv[1]} `); process.exit(2); } if (!existsSync(SCHEMA_PATH)) { console.error(`❌ Schema not found: ${SCHEMA_PATH}`); process.exit(2); } const data = loadJson(process.argv[2]); const errors = validate(data); if (errors.length) { console.log(`❌ ${errors.length} error(s):`); for (const e of errors) console.log(` • ${e}`); process.exit(1); } else { console.log(`✅ Valid — ${(data.questions || []).length} question(s)`); process.exit(0); } } main();