Files
ponomar-pwa/scripts/validate-great-canon.ts
T
2026-08-01 02:43:24 +03:00

135 lines
4.5 KiB
TypeScript

import { readFileSync, existsSync, readdirSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
const __dirname = new URL('.', import.meta.url).pathname;
const CANONS_DIR = join(__dirname, '..', 'static', 'data', 'shared', 'services', 'canons', 'great-canon');
interface GreatCanonOde {
ode: number;
irmos: string;
troparia: string[];
trinityTroparion: string;
theotokion: string;
kontakion?: string;
saintTroparia?: string[];
}
interface GreatCanonPart {
part: number;
title: string;
odes: GreatCanonOde[];
}
interface ServiceNode {
type: 'HEADER' | 'TEXT';
value: string;
}
const ODE_SLAV = ['', 'а҃', 'в҃', 'г҃', 'д҃', 'є҃', 'ѕ҃', 'з҃', 'и҃', 'ѳ҃'];
function simulateLoadGreatCanon(data: GreatCanonPart): ServiceNode[] {
const nodes: ServiceNode[] = [];
nodes.push({ type: 'HEADER', value: 'Вели́кїй канѡ́нъ, гла́съ ѕ҃.' });
nodes.push({ type: 'HEADER', value: 'Творе́нїе ст҃а́гѡ ѻ҆тца̀ на́шегѡ а҆ндре́а кри́тскагѡ.' });
for (const ode of data.odes) {
const slavNum = ODE_SLAV[ode.ode] ?? String(ode.ode);
nodes.push({ type: 'HEADER', value: `Пѣ́снь ${slavNum}.` });
if (ode.irmos) {
nodes.push({ type: 'HEADER', value: 'І҆рмо́съ:' });
nodes.push({ type: 'TEXT', value: ode.irmos });
}
const refrain = 'Поми́лꙋй мѧ̀, бж҃е, поми́лꙋй мѧ̀.';
for (const trop of ode.troparia) {
nodes.push({ type: 'TEXT', value: `${trop}\nПрипѣ́въ: ${refrain}` });
}
if (ode.trinityTroparion) {
nodes.push({ type: 'HEADER', value: 'Сла́ва, трⷪ҇ченъ:' });
nodes.push({ type: 'TEXT', value: ode.trinityTroparion });
}
if (ode.theotokion) {
nodes.push({ type: 'HEADER', value: 'И҆ ны́нѣ, бг҃оро́диченъ:' });
nodes.push({ type: 'TEXT', value: ode.theotokion });
}
if (ode.saintTroparia && ode.saintTroparia.length > 0) {
for (const saintTrop of ode.saintTroparia) {
nodes.push({ type: 'TEXT', value: saintTrop });
}
}
if (ode.kontakion) {
nodes.push({ type: 'HEADER', value: 'Конда́къ, гла́съ ѕ҃:' });
nodes.push({ type: 'TEXT', value: ode.kontakion });
}
}
return nodes;
}
let errors = 0;
let warnings = 0;
function err(msg: string) { console.error(` ERROR: ${msg}`); errors++; }
function warn(msg: string) { console.warn(` WARN: ${msg}`); warnings++; }
for (const entry of readdirSync(CANONS_DIR).sort()) {
const fullPath = join(CANONS_DIR, entry);
if (!entry.endsWith('.json')) continue;
console.log(`\n=== ${entry} ===`);
let data: GreatCanonPart;
try {
data = JSON.parse(readFileSync(fullPath, 'utf-8'));
} catch (e) {
err(`${entry}: failed to parse: ${(e as Error).message}`);
continue;
}
if (!data.part || typeof data.part !== 'number' || data.part < 1 || data.part > 4)
err(`${entry}: invalid part ${data.part}`);
if (!Array.isArray(data.odes))
err(`${entry}: odes must be an array`);
if (data.odes.length !== 9)
err(`${entry}: expected 9 odes, got ${data.odes.length}`);
for (let oi = 0; oi < data.odes.length; oi++) {
const ode = data.odes[oi];
const pfx = `${entry}/odes[${oi}]/ode${ode.ode}`;
if (!ode.ode || typeof ode.ode !== 'number' || ode.ode < 1 || ode.ode > 9)
err(`${pfx}: invalid ode number`);
if (oi > 0 && data.odes[oi - 1].ode >= ode.ode)
err(`${pfx}: ode not in ascending order`);
if (typeof ode.irmos !== 'string' || ode.irmos.length < 10)
err(`${pfx}: irmos missing or too short (${ode.irmos.length} chars)`);
if (!Array.isArray(ode.troparia) || ode.troparia.length === 0)
err(`${pfx}: troparia missing or empty`);
if (typeof ode.trinityTroparion !== 'string' || ode.trinityTroparion.length < 10)
err(`${pfx}: trinityTroparion missing or too short`);
if (typeof ode.theotokion !== 'string' || ode.theotokion.length < 10)
err(`${pfx}: theotokion missing or too short`);
// Ode 6 should have kontakion
if (ode.ode === 6 && (!ode.kontakion || ode.kontakion.length < 10))
warn(`${pfx}: Ode 6 missing kontakion`);
}
const nodes = simulateLoadGreatCanon(data);
if (nodes.length === 0) {
err(`${entry}: zero ServiceNodes produced`);
continue;
}
console.log(` ${data.odes.length} odes, ${nodes.length} ServiceNodes`);
}
console.log(`\n${errors + warnings} issues (${errors} errors, ${warnings} warnings)`);
process.exit(errors > 0 ? 1 : 0);