a462464f7c
Adds the full 8-hour Office of the Dead for All Souls' Day: Matins (3 nocturns, Job/Augustine/1 Cor lessons and their own 9 responsories, no Te Deum), Lauds, Vespers (Second Vespers only — Nov 1 evening stays First Vespers of All Saints), and reduced special psalmody at Prime/Terce/Sext/None/Compline. The "Requiem gloria" rubric substitutes "Réquiem ætérnam/Et lux perpétua" for the ordinary Gloria Patri all day, wired centrally into resolve-common.ts's resolveGloriaPatri alongside its existing Triduum omission. Content is split into a reusable src/hours/office-of-the-dead.ts engine (Commune C9's own generic psalmody/lessons/collects, usable for any future occasion this office is said) and a thin src/hours/all-souls.ts supplying only what's genuinely Nov 2-specific (the date trigger, the Augustine + 1 Cor lesson substitution, the Martyrology introduction, and two day-specific collects). No new calendar trigger, route, or per-day override table is added — Nov 2 remains the only caller today. Live-verified hour-by-hour against the reference engine's own rendering, which caught two things the raw static source alone would have gotten wrong: Lauds' "Ps 222" is the Canticle of Ezechias, not a numbered psalm, and Compline's would-be 4th special psalm never actually renders under any selectable rubric version, so it's dropped rather than invented. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGsATZvfnpQ81HQuoF5JuL
40 lines
1.3 KiB
TypeScript
40 lines
1.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { resolveOrdo } from '../../src/hours';
|
||
import { HOUR_IDS } from '../../src/hours/types';
|
||
|
||
function collectStatuses(value: unknown, statuses: string[]): void {
|
||
if (Array.isArray(value)) {
|
||
value.forEach((v) => collectStatuses(v, statuses));
|
||
return;
|
||
}
|
||
if (value && typeof value === 'object') {
|
||
for (const [key, v] of Object.entries(value as Record<string, unknown>)) {
|
||
if (key === 'status' && v && typeof v === 'object') {
|
||
statuses.push(...Object.values(v as Record<string, string>));
|
||
} else {
|
||
collectStatuses(v, statuses);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
describe('All Souls’ Day (Nov 2) — Office of the Dead', () => {
|
||
for (const date of ['2026-11-02', '2027-11-02']) {
|
||
for (const hourId of HOUR_IDS) {
|
||
it(`${hourId} on ${date} has no missing text`, () => {
|
||
const ordo = resolveOrdo(hourId, date);
|
||
const statuses: string[] = [];
|
||
collectStatuses(ordo.parts, statuses);
|
||
expect(statuses.length).toBeGreaterThan(0);
|
||
expect(statuses).not.toContain('missing');
|
||
});
|
||
}
|
||
}
|
||
|
||
it('does not affect Nov 1 First Vespers of All Saints', () => {
|
||
const ordo = resolveOrdo('vespers', '2026-11-01');
|
||
const label = ordo.dayLabel?.en ?? '';
|
||
expect(label.toLowerCase()).not.toContain('all souls');
|
||
});
|
||
});
|