5f4cf33776
Deploy / deploy (push) Successful in 51s
Prime/Terce/Sext/None had no per-feast override mechanism at all before this -- their psalm antiphon always fell back to the plain weekday default and their chapter was always a single fixed per-annum/per-weekday text, regardless of what was actually being celebrated, even on a saint's own actual feast day (not just during an octave). Confirmed live (Monastic Tridentinum 1617) that St. Lawrence has his own real antiphon and chapter at every one of these hours, both on his own day and throughout his octave. New shared mechanism in hours/resolve-common.ts: getMinorHourOverrideId (duplex-majus+ threshold, kept in sync with Lauds' own so it's one shared backlog), resolveMinorHourAntiphon, resolveMinorHourChapter -- all resolveOfficeWinner-aware, so an octave day gets the same override as the feast's own actual day. Prime additionally needed isSundayOrFeastOffice (its capitulum/Preces Sunday-vs-ferial choice) made octave-aware -- live-verified Prime's capitulum stays the Sunday/feast form throughout the octave too, so no new file was needed there, just reaching the existing one correctly. Also fills in Lauds' own still-missing Lawrence office bundle (capitulum/responsory/hymn/versicle) -- flagged in TODO.md as worth fixing "even before tackling the other 32" and still not done until now. Authored real content for St. Lawrence across all five hours from live queries. The same ~31-saint backlog (TODO.md) now applies uniformly across Lauds and the four hours fixed here. Also documents a real, found-not-fixed gap: when two octaves overlap with no temporal standing to yield to (St. Lawrence's and the Assumption's genuinely overlap every Aug 16-17), this app currently picks whichever started first, which happens to match the live engine on Aug 17 (Lawrence's own elevated final "Octave Day" outranks the Assumption's ordinary octave-day strength) but only by coincidence, not a real rank comparison -- proposed design written up in TODO.md pending a decision on scope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
124 lines
6.1 KiB
TypeScript
124 lines
6.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { resolveOrdo } from '../../src/hours';
|
|
|
|
// Clean per-annum dates, verified against Divinum Officium (Monastic
|
|
// Tridentinum 1617) to have no sanctoral override — see
|
|
// data/psalter-distribution.yml and data/hours/*-antiphons.yml for how
|
|
// each was chosen.
|
|
const SUNDAY = '2026-08-09';
|
|
const MONDAY = '2026-08-31';
|
|
const TUESDAY = '2026-10-13';
|
|
|
|
const LITTLE_HOURS = ['terce', 'sext', 'none'] as const;
|
|
const PSALMS_BY_HOUR: Record<(typeof LITTLE_HOURS)[number], [number, number, number]> = {
|
|
terce: [119, 120, 121],
|
|
sext: [122, 123, 124],
|
|
none: [125, 126, 127],
|
|
};
|
|
|
|
describe.each(LITTLE_HOURS)('resolveOrdo(%s, ...)', (hourId) => {
|
|
it('is implemented', () => {
|
|
const ordo = resolveOrdo(hourId, TUESDAY);
|
|
expect(ordo.notImplemented).toBeUndefined();
|
|
expect(ordo.hourId).toBe(hourId);
|
|
});
|
|
|
|
it('resolves three psalms from the gradual set on a plain weekday, framed by one antiphon', () => {
|
|
const ordo = resolveOrdo(hourId, TUESDAY);
|
|
const psalmParts = ordo.parts.filter((p) => p.kind === 'psalm');
|
|
expect(psalmParts.map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined))).toEqual(
|
|
PSALMS_BY_HOUR[hourId],
|
|
);
|
|
expect(psalmParts[0]?.kind === 'psalm' ? psalmParts[0].antiphon?.status.en : undefined).toBe('verified');
|
|
// No closing repeat of the antiphon — confirmed against the real engine.
|
|
expect(psalmParts[1]?.kind === 'psalm' ? psalmParts[1].antiphon : undefined).toBeUndefined();
|
|
expect(psalmParts[2]?.kind === 'psalm' ? psalmParts[2].antiphon : undefined).toBeUndefined();
|
|
expect(ordo.parts.some((p) => p.kind === 'antiphon')).toBe(false);
|
|
});
|
|
|
|
it('slices Psalm 118 by section on Sunday and Monday instead of the gradual set', () => {
|
|
const sunday = resolveOrdo(hourId, SUNDAY);
|
|
const sundayPsalms = sunday.parts.filter((p) => p.kind === 'psalm');
|
|
expect(sundayPsalms.every((p) => (p.kind === 'psalm' ? p.psalmNumber === 118 : false))).toBe(true);
|
|
|
|
const monday = resolveOrdo(hourId, MONDAY);
|
|
const mondayPsalms = monday.parts.filter((p) => p.kind === 'psalm');
|
|
expect(mondayPsalms.every((p) => (p.kind === 'psalm' ? p.psalmNumber === 118 : false))).toBe(true);
|
|
|
|
// Different sections, not a repeat of Sunday's.
|
|
const sundayVerseNs = sundayPsalms[0]?.kind === 'psalm' ? sundayPsalms[0].verses.map((v) => v.n) : [];
|
|
const mondayVerseNs = mondayPsalms[0]?.kind === 'psalm' ? mondayPsalms[0].verses.map((v) => v.n) : [];
|
|
expect(sundayVerseNs).not.toEqual(mondayVerseNs);
|
|
});
|
|
|
|
it('resolves the hymn as fully self-contained, verified text (no seasonal doxology append)', () => {
|
|
const ordo = resolveOrdo(hourId, TUESDAY);
|
|
const hymn = ordo.parts.find((p) => p.kind === 'hymn');
|
|
expect(hymn && hymn.kind === 'hymn' ? hymn.text.status.en : undefined).toBe('verified');
|
|
expect(hymn && hymn.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Amen.');
|
|
});
|
|
|
|
it('resolves the per-annum capitulum with its citation', () => {
|
|
const ordo = resolveOrdo(hourId, TUESDAY);
|
|
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
|
|
expect(chapter && chapter.kind === 'chapter' ? chapter.text.status.en : undefined).toBe('verified');
|
|
expect(chapter && chapter.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBeDefined();
|
|
});
|
|
|
|
it("resolves the day's own collect as the closing prayer", () => {
|
|
const ordo = resolveOrdo(hourId, SUNDAY); // post-pentecost-11 — real, verified collect
|
|
const prayer = ordo.parts.find((p) => p.kind === 'prayer');
|
|
expect(prayer && prayer.kind === 'prayer' ? prayer.text.status.en : undefined).toBe('verified');
|
|
expect(prayer && prayer.kind === 'prayer' ? prayer.text.text.en : undefined).toContain('Let us pray.');
|
|
});
|
|
|
|
it('ends with the shared Little Hour conclusion, identical across all three hours', () => {
|
|
const ordo = resolveOrdo(hourId, TUESDAY);
|
|
const conclusion = ordo.parts.find((p) => p.kind === 'preces' && p.label === 'Conclusion');
|
|
expect(conclusion && conclusion.kind === 'preces' ? conclusion.text.text.en : undefined).toContain(
|
|
'Let us bless the Lord.',
|
|
);
|
|
});
|
|
|
|
// 2026-08-10: St. Lawrence's own day (Duplex II. classis). Live-verified
|
|
// each hour has its own real proper antiphon and chapter (2026-08),
|
|
// previously falling back all the way to the plain weekday default with
|
|
// no override mechanism at all.
|
|
const LAWRENCE_ANTIPHON_INCIPIT: Record<(typeof LITTLE_HOURS)[number], string> = {
|
|
terce: 'Lauréntius',
|
|
sext: 'Adhǽsit',
|
|
none: 'Beátus Lauréntius',
|
|
};
|
|
const LAWRENCE_CHAPTER_CITATION: Record<(typeof LITTLE_HOURS)[number], string> = {
|
|
terce: '2 Cor 9:6',
|
|
sext: '2 Cor 9:7',
|
|
none: '2 Cor 9:8-9',
|
|
};
|
|
|
|
it("resolves St. Lawrence's own proper antiphon and chapter on his own feast day", () => {
|
|
const ordo = resolveOrdo(hourId, '2026-08-10');
|
|
const antiphon = ordo.parts.find((p) => p.kind === 'psalm')?.antiphon;
|
|
expect(antiphon?.text.la).toContain(LAWRENCE_ANTIPHON_INCIPIT[hourId]);
|
|
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
|
|
expect(chapter && chapter.kind === 'chapter' ? chapter.text.citation?.la : undefined).toBe(
|
|
LAWRENCE_CHAPTER_CITATION[hourId],
|
|
);
|
|
});
|
|
|
|
it("resolves the same proper antiphon and chapter on day 3 of St. Lawrence's own octave, not the plain weekday default -- the concrete bug this override mechanism (resolveOfficeWinner) was built to fix", () => {
|
|
const ordo = resolveOrdo(hourId, '2026-08-12');
|
|
const antiphon = ordo.parts.find((p) => p.kind === 'psalm')?.antiphon;
|
|
expect(antiphon?.text.la).toContain(LAWRENCE_ANTIPHON_INCIPIT[hourId]);
|
|
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
|
|
expect(chapter && chapter.kind === 'chapter' ? chapter.text.citation?.la : undefined).toBe(
|
|
LAWRENCE_CHAPTER_CITATION[hourId],
|
|
);
|
|
});
|
|
|
|
it('still falls back to the plain weekday default on an ordinary day with no eligible override', () => {
|
|
const ordo = resolveOrdo(hourId, TUESDAY);
|
|
const antiphon = ordo.parts.find((p) => p.kind === 'psalm')?.antiphon;
|
|
expect(antiphon?.text.la).not.toContain(LAWRENCE_ANTIPHON_INCIPIT[hourId]);
|
|
});
|
|
});
|