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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGsATZvfnpQ81HQuoF5JuL
This commit is contained in:
2026-09-05 20:08:21 -04:00
parent 3e7af0b0a4
commit a9e7d717b8
2 changed files with 114 additions and 3 deletions
@@ -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."
+41 -3
View File
@@ -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;