91 lines
3.3 KiB
TypeScript
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}`);
|
|
} |