Files
zalbot/scripts/validate-spec.ts
Kaysser Kayyali b884a13d98
Some checks failed
tests / Unit tests (Node 22) (push) Failing after 28s
fix wizard for new stories
2026-06-20 06:52:19 +00:00

91 lines
3.3 KiB
TypeScript

// Validate a draft encounter spec against the engine's real EncounterSpecSchema.
// This is the acceptance test for the mardonar-encounter authoring skill: it runs
// the SAME code path (`loadSpec` → `EncounterSpecSchema.parse`) that `/encounter start`
// uses, so a spec that passes here boots a session there.
//
// Usage (run from the engine repo root):
// npx tsx scripts/validate-spec.ts <slug> # resolves ./specs/<slug>.yaml
// npx tsx scripts/validate-spec.ts ./path/to/draft.yaml
//
// Exit 0 + a one-line summary on success.
// Exit 1 + field-named Zod issues on failure.
// Exit 2 on usage error.
//
// NOTE: importing src/spec/loader.js pulls src/config.js, which requires the
// engine's .env (DISCORD_TOKEN / DISCORD_CLIENT_ID) at module load — that's
// expected when running inside the engine repo.
import { resolve } from 'node:path';
import { readFileSync, existsSync } from 'node:fs';
import { load } from 'js-yaml';
import { EncounterSpecSchema, loadSpec } from '../src/spec/loader.js';
const arg = process.argv[2];
if (!arg) {
console.error('Usage: npx tsx scripts/validate-spec.ts <slug-or-path>');
console.error(' <slug> resolves to ./specs/<slug>.yaml');
console.error(' <path> a .yaml/.yml file path');
process.exit(2);
}
// Decide: file path vs. slug. A path contains a slash or a .yaml/.yml extension.
const isPath = arg.includes('/') || /\.ya?ml$/i.test(arg);
let label: string;
let parsed: unknown;
try {
if (isPath) {
const p = resolve(arg);
if (!existsSync(p)) {
console.error(`FAIL ${p}`);
console.error(' file not found');
process.exit(1);
}
label = p;
parsed = load(readFileSync(p, 'utf-8'));
} else {
// slug → loadSpec (the real /encounter start code path). loadSpec reads
// config.SPECS_DIR/<slug>.yaml and runs EncounterSpecSchema.parse.
const spec = loadSpec(arg);
label = `${arg} (via loadSpec)`;
// loadSpec already parsed; re-run safeParse only to get structured issues if
// we ever want them. On success, report and exit.
reportOk(label, spec);
process.exit(0);
}
} catch (err) {
// loadSpec / readFileSync / load can all throw. Zod errors come from parse below.
console.error(`FAIL ${arg}`);
console.error(` ${String((err as Error)?.message ?? err)}`);
process.exit(1);
}
// path branch: run safeParse for field-named issues.
const result = EncounterSpecSchema.safeParse(parsed);
if (result.success) {
reportOk(label, result.data);
process.exit(0);
}
console.error(`FAIL ${label}`);
for (const issue of result.error.issues) {
const path = issue.path.length ? issue.path.join('.') : '(root)';
console.error(` - ${path}: ${issue.message}`);
}
process.exit(1);
function reportOk(label: string, s: {
encounterId: string;
title: string;
npcs: unknown[];
goals: { primary: unknown[]; secondary: unknown[] };
tools?: unknown[];
xpReward?: number;
}): void {
const tools = s.tools ? `${s.tools.length}` : 'default';
const xp = s.xpReward !== undefined ? `${s.xpReward}` : 'none';
console.log(`OK ${label}`);
console.log(` encounterId=${s.encounterId} title="${s.title}"`);
console.log(` npcs=${s.npcs.length} primaryGoals=${s.goals.primary.length} secondaryGoals=${s.goals.secondary.length} tools=${tools} xpReward=${xp}`);
}