136 lines
5.5 KiB
TypeScript
136 lines
5.5 KiB
TypeScript
/**
|
||
* Validate Holy Week service data files: every full.json must be
|
||
* well-formed, and referenced templates must exist.
|
||
* Run with: npx tsx scripts/validate-holy-week.ts
|
||
*/
|
||
|
||
import { readFileSync, existsSync } from 'fs';
|
||
import { join } from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
|
||
const __dirname = new URL('.', import.meta.url).pathname;
|
||
const ROOT = join(__dirname, '..', 'static', 'data', 'shared', 'services');
|
||
|
||
interface ServiceNode { type: string; value: string }
|
||
|
||
const SERVICES: Record<string, number> = {
|
||
'palm-sunday': -7,
|
||
'great-monday': -6,
|
||
'great-tuesday': -5,
|
||
'great-wednesday': -4,
|
||
'great-thursday': -3,
|
||
'passion-gospels': -3,
|
||
'royal-hours-friday': -2,
|
||
'lamentations': -1,
|
||
'burial-vespers': -2,
|
||
'saturday-hours': -1,
|
||
'saturday-vespers-liturgy': -1,
|
||
'saturday-midnight': -1,
|
||
'pascha': 0,
|
||
'bright-monday': 1,
|
||
'bright-tuesday': 2,
|
||
'bright-wednesday': 3,
|
||
'bright-thursday': 4,
|
||
'bright-friday': 5,
|
||
'bright-saturday': 6,
|
||
};
|
||
|
||
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 [dir, nday] of Object.entries(SERVICES)) {
|
||
const path = join(ROOT, dir, 'full.json');
|
||
if (!existsSync(path)) { err(`${dir}: missing full.json`); continue; }
|
||
|
||
let nodes: ServiceNode[];
|
||
try {
|
||
nodes = JSON.parse(readFileSync(path, 'utf-8'));
|
||
} catch (e) {
|
||
err(`${dir}: failed to parse: ${(e as Error).message}`);
|
||
continue;
|
||
}
|
||
|
||
if (!Array.isArray(nodes) || nodes.length < 20) {
|
||
err(`${dir}: too few nodes (${Array.isArray(nodes) ? nodes.length : 'n/a'})`);
|
||
continue;
|
||
}
|
||
|
||
let headers = 0, empty = 0;
|
||
for (let i = 0; i < nodes.length; i++) {
|
||
const n = nodes[i];
|
||
if (n.type !== 'HEADER' && n.type !== 'TEXT') { err(`${dir}[${i}]: invalid type '${n.type}'`); continue; }
|
||
if (!n.value || typeof n.value !== 'string') { err(`${dir}[${i}]: missing value`); continue; }
|
||
if (n.value.length < 3) empty++;
|
||
if (n.type === 'HEADER') headers++;
|
||
}
|
||
if (headers === 0) err(`${dir}: no HEADER nodes`);
|
||
if (empty > 0) warn(`${dir}: ${empty} suspiciously short values`);
|
||
|
||
// Service-specific presence checks
|
||
const all = nodes.map(n => n.value);
|
||
if (dir === 'passion-gospels') {
|
||
const gospels = all.filter(v => /є҆ѵⷢ҇лїе/i.test(v) && /завѣ́та/i.test(v));
|
||
if (gospels.length !== 12) err(`${dir}: expected 12 gospel markers, got ${gospels.length}`);
|
||
const dolg = all.filter(v => v.includes('долготерпѣ́нїю')).length;
|
||
if (dolg < 11) warn(`${dir}: only ${dolg} 'Слава долготерпению' responses`);
|
||
}
|
||
if (dir === 'lamentations') {
|
||
const statii = all.filter(v => v.startsWith('Статїѧ̀')).length;
|
||
if (statii !== 3) err(`${dir}: expected 3 статии, got ${statii}`);
|
||
}
|
||
if (dir === 'royal-hours-friday') {
|
||
// Hours are marked inline: "Ча́съ пе́рвый", "Посе́мъ пое́мъ тре́тїй ча́съ", etc.
|
||
const hours = all.filter(v => /ча́съ/.test(v));
|
||
if (hours.length < 4) warn(`${dir}: only ${hours.length} hour markers`);
|
||
}
|
||
if (dir === 'saturday-vespers-liturgy') {
|
||
// 15 paroemia readings + special hymns
|
||
if (!all.some(v => v.includes('Да молчи́тъ') || v.includes('молчи́тъ всѧ́каѧ пло́ть')))
|
||
err(`${dir}: missing 'Да молчит всякая плоть'`);
|
||
if (!all.some(v => v.includes('рыда́й')))
|
||
err(`${dir}: missing 'Не рыдай Мене Мати'`);
|
||
if (!all.some(v => /[ЕЄ]҆?ли́цы/.test(v) || v.includes('крести́стесѧ')))
|
||
warn(`${dir}: 'Елицы во Христа крестистеся' not found`);
|
||
if (!all.some(v => v.includes('Воскрⷭ҇нѝ')))
|
||
err(`${dir}: missing 'Воскресни Боже'`);
|
||
}
|
||
if (dir === 'saturday-midnight') {
|
||
const odes = all.filter(v => /Пѣ́снь [а-ѳ]/.test(v)).length;
|
||
if (odes < 8) warn(`${dir}: only ${odes} canon odes`);
|
||
}
|
||
if (dir === 'burial-vespers') {
|
||
// Burial Vespers should include the "Благообразный Иосиф" troparion
|
||
if (!all.some(v => v.includes('і҆ѡ́сиф') || v.includes('И҆ѡ́сиф')))
|
||
warn(`${dir}: troparion of St. Joseph not found`);
|
||
}
|
||
if (dir === 'pascha') {
|
||
if (!all.some(v => v.includes('златоꙋ́стагѡ') || v.includes('Златоꙋ́стагѡ')))
|
||
err(`${dir}: missing Catechetical homily of St. John Chrysostom`);
|
||
if (!all.some(v => v.includes('а҆́ртоса') || v.includes('артоса')))
|
||
warn(`${dir}: Artos blessing not found`);
|
||
const antiphons = all.filter(v => v.startsWith('А҆нтїфѡ́нъ')).length;
|
||
if (antiphons < 3) err(`${dir}: expected 3 antiphons, got ${antiphons}`);
|
||
}
|
||
|
||
console.log(`${dir}: ${nodes.length} nodes, ${headers} headers (${nday < 0 ? 'nday ' + nday : 'fixed'})`);
|
||
}
|
||
|
||
// Verify templates referenced by services exist
|
||
const templates = [
|
||
'PalmSunday', 'GreatMonday', 'GreatTuesday', 'GreatWednesday', 'GreatThursday',
|
||
'PassionGospels', 'RoyalHoursFriday', 'Lamentations',
|
||
'BurialVespers', 'SaturdayHours', 'SaturdayVespersLiturgy', 'SaturdayMidnight',
|
||
'Pascha', 'BrightMonday', 'BrightTuesday', 'BrightWednesday', 'BrightThursday',
|
||
'BrightFriday', 'BrightSaturday',
|
||
];
|
||
for (const t of templates) {
|
||
if (!existsSync(join(ROOT, 'templates', `${t}.json`))) {
|
||
err(`missing template: ${t}.json`);
|
||
}
|
||
}
|
||
|
||
console.log(`\n${errors + warnings} issues (${errors} errors, ${warnings} warnings)`);
|
||
process.exit(errors > 0 ? 1 : 0);
|