From 7f97934282a59ca4d14de152408770699542eca3 Mon Sep 17 00:00:00 2001 From: Will Estes Date: Sat, 5 Sep 2026 20:07:05 -0400 Subject: [PATCH] Give temporal commemorations at Lauds/Vespers a real Ant+V/R+collect bundle getDayCollects previously rendered a commemorated Sunday/privileged feria (calendar/commemorations.ts's ordinary-sunday/privileged-sunday/ privileged-feria-minor branches) as a bare, unlabeled collect -- no antiphon, no versicle, no indication it was even a commemoration. Only sanctoral commemorations had gotten the fuller bundle treatment (2026-08-30 pass). Adds temporalCommemorationPart, reusing the temporal day's own benedictus/magnificat antiphon and weekday-keyed versicle (the same content it would use had it won outright), falling back to a labeled bare collect or honest "missing" block when no antiphon is authored for that id. Verified live for Nov 1, 2026 (All Saints on a Sunday): both Lauds and Vespers now show "Commemoration of The 22nd Sunday after Trinity" with real antiphon and versicle instead of a silent bare prayer. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LGsATZvfnpQ81HQuoF5JuL --- src/hours/lauds.ts | 2 +- src/hours/resolve-common.ts | 43 ++++++++++++++++++- src/hours/vespers.ts | 2 +- .../vigil-of-christmas-sunday.test.ts | 29 +++++++------ tests/hours/lauds-psalmody-overrides.test.ts | 19 ++++---- 5 files changed, 69 insertions(+), 26 deletions(-) diff --git a/src/hours/lauds.ts b/src/hours/lauds.ts index 6e64edf..a3ddb3f 100644 --- a/src/hours/lauds.ts +++ b/src/hours/lauds.ts @@ -373,7 +373,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] { case 'preces': return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }]; case 'day-collects': - return getDayCollects(day); + return getDayCollects(day, 'lauds'); case 'suffrages': return resolveSuffrages(day, part.omitOnDouble, 'lauds'); case 'versicle': diff --git a/src/hours/resolve-common.ts b/src/hours/resolve-common.ts index d200b72..d7b81a2 100644 --- a/src/hours/resolve-common.ts +++ b/src/hours/resolve-common.ts @@ -6,6 +6,7 @@ import { getSaintRecord } from '../calendar/feasts'; import { resolveActiveOctave, activeOctavesFor, octaveGoverningPrivilegedDay, isAtLeast, isSundayOrFeast } from '../calendar'; import { getTemporalFeastRecord } from '../calendar/temporal-feasts'; import { isInTriduum } from '../calendar/temporal'; +import { temporalLabel } from '../calendar/day-label'; import { splitAntiphon, isDoubleOrHigher } from './antiphon'; import { getLaudsSaintOverride } from './lauds-psalmody-overrides'; import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml'; @@ -720,6 +721,44 @@ function octaveCommemorationPart(commemoration: Extract, + day: LiturgicalDay, + hour: 'lauds' | 'vespers', +): ResolvedPart { + const label = `Commemoration of ${temporalLabel(day).en}`; + const collect = toResolvedText(getTemporalProper(`${commemoration.id}-collect`)); + const antiphon = toResolvedText( + getTemporalProper(`${commemoration.id}-${hour === 'lauds' ? 'benedictus' : 'magnificat'}-antiphon`), + ); + if (antiphon.status.la !== 'missing' || antiphon.status.en !== 'missing') { + const versicleId = + hour === 'lauds' + ? `lauds-versicle-${day.weekday}` + : day.weekday === 'saturday' + ? 'vespers-versicle-saturday' + : 'vespers-versicle-sunday-ferial'; + const versicle = resolveCommon(versicleId); + return { kind: 'preces', text: combineCommemorationBundle(antiphon, versicle, collect), label }; + } + if (collect.status.la !== 'missing' || collect.status.en !== 'missing') { + return { kind: 'preces', text: collect, label }; + } + return { kind: 'preces', text: { text: {}, status: { la: 'missing', en: 'missing' } }, label }; +} + /** * The day's own collect, plus one more per commemoration (calendar/ * types.ts's LiturgicalDay.commemorations) — Lauds/Vespers say all of @@ -753,7 +792,7 @@ function octaveCommemorationPart(commemoration: Extract { expect(day.commemorations).toEqual([{ kind: 'temporal', id: 'advent-4' }]); }); - it("Lauds carries both collects (Vigil's own, then Advent 4's commemorated) and the Vigil's own proper antiphons/Benedictus antiphon", () => { + it("Lauds carries the Vigil's own collect plus Advent 4's commemorated Ant+V/R+collect bundle, and the Vigil's own proper antiphons/Benedictus antiphon", () => { const lauds = resolveOrdo('lauds', '2023-12-24'); - const collects = lauds.parts.filter((p) => p.kind === 'prayer'); - expect(collects).toHaveLength(2); - for (const c of collects) { - if (c.kind !== 'prayer') continue; - expect(c.text.status.la).toBe('verified'); - expect(c.text.status.en).toBe('verified'); + const ownCollect = lauds.parts.find((p) => p.kind === 'prayer'); + expect(ownCollect?.kind).toBe('prayer'); + if (ownCollect?.kind === 'prayer') { + expect(ownCollect.text.status.la).toBe('verified'); + expect(ownCollect.text.status.en).toBe('verified'); + expect(ownCollect.text.text.la).toContain('Deus, qui nos redemptiónis nostræ'); + } + + // Advent 4's own real standing gets the fuller Ant+V/R+collect + // commemoration bundle (see resolve-common.ts's temporalCommemorationPart), + // not a bare collect -- same treatment a sanctoral commemoration gets. + const commemoration = lauds.parts.find((p) => p.kind === 'preces' && p.label?.startsWith('Commemoration of')); + expect(commemoration?.kind).toBe('preces'); + if (commemoration?.kind === 'preces') { + expect(commemoration.text.text.la).toContain('Excita, quǽsumus, Dómine'); } - expect(collects[0]!.kind === 'prayer' ? collects[0]!.text.text.la : undefined).toContain( - 'Deus, qui nos redemptiónis nostræ', - ); - expect(collects[1]!.kind === 'prayer' ? collects[1]!.text.text.la : undefined).toContain( - 'Excita, quǽsumus, Dómine', - ); const benedictus = lauds.parts.find((p) => p.kind === 'canticle' && p.canticleId === 'benedictus'); expect(benedictus?.kind).toBe('canticle'); diff --git a/tests/hours/lauds-psalmody-overrides.test.ts b/tests/hours/lauds-psalmody-overrides.test.ts index 14969d3..cc153a0 100644 --- a/tests/hours/lauds-psalmody-overrides.test.ts +++ b/tests/hours/lauds-psalmody-overrides.test.ts @@ -98,18 +98,19 @@ describe('Lauds psalmody override (duplex-majus+ sanctoral, and Marian Saturday) 'He hath made us', ); - // One extra collect (the commemorated Sunday's own, still an - // unlabeled 'prayer' -- a temporal commemoration, not a sanctoral - // one) plus the Simplex saint already commemorated under that Sunday - // before Christ the King displaced it too, now a labeled 'preces' - // block rather than a third bare 'prayer' -- see - // tests/calendar/christ-the-king.test.ts. + // The day's own single collect (Christ the King's) plus two labeled + // 'preces' commemoration bundles: the commemorated Sunday's own (a + // temporal commemoration, now the fuller Ant+V/R+collect bundle since + // post-pentecost-22 has a real Benedictus antiphon authored -- see + // resolve-common.ts's temporalCommemorationPart) and the Simplex saint + // already commemorated under that Sunday before Christ the King + // displaced it too -- see tests/calendar/christ-the-king.test.ts. const collects = ordo.parts.filter((p) => p.kind === 'prayer'); - expect(collects.length).toBe(2); - const sanctoralCommemoration = ordo.parts.find( + expect(collects.length).toBe(1); + const commemorations = ordo.parts.filter( (p) => p.kind === 'preces' && p.label?.startsWith('Commemoration of'), ); - expect(sanctoralCommemoration).toBeDefined(); + expect(commemorations.length).toBe(2); const SUFFRAGE_LABELS = new Set([ 'Of the Holy Cross',