From a9e7d717b8772d21c9ebc65deabbf24b2bb96e96 Mon Sep 17 00:00:00 2001 From: Will Estes Date: Sat, 5 Sep 2026 20:08:21 -0400 Subject: [PATCH] Let a sanctoral winner's proper Matins psalmody win over a Sunday's resolveOrdo's Sunday-nocturn branch fired unconditionally whenever day.weekday === 'sunday', regardless of who actually won the day -- silently making any saint's own proper Matins psalmody override (getMatinsSaintOverride) unreachable on a Sunday, even when authored. The source's own "Psalmi Dominica" rubric line reads as if the plain Sunday psalm cursus always applies whenever a Duplex+ feast wins over a Sunday, but live-querying All Saints (Nov 1, 2026, a Sunday that year) shows it actually has its own distinct proper psalms (Ps 1/4/8, 14/23/ 31, 33/60/96, each its own antiphon) -- not the plain Sunday Nocturn psalms this app was falling back to. Checks for a sanctoral proper override before the Sunday branch now. That override's own Nocturn 3 is psalms, not the 3-OT-canticle shape every other saint/category file uses -- added renderOverrideNocturn3 to dispatch on which field is actually populated instead of assuming canticles unconditionally (which produced a silently empty antiphon and no content). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LGsATZvfnpQ81HQuoF5JuL --- .../saints/all-saints.yml | 73 +++++++++++++++++++ src/hours/matins.ts | 44 ++++++++++- 2 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/data/hours/matins-psalmody-overrides/saints/all-saints.yml diff --git a/src/data/hours/matins-psalmody-overrides/saints/all-saints.yml b/src/data/hours/matins-psalmody-overrides/saints/all-saints.yml new file mode 100644 index 0000000..31becc1 --- /dev/null +++ b/src/data/hours/matins-psalmody-overrides/saints/all-saints.yml @@ -0,0 +1,73 @@ +# Live-verified against Divinum Officium (Monastic Tridentinum 1617, +# command=prayMatutinum, 2026-11-01, clean year). The source file's own +# [Rule] line ("Psalmi Dominica") reads as if the plain Sunday psalm +# cursus applies, but that's misleading -- the live render shows a real, +# distinct proper set (Ps 1/4/8, 14/23/31, 33/60/96, each psalm its own +# All-Saints-themed antiphon, not one antiphon shared per nocturn), not +# the plain Sunday Nocturn psalms this app was falling back to before +# this file existed. Versicles read directly from the live render, +# immediately before each Nocturn's own Pater Noster. +id: all-saints +nocturn1: + groups: + - psalms: [1] + antiphon: + la: "Novit Dóminus * viam iustórum, qui in lege ejus meditántur die ac nocte." + en: "The Lord knoweth the way of the righteous, * who in His Law do meditate day and night." + - psalms: [4] + antiphon: + la: "Mirificávit * Dóminus Sanctos suos, et exaudívit eos clamántes ad se." + en: "The Lord hath set apart for Himself them that are holy, * and when they called, He hath heard them." + - psalms: [8] + antiphon: + la: "Admirábile est * nomen tuum, Dómine: quia glória et honóre coronásti Sanctos tuos, et constituísti eos super ópera mánuum tuárum." + en: "How excellent is thy Name, O Lord, Who hast crowned thy Saints with glory and honour, * and madest them to have dominion over the works of thy hands." + versicle: + v: + la: "Lætámini in Dómino, et exsultáte justi." + en: "Be glad in the Lord, and rejoice, ye righteous." + r: + la: "Et gloriámini omnes recti corde." + en: "And glory, all ye that are upright in heart." +nocturn2: + groups: + - psalms: [14] + antiphon: + la: "Dómine, * qui operáti sunt justítiam, habitábunt in tabernáculo tuo, et requiéscent in monte sancto tuo." + en: "They that work righteousness, * O Lord, even they, shall abide in thy tabernacle, and dwell in thy holy Hill." + - psalms: [23] + antiphon: + la: "Hæc est generátio * quæréntium Dóminum, quæréntium fáciem Dei Iacob." + en: "This is the generation of them that seek the Lord, * that seek the face of the God of Jacob." + - psalms: [31] + antiphon: + la: "Lætámini in Dómino, * et exsultáte justi, et gloriámini omnes recti corde." + en: "Be glad in the Lord, and rejoice, ye righteous, * and shout for joy, all ye that are upright in heart." + versicle: + v: + la: "Exsúltent justi in conspéctu Dei." + en: "Let the righteous rejoice before God." + r: + la: "Et delecténtur in lætítia." + en: "Yea, let them exceedingly rejoice." +nocturn3: + groups: + - psalms: [33] + antiphon: + la: "Timéte Dóminum * omnes Sancti ejus, quóniam nihil deest timéntibus eum: ecce óculi Dómini super justos, et aures ejus ad preces eórum." + en: "O fear the Lord, all ye His Saints, * for there is no want to them that fear Him. Behold, the eyes of the Lord are upon the righteous, and His ears are open unto their cry." + - psalms: [60] + antiphon: + la: "Dómine, spes Sanctórum * et turris fortitúdinis eórum, dedísti hereditátem timéntibus nomen tuum, et habitábunt in tabernáculo tuo in sǽcula." + en: "O Lord, Thou hast been a shelter for thy Saints, * a strong tower from the enemy. Thou hast given the heritage to those that fear thy Name, and they shall abide in thy tabernacle for ever." + - psalms: [96] + antiphon: + la: "Qui dilígitis Dóminum, * lætámini in Dómino, et confitémini memóriæ sanctitátis ejus." + en: "Ye that love the Lord, rejoice in the Lord, * and give thanks at the remembrance of His holiness." + versicle: + v: + la: "Justi autem in perpétuum vivent." + en: "The righteous live for evermore." + r: + la: "Et apud Dóminum est merces eórum." + en: "Their reward also is with the Lord." diff --git a/src/hours/matins.ts b/src/hours/matins.ts index 4c99ea3..3413214 100644 --- a/src/hours/matins.ts +++ b/src/hours/matins.ts @@ -389,6 +389,18 @@ function sundayCanticleNocturn(group: SundayNocturn, day: LiturgicalDay): Resolv return parts; } +/** A per-feast/per-Common Nocturn 3 override is normally the 3-OT- + * canticle shape (`sundayCanticleNocturn`) every existing saint/category + * file uses — but a real exception exists (All Saints, live-verified + * 2026-09-05: Ps 33/60/96, each its own antiphon, not canticles at all). + * Dispatches on which field the override actually populated rather than + * assuming canticles unconditionally, so a `groups`-shaped Nocturn 3 + * renders as real psalms via `sundayPsalmNocturn` instead of silently + * producing an empty antiphon and no content. */ +function renderOverrideNocturn3(group: SundayNocturn, day: LiturgicalDay): ResolvedPart[] { + return group.groups ? sundayPsalmNocturn(group, day) : sundayCanticleNocturn(group, day); +} + /** The Matins hymn — a duplex-majus+ saint's or eligible named temporal * feast's own proper hymn (`matins-hymn-${overrideId}`, via the same * getOfficeOverrideId eligibility Lauds/Vespers' own resolveOffice uses), @@ -1082,7 +1094,33 @@ export function resolveOrdo(date: string): ResolvedOrdo { { kind: 'hymn', text: resolveMatinsHymn(day) }, ]; - if (threeNocturns && (day.weekday === 'sunday' || hasTemporalNamedOverride)) { + // A sanctoral winner's own proper Matins psalmody takes precedence even + // on a Sunday — live-verified 2026-09-05 (All Saints, Nov 1, a Sunday + // that year): the source's own "Psalmi Dominica" rubric line reads as + // if the plain Sunday psalm cursus always applies whenever a Duplex+ + // feast wins over a Sunday, but that's misleading — the live render + // shows All Saints' own distinct proper psalms (Ps 1/4/8, 14/23/31, + // 33/60/96), not the plain Sunday Nocturn psalms. Checked ahead of the + // `day.weekday === 'sunday'` branch below, which otherwise unconditionally + // wins regardless of who the day's winner is — silently making any + // saint's own proper Matins psalmody unreachable on a Sunday. Proper + // only (not Common-category) for now: no live-verified case yet + // confirms a Common's own Matins psalmody should win over a Sunday's, + // unlike a saint's own genuinely proper content. + const sundaySanctoralProperOverride = day.weekday === 'sunday' && winner.kind === 'sanctoral' ? getMatinsSaintOverride(winner.id) : undefined; + if (sundaySanctoralProperOverride) { + parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` }); + parts.push(...sundayPsalmNocturn(sundaySanctoralProperOverride.nocturn1, day)); + parts.push(...(nocturn1Readings ?? [])); + parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[1]}` }); + parts.push(...sundayPsalmNocturn(sundaySanctoralProperOverride.nocturn2, day)); + parts.push(...(nocturn2Readings ?? [])); + parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[2]}` }); + parts.push(...renderOverrideNocturn3(sundaySanctoralProperOverride.nocturn3, day)); + parts.push(...(nocturn3Readings ?? [])); + parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') }); + parts.push({ kind: 'versicle', text: resolveCommon('te-decet-laus') }); + } else if (threeNocturns && (day.weekday === 'sunday' || hasTemporalNamedOverride)) { parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` }); parts.push(...sundayPsalmNocturn(sundayNocturnFor('nocturn1', day, temporalId), day)); parts.push(...(nocturn1Readings ?? [])); @@ -1121,7 +1159,7 @@ export function resolveOrdo(date: string): ResolvedOrdo { parts.push(...sundayPsalmNocturn(properOverride.nocturn2, day)); parts.push(...(nocturn2Readings ?? [])); parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[2]}` }); - parts.push(...sundayCanticleNocturn(properOverride.nocturn3, day)); + parts.push(...renderOverrideNocturn3(properOverride.nocturn3, day)); parts.push(...(nocturn3Readings ?? [])); } else if (isAboveDuplex && commonOverride) { parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` }); @@ -1131,7 +1169,7 @@ export function resolveOrdo(date: string): ResolvedOrdo { parts.push(...sundayPsalmNocturn(commonOverride.nocturn2, day)); parts.push(...(nocturn2Readings ?? [])); parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[2]}` }); - parts.push(...sundayCanticleNocturn(commonOverride.nocturn3, day)); + parts.push(...renderOverrideNocturn3(commonOverride.nocturn3, day)); parts.push(...(nocturn3Readings ?? [])); } else { const commonId = !isAboveDuplex && winner.kind === 'sanctoral' ? getSaintRecord(winner.id)?.minorHoursCommon : undefined;