#!/usr/bin/env node // validate-kb-file.mjs — the executable create-time-guard (Spor 3 Port 2). // // The gate any generator (generate-skills, kb-update) calls AFTER writing a candidate // KB file and BEFORE committing it. It wraps transform.validateKbFile over one or more // paths and exits NON-ZERO if any file violates the contract — so a regenerated file // that lost its Source, is not born-verified, or dropped its TOC can never ship. This // is what makes "create-guard MUST be in place before Spor 1 corpus-fixing" enforceable: // the generator physically cannot re-introduce the drift the corpus pass is removing. // // Usage: // node scripts/kb-update/validate-kb-file.mjs [ ...] // // Exit code: 0 = all files valid; 1 = at least one violation (or usage error). import { readFileSync, realpathSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { validateKbFile } from './lib/transform.mjs'; /** * Validate a batch of candidate KB files. Pure + dependency-injected (readFile) so it is * unit-testable without disk. A path that cannot be read is reported as invalid (with a * `read-error` marker) rather than throwing — one unreadable file must not crash the gate. * * @param {string[]} paths * @param {(p: string) => string} [readFile] — reader (defaults to readFileSync utf8) * @returns {{ok: boolean, results: Array<{path: string, valid: boolean, missing: string[]}>}} */ export function validatePaths(paths, readFile = (p) => readFileSync(p, 'utf8')) { const results = (paths ?? []).map((path) => { let content; try { content = readFile(path); } catch (err) { return { path, valid: false, missing: [`read-error: ${err.message}`] }; } const { valid, missing } = validateKbFile(content); return { path, valid, missing }; }); return { ok: results.every((r) => r.valid), results }; } function main(argv) { const paths = argv.slice(2); if (paths.length === 0) { process.stderr.write('usage: validate-kb-file.mjs [ ...]\n'); process.exit(1); } const { ok, results } = validatePaths(paths); for (const r of results) { if (r.valid) { process.stdout.write(`OK ${r.path}\n`); } else { process.stdout.write(`FAIL ${r.path} — missing: ${r.missing.join(', ')}\n`); } } process.exit(ok ? 0 : 1); } // Robust direct-invocation check (mirrors detect-courses.mjs): only run main when this // file is the entrypoint, not when imported by a test. const isMain = (() => { try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })(); if (isMain) main(process.argv);