#!/usr/bin/env node // genealogy.pro -> swipl -> src/data/canon.generated.ts // Audit findings warn by default; --strict makes them fatal (CI). import { spawnSync } from 'node:child_process' import { mkdirSync, writeFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' const root = join(dirname(fileURLToPath(import.meta.url)), '..') const outFile = join(root, 'src/data/canon.generated.ts') const strict = process.argv.includes('--strict') const swipl = spawnSync( 'swipl', ['-q', '-g', 'main,halt(0)', '-t', 'halt(1)', join(root, 'scripts/canon_export.pl')], { encoding: 'utf8' }, ) if (swipl.error?.code === 'ENOENT') { console.error( 'generate-canon: `swipl` not found on PATH. The canon data pipeline needs SWI-Prolog:', ) console.error(' macOS: brew install swi-prolog') console.error(' other: https://www.swi-prolog.org/download/stable') process.exit(1) } if (swipl.status !== 0) { if (swipl.stderr) console.error(swipl.stderr.trimEnd()) console.error('generate-canon: swipl failed to export the canon') process.exit(1) } const canon = JSON.parse(swipl.stdout) const flagged = canon.audits.filter((a) => a.findings.length > 0) for (const { audit, findings } of flagged) { for (const finding of findings) console.warn(`audit ${audit}: ${finding}`) } if (flagged.length > 0) { const count = flagged.reduce((n, a) => n + a.findings.length, 0) if (strict) { console.error(`generate-canon: ${count} audit finding(s) (--strict)`) process.exit(1) } console.warn(`generate-canon: ${count} audit finding(s)`) } const quote = (s) => `'${s}'` const bandNames = [...new Set(canon.bands.map((b) => b.band))].sort() const bandByFigure = new Map(canon.bands.map((b) => [b.figure, b.band])) const rows = (items, render) => items.map((item) => ` ${render(item)},`).join('\n') const ts = `// Generated from src/genealogy.pro by scripts/generate-canon.mjs — do not edit. export type FigureId = ${canon.figures.map((f) => ` | ${quote(f)}`).join('\n')} export type Band = ${bandNames.map(quote).join(' | ')} export const figures: readonly FigureId[] = [ ${rows(canon.figures, quote)} ] export const bands: Record = { ${rows(canon.figures, (f) => `${quote(f)}: ${quote(bandByFigure.get(f))}`)} } export const parents: readonly { parent: FigureId; child: FigureId }[] = [ ${rows(canon.parents, (p) => `{ parent: ${quote(p.parent)}, child: ${quote(p.child)} }`)} ] export const couples: readonly { a: FigureId; b: FigureId }[] = [ ${rows(canon.couples, (c) => `{ a: ${quote(c.a)}, b: ${quote(c.b)} }`)} ] export const slayings: readonly { monster: FigureId; slayer: FigureId }[] = [ ${rows(canon.slayings, (s) => `{ monster: ${quote(s.monster)}, slayer: ${quote(s.slayer)} }`)} ] export const labours: readonly { ordinal: number; target: FigureId; source: string }[] = [ ${rows(canon.labours, (l) => `{ ordinal: ${l.ordinal}, target: ${quote(l.target)}, source: ${quote(l.source)} }`)} ] ` mkdirSync(dirname(outFile), { recursive: true }) writeFileSync(outFile, ts) console.log(`generate-canon: ${canon.figures.length} figures -> src/data/canon.generated.ts`)