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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
This commit is contained in:
2026-09-04 15:37:53 -04:00
parent b90789cab3
commit 58b201dc23
5 changed files with 104 additions and 3 deletions
@@ -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
@@ -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
+11 -1
View File
@@ -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) }];
}
+26 -1
View File
@@ -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<string, string> = {
* 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
+8 -1
View File
@@ -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':