Build All Souls' Day (Nov 2) as a reusable Office of the Dead

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
This commit is contained in:
2026-09-05 22:35:39 -04:00
parent 5cd4fb69a9
commit a462464f7c
12 changed files with 764 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
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');
});
});