Compare commits

...

2 Commits

Author SHA1 Message Date
will 2a98386bdd Log the Suffragium Paschale mechanism fix in TODO.md
Deploy / deploy (push) Successful in 1m52s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 15:37:59 -04:00
will 58b201dc23 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
2026-09-04 15:37:53 -04:00
6 changed files with 147 additions and 6 deletions
+43 -3
View File
@@ -19,9 +19,8 @@ across every hour and content type in this app.
1. **Mechanism gaps** — a real design/code piece not built yet, blocking any content pass on
it from starting:
- Paschaltide should substitute the ordinary suffrages/ferial-Preces set with a single
alleluia-form "Suffragium Paschale" — not modeled; the ordinary set is shown unchanged
throughout Paschaltide instead.
- (none currently open — the last one, Paschaltide's "Suffragium Paschale" substitution,
was closed 2026-09-04, see the dated log below)
2. **Minimal-proof-done, bulk content pending** — mechanism is built and trusted (a proof
date/entry already resolves correctly end-to-end); what's left is authoring/importing more
@@ -5643,3 +5642,44 @@ word-swap, confirmed live), shared "Ángelis suis" versicle. `lauds-hymn-lent.ym
checking `lauds-hymn-saturday.yml`'s real content against the same 2026-02-28 Lenten Ember Saturday
date) — byte-identical, just needed the status flipped. All 8 files now `verified`; no more draft
Lent content anywhere in this app. `npm test` (2025 passed) and `tsc --noEmit` both pass.
### Paschaltide's "Suffragium Paschale" mechanism (2026-09-04)
Closed the highest-priority remaining mechanism gap. Live-verified against Divinum Officium (both
Monastic Tridentinum 1617 and Tridentine 1906, byte-identical between the two tracks — the source's
own `[Suffragium]` macro literally has a `(sed tempore paschali) @:Suffragium Paschale` override,
2026-04-20, a plain Eastertide feria): throughout the whole Paschaltide window (`eastertide`/
`ascensiontide`/`pentecost` — the same boundary `PASCHALTIDE_SEASONS` already governs elsewhere in
this store), two things happen whenever they'd otherwise show at all:
1. The ordinary 5-suffrage set (Cross/BVM/Joseph/Apostles/Peace) collapses into one single
alleluia-form "Suffragium Paschale" block — one antiphon (different at Lauds, "Crucifíxus
surréxit a mórtuis...", vs. Vespers, "Crucem sanctam súbiit...", unlike the ordinary 4/5
suffrages, which share identical text at both hours), one shared versicle, one shared collect.
2. The ferial-Preces litany (`lauds-preces-feriales`/`vespers-preces-feriales`, sourced from
Tridentine 1906/1910 since Monastic 1617 has none at all — see that file's own header) drops
entirely rather than being replaced by anything ("Preces Feriales{omittitur}" live) — a
Sunday/feast's own short litany (`lauds-short-litany`) is untouched, a separate, universal
collect-opening dialogue unrelated to this ferial-specific one.
Exported a new `isPaschaltide(season)` helper from `hours/resolve-common.ts` (wraps the existing
`PASCHALTIDE_SEASONS` set, previously private to that file) and used it in both
`resolveSuffrages` (gated after the pre-existing "omit suffrages entirely" checks — Double+/active
octave/Advent-Christmastide-Passiontide already drop suffrages regardless of season, unaffected)
and a new check in `hours/lauds.ts`'s/`hours/vespers.ts`'s own `'lauds-preces'`/`'vespers-preces'`
cases (gated on `isFerialOrVigil(day)`, matching the existing predicate that already selects the
ferial litany in the first place). `resolveSuffrages` gained a third `hourId: 'lauds' | 'vespers'`
parameter (both call sites updated) to pick the right antiphon.
Authored `lauds-suffrage-paschale.yml` and `vespers-suffrage-paschale.yml` — English on the Vespers
antiphon is this project's own translation and left `draft` (the reference engine's own English
render leaves that one antiphon untranslated, Latin-only); everything else `verified`.
Verified end-to-end via `resolveOrdo` on real dates for Lauds (both the suffrage collapse and the
Preces omission fire together correctly on a clean Eastertide feria, 2026-04-20) and via a direct
`resolveSuffrages` unit call plus a separately-found clean Vespers evening (2026-04-15, since this
particular stretch of Eastertide 2026 is otherwise densely packed with First-Vespers anticipations
and octaves — Easter's own, the Patronage of St. Joseph's, Ascension's, Pentecost's — that already
correctly suppress suffrages for other, pre-existing reasons and would have made a false negative
look like a false positive). `npm test` (2025 passed, no pre-existing tests exercised this path at
all) and `tsc --noEmit` both pass.
@@ -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':