From 58b201dc230e4c22982f678d1f3fbe5a76f46197 Mon Sep 17 00:00:00 2001 From: Will Estes Date: Fri, 4 Sep 2026 15:37:53 -0400 Subject: [PATCH] Build the Paschaltide "Suffragium Paschale" mechanism Throughout Paschaltide (Eastertide/Ascensiontide/Pentecost), two things happen wherever they'd otherwise show: the ordinary 5-suffrage set (Cross/BVM/Joseph/Apostles/Peace) collapses into one alleluia-form "Suffragium Paschale" (Lauds and Vespers each have their own distinct antiphon here, unlike the ordinary suffrages, which share identical text at both hours), and the ferial-Preces litany drops entirely rather than being replaced. Live-verified against the reference source's own [Suffragium] macro directly, byte-identical between Monastic Tridentinum 1617 and Tridentine 1906. Exported isPaschaltide(season) from resolve-common.ts (wraps the pre-existing PASCHALTIDE_SEASONS set). resolveSuffrages gained a third hourId parameter to pick the right antiphon; the Preces omission is a small new check gated on the same isFerialOrVigil predicate that already selects the ferial litany form. Authored lauds-suffrage-paschale.yml and vespers-suffrage- paschale.yml -- English on the Vespers antiphon is this project's own translation (the reference engine leaves it untranslated) and marked draft; everything else verified. Closes the last item in TODO.md's "Mechanism gaps" priority tier. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak --- .../common/lauds-suffrage-paschale.yml | 29 ++++++++++++++++++ .../common/vespers-suffrage-paschale.yml | 30 +++++++++++++++++++ src/hours/lauds.ts | 12 +++++++- src/hours/resolve-common.ts | 27 ++++++++++++++++- src/hours/vespers.ts | 9 +++++- 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/data/propers/common/lauds-suffrage-paschale.yml create mode 100644 src/data/propers/common/vespers-suffrage-paschale.yml diff --git a/src/data/propers/common/lauds-suffrage-paschale.yml b/src/data/propers/common/lauds-suffrage-paschale.yml new file mode 100644 index 0000000..ca08c03 --- /dev/null +++ b/src/data/propers/common/lauds-suffrage-paschale.yml @@ -0,0 +1,29 @@ +# Throughout Paschaltide (Eastertide/Ascensiontide/Pentecost), the whole +# ordinary 5-suffrage set (Cross/BVM/Joseph/Apostles/Peace) collapses into +# this single alleluia-form "Suffragium Paschale" instead — live-verified +# against Divinum Officium (Monastic Tridentinum 1617 and Tridentine 1906, +# byte-identical between the two tracks), Lauds live query, 2026-04-20 (a +# plain Eastertide feria). Distinct antiphon from Vespers' own +# (vespers-suffrage-paschale.yml, "Crucem sanctam súbiit..."); the +# versicle/response and collect are shared between both hours. +id: lauds-suffrage-paschale +text: + la: | + Ant. Crucifíxus surréxit a mórtuis, et redémit nos, allelúja, allelúja. + V. Dícite in natiónibus, allelúja. + R. Quia Dóminus regnávit a ligno, allelúja. + Orémus. + Deus qui pro nobis Fílium tuum Crucis patíbulum subíre voluísti, ut inimíci a nobis expélleres potestátem: concéde nobis fámulis tuis; ut resurrectiónis grátiam consequámur. + Per eúmdem Dóminum nostrum Jesum Christum Fílium tuum, qui tecum vivit et regnat in unitáte Spíritus Sancti, Deus, per ómnia sǽcula sæculórum. + R. Amen. + en: | + Ant. He that was crucified is risen from the dead, and hath redeemed us. Alleluia, alleluia. + V. Say among the heathen, alleluia. + R. That the Lord reigneth from the tree, alleluia. + Let us pray. + O God, who didst send thy Son to suffer death for us upon the Cross, that thou mightest deliver us from the power of the enemy: grant unto us thy servants to be made partakers of his Resurrection. + Through the same Jesus Christ, thy Son, Our Lord, Who liveth and reigneth with thee in the unity of the Holy Ghost, God, world without end. + R. Amen. +status: + la: verified + en: verified diff --git a/src/data/propers/common/vespers-suffrage-paschale.yml b/src/data/propers/common/vespers-suffrage-paschale.yml new file mode 100644 index 0000000..3b59a69 --- /dev/null +++ b/src/data/propers/common/vespers-suffrage-paschale.yml @@ -0,0 +1,30 @@ +# Vespers' own antiphon for the same "Suffragium Paschale" substitution +# described in lauds-suffrage-paschale.yml's own header — shares that +# file's versicle/response and collect, but its own distinct antiphon +# ("Crucem sanctam súbiit..."), live-verified against Divinum Officium +# (Monastic Tridentinum 1617), Vespers live query, 2026-04-20 (a plain +# Eastertide feria). English antiphon translation is this project's own +# (the reference engine's own English-language render leaves this +# particular antiphon untranslated, Latin-only) — status left `draft` +# for the English text specifically pending a better-sourced translation. +id: vespers-suffrage-paschale +text: + la: | + Ant. Crucem sanctam súbiit, qui inférnum confrégit; accínctus est poténtia, surréxit die tértia, allelúja. + V. Dícite in natiónibus, allelúja. + R. Quia Dóminus regnávit a ligno, allelúja. + Orémus. + Deus qui pro nobis Fílium tuum Crucis patíbulum subíre voluísti, ut inimíci a nobis expélleres potestátem: concéde nobis fámulis tuis; ut resurrectiónis grátiam consequámur. + Per eúmdem Dóminum nostrum Jesum Christum Fílium tuum, qui tecum vivit et regnat in unitáte Spíritus Sancti, Deus, per ómnia sǽcula sæculórum. + R. Amen. + en: | + Ant. He bore the holy Cross, who broke the gates of hell; he was girded with power, and rose again on the third day, alleluia. + V. Say among the heathen, alleluia. + R. That the Lord reigneth from the tree, alleluia. + Let us pray. + O God, who didst send thy Son to suffer death for us upon the Cross, that thou mightest deliver us from the power of the enemy: grant unto us thy servants to be made partakers of his Resurrection. + Through the same Jesus Christ, thy Son, Our Lord, Who liveth and reigneth with thee in the unity of the Holy Ghost, God, world without end. + R. Amen. +status: + la: verified + en: draft diff --git a/src/hours/lauds.ts b/src/hours/lauds.ts index 4bf36ad..4dadf4a 100644 --- a/src/hours/lauds.ts +++ b/src/hours/lauds.ts @@ -26,6 +26,7 @@ import { resolveResponsory, resolveOfficeBundle, resolveSuffrages, + isPaschaltide, } from './resolve-common'; import laudsDefinitionData from '../data/hours/lauds.yml'; import laudsAntiphonsData from '../data/hours/lauds-antiphons.yml'; @@ -366,7 +367,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] { case 'day-collects': return getDayCollects(day); case 'suffrages': - return resolveSuffrages(day, part.omitOnDouble); + return resolveSuffrages(day, part.omitOnDouble, 'lauds'); case 'versicle': case 'chapter': case 'responsory': @@ -377,6 +378,15 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] { return [{ kind: 'preces', text: resolveCommon(id), label: getMarianAntiphonLabel(id) }]; } case 'lauds-preces': { + // Throughout Paschaltide, the whole ferial-Preces litany drops + // entirely rather than being replaced by anything — live-verified + // ("Preces Feriales{omittitur}" against both Monastic Tridentinum + // 1617 and Tridentine 1906, a plain Eastertide feria, 2026-04-20). + // Sunday/feast's own short litany is untouched — that's a separate, + // universal collect-opening dialogue, not this ferial-specific one. + if (isFerialOrVigil(day) && isPaschaltide(day.season)) { + return []; + } const id = isFerialOrVigil(day) ? 'lauds-preces-feriales' : 'lauds-short-litany'; return [{ kind: 'preces', text: resolveCommon(id) }]; } diff --git a/src/hours/resolve-common.ts b/src/hours/resolve-common.ts index 36e17d9..d200b72 100644 --- a/src/hours/resolve-common.ts +++ b/src/hours/resolve-common.ts @@ -306,6 +306,15 @@ export function getMinorHourOverrideId(day: LiturgicalDay): string | undefined { */ const PASCHALTIDE_SEASONS = new Set(['eastertide', 'ascensiontide', 'pentecost']); +/** Whether `season` falls within the same broad Paschaltide window + * `PASCHALTIDE_SEASONS` already governs elsewhere in this file (Marian + * antiphon, Common-category text, hymn doxology) — exported so + * hours/lauds.ts's and hours/vespers.ts's own ferial-Preces omission can + * share the identical boundary rather than redeclaring it. */ +export function isPaschaltide(season: string): boolean { + return PASCHALTIDE_SEASONS.has(season); +} + /** * Which seasonal office-content suffix (if any) a day's season maps to — * shared by Lauds' and Vespers' own resolveOffice, both of which fall @@ -789,7 +798,11 @@ const SUFFRAGE_LABELS: Record = { * the caveat on what real practice also suppresses them for that isn't * modeled. */ -export function resolveSuffrages(day: LiturgicalDay, omitOnDouble: boolean | undefined): ResolvedPart[] { +export function resolveSuffrages( + day: LiturgicalDay, + omitOnDouble: boolean | undefined, + hourId: 'lauds' | 'vespers' = 'lauds', +): ResolvedPart[] { // isDoubleOrHigher only ever looks at a sanctoral rank — Christ the // King is modeled as a named temporal winner (see // ALWAYS_OVERRIDE_TEMPORAL_IDS above) with no FeastClass to check at @@ -812,6 +825,18 @@ export function resolveSuffrages(day: LiturgicalDay, omitOnDouble: boolean | und if (omitOnDouble && (isDoubleOrHigher(day.winner) || isChristTheKing || isSeasonallyExcluded || isWithinAnOctave)) { return []; } + // Throughout the rest of Paschaltide, the whole ordinary 5-suffrage set + // below collapses into a single alleluia-form "Suffragium Paschale" — + // live-verified against both Monastic Tridentinum 1617 and Tridentine + // 1906 directly (a plain Eastertide feria, 2026-04-20), byte-identical + // between the two tracks. Checked after the omit-entirely gate above + // (an active octave/Double+/Advent-Christmastide-Passiontide already + // drops suffrages regardless of season, unaffected by this) but before + // the ordinary per-suffrage filtering below, since this replaces that + // filtering outright rather than adjusting it. + if (isPaschaltide(day.season)) { + return [{ kind: 'preces', text: resolveCommon(`${hourId}-suffrage-paschale`) }]; + } // "Of the Holy Cross" is the real *ferial*-office suffrage, sourced // from Tridentine 1906/1910 (a different track than the other four, // which come from Monastic 1617 -- that track has no Cross suffrage diff --git a/src/hours/vespers.ts b/src/hours/vespers.ts index 2328f2c..6047d7b 100644 --- a/src/hours/vespers.ts +++ b/src/hours/vespers.ts @@ -26,6 +26,7 @@ import { resolveResponsory, resolveOfficeBundle, resolveSuffrages, + isPaschaltide, } from './resolve-common'; import vespersDefinitionData from '../data/hours/vespers.yml'; import vespersAntiphonsData from '../data/hours/vespers-antiphons.yml'; @@ -275,13 +276,19 @@ function resolvePart(part: HourPart, day: LiturgicalDay, weekday: Weekday): Reso ]; } case 'vespers-preces': { + // Throughout Paschaltide, the whole ferial-Preces litany drops + // entirely rather than being replaced by anything — see lauds.ts's + // identical 'lauds-preces' case for the live-verification detail. + if (isFerialOrVigil(day) && isPaschaltide(day.season)) { + return []; + } const id = isFerialOrVigil(day) ? 'vespers-preces-feriales' : 'lauds-short-litany'; return [{ kind: 'preces', text: resolveCommon(id) }]; } case 'day-collects': return getDayCollects(day); case 'suffrages': - return resolveSuffrages(day, part.omitOnDouble); + return resolveSuffrages(day, part.omitOnDouble, 'vespers'); case 'preces': return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }]; case 'versicle':