13ffce00bf
Deploy / deploy (push) Successful in 1m19s
Real Monastic 1617 has no Preces at the Little Hours, and that was previously documented as intentional. Per this project's "not a reconstruction" design, Prime was recently given Preces in the secular Tridentine form as a deliberate deviation from Monastic — do the same here: the reference engine's ferial minor-hour Preces (Kyrie, Pater Noster, then "Domine Deus virtutum, converte nos..." V/R pairs) is authored as a single shared common id and wired into all three hours after the chapter. By explicit choice this is said every day regardless of rank, mirroring Prime's own "every ferial day" simplification, rather than importing the reference engine's Sunday/feast form, whose sourcing (a V/R block reused from Compline) looks like a data quirk rather than genuine minor-hour content. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXySSbbUh1EhdN1RnAoCaP
132 lines
6.7 KiB
TypeScript
132 lines
6.7 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('resolves a shared Preces (Kyrie/Pater Noster + short litany) after the chapter, identical across all three hours', () => {
|
|
const ordo = resolveOrdo(hourId, TUESDAY);
|
|
const preces = ordo.parts.find((p) => p.kind === 'preces' && p.label === 'Preces');
|
|
expect(preces && preces.kind === 'preces' ? preces.text.status.en : undefined).toBe('verified');
|
|
expect(preces && preces.kind === 'preces' ? preces.text.text.en : undefined).toContain('Lord, have mercy.');
|
|
expect(preces && preces.kind === 'preces' ? preces.text.text.en : undefined).toContain('Our Father');
|
|
});
|
|
|
|
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]);
|
|
});
|
|
});
|