From 44dd4bb47ac296e0ea06b162dda5d9f5752ba919 Mon Sep 17 00:00:00 2001 From: Will Estes Date: Fri, 4 Sep 2026 11:08:11 -0400 Subject: [PATCH] Give Ascension its own hymnody at Matins/Lauds/Vespers, all octave `ascension` is a temporal-feast id, not a saint record, so resolveOfficeWinner's octave-synthesis (which already handles saint-based octaves like Assumption's/St. Lawrence's) never recognized it -- Ascension Day itself and its whole octave silently rendered the generic Paschaltide hymn at every hour. Fixed the mechanism: added `ascension` to ALWAYS_OVERRIDE_TEMPORAL_IDS (covers the feast day itself), and taught resolveOfficeWinner to also synthesize a {kind:'temporal'} winner from an active octave with no saint record but a real temporal-feast record (covers ordinary-feria octave days; a real winning saint within the octave still correctly keeps priority). A third gap surfaced testing this: octaveGoverningPrivilegedDay's correct-by-design "a privileged Sunday never accepts an octave override" rule is live-verified wrong specifically for "Dominica infra Octavam Ascensionis" (keeps the octave's own hymn/responsory, just with its own distinct proper chapter). Rather than loosen that shared gate for every octave, gave resolveLaudsHymn/resolveVespersHymn a narrow, hymn-only intercept guarded to saint-less octaves, checked before the season/weekday default. Authored matins-hymn-ascension.yml, lauds-{capitulum,responsory,hymn, versicle}-ascension.yml, and vespers-{responsory,hymn,versicle}- ascension.yml (Vespers reuses Lauds' chapter via the existing cross-hour fallback) -- all live-verified against Divinum Officium (Monastic Tridentinum 1617), 2026-05-14. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak --- .../common/lauds-capitulum-ascension.yml | 18 +++ .../propers/common/lauds-hymn-ascension.yml | 101 +++++++++++++++ .../common/lauds-responsory-ascension.yml | 24 ++++ .../common/lauds-versicle-ascension.yml | 11 ++ .../propers/common/matins-hymn-ascension.yml | 117 ++++++++++++++++++ .../propers/common/vespers-hymn-ascension.yml | 98 +++++++++++++++ .../common/vespers-responsory-ascension.yml | 24 ++++ .../common/vespers-versicle-ascension.yml | 11 ++ src/hours/lauds.ts | 22 +++- src/hours/resolve-common.ts | 11 ++ src/hours/vespers.ts | 15 +++ 11 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 src/data/propers/common/lauds-capitulum-ascension.yml create mode 100644 src/data/propers/common/lauds-hymn-ascension.yml create mode 100644 src/data/propers/common/lauds-responsory-ascension.yml create mode 100644 src/data/propers/common/lauds-versicle-ascension.yml create mode 100644 src/data/propers/common/matins-hymn-ascension.yml create mode 100644 src/data/propers/common/vespers-hymn-ascension.yml create mode 100644 src/data/propers/common/vespers-responsory-ascension.yml create mode 100644 src/data/propers/common/vespers-versicle-ascension.yml diff --git a/src/data/propers/common/lauds-capitulum-ascension.yml b/src/data/propers/common/lauds-capitulum-ascension.yml new file mode 100644 index 0000000..a5415c1 --- /dev/null +++ b/src/data/propers/common/lauds-capitulum-ascension.yml @@ -0,0 +1,18 @@ +# The Ascension's own Lauds chapter (Acts 1:1-2) — shared with Vespers +# via vespersCapitulumForOverride's own cross-hour fallback (see +# hours/vespers.ts), not duplicated into a separate vespers-capitulum- +# ascension.yml. Live-verified against Divinum Officium (Monastic +# Tridentinum 1617), Lauds/Vespers live query, 2026-05-14 (Ascension Day +# itself) — see matins-hymn-ascension.yml's own header for the fuller +# mechanism story (a temporal-feast octave with no saint record wasn't +# being picked up at all before this). +id: lauds-capitulum-ascension +text: + la: "Primum quidem sermónem feci de ómnibus, o Theóphile, quæ cœpit Jesus fácere et docére usque in diem, qua præcípiens Apóstolis per Spíritum Sanctum, quos elégit, assúmptus est." + en: "The former treatise I made, O Theophilus, of all things which Jesus began to do and to teach, Until the day on which, giving commandments by the Holy Ghost to the apostles whom he had chosen, he was taken up." +citation: + la: "Act 1:1-2" + en: "Acts 1:1-2" +status: + la: verified + en: verified diff --git a/src/data/propers/common/lauds-hymn-ascension.yml b/src/data/propers/common/lauds-hymn-ascension.yml new file mode 100644 index 0000000..d3a010b --- /dev/null +++ b/src/data/propers/common/lauds-hymn-ascension.yml @@ -0,0 +1,101 @@ +# The Ascension's own Lauds hymn ("Jesu nostra redémptio") — identical +# text to Matins' own (matins-hymn-ascension.yml) and Vespers' own +# (vespers-hymn-ascension.yml). Live-verified against Divinum Officium +# (Monastic Tridentinum 1617), Lauds live query, 2026-05-14 (Ascension +# Day itself). Self-contained — this hour's own resolveOfficeBundle +# doesn't run a hymn through the doxology-split/append mechanism at all, +# and this hymn's own natural ending has no separate doxology stanza in +# the source anyway. +id: lauds-hymn-ascension +text: + la: >- + Jesu nostra redémptio, + + Amor et desidérium, + + Deus Creátor ómnium, + + Homo in fine témporum: + + + Quæ te vicit cleméntia, + + Ut ferres nostra crímina, + + Crudélem mortem pátiens, + + Ut nos a morte tólleres! + + + Inférni claustra pénetrans, + + Tuos captívos rédimens, + + Victor triúmpho nóbili + + Ad dextram Patris résidens. + + + Ipsa te cogat píetas, + + Ut mala nostra súperes + + Parcéndo, et voti cómpotes + + Nos tuo vultu sáties. + + + Tu esto nostrum gáudium, + + Qui es futúrus prǽmium, + + Sit nostra in te glória, + + Per cuncta semper sǽcula. Amen. + en: >- + Hail, thou who man's Redeemer art, + + Jesu, the joy of every heart; + + Great Maker of the world's wide frame, + + And purest love's delight and flame: + + + What nameless mercy thee o'ercame, + + To bear our load of sin and shame? + + For guiltless, thou thy life didst give, + + That sinful erring man might live. + + + The realms of woe are forced by thee, + + Its captives from their chains set free; + + And thou, amid thy ransomed train, + + At God's right hand dost victor reign. + + + Let mercy sweet with thee prevail, + + To cure the wounds we now bewail; + + Oh, bless us with thy holy sight, + + And fill us with eternal light. + + + Our guide, our way to heavenly rest, + + Be thou the aim of every breast; + + Be thou the soother of our tears, + + Our sweet reward above the spheres. Amen. +status: + la: verified + en: verified diff --git a/src/data/propers/common/lauds-responsory-ascension.yml b/src/data/propers/common/lauds-responsory-ascension.yml new file mode 100644 index 0000000..60610c7 --- /dev/null +++ b/src/data/propers/common/lauds-responsory-ascension.yml @@ -0,0 +1,24 @@ +# The Ascension's own Lauds responsory ("Ascéndit Deus in jubilatióne") +# — distinct from Vespers' own ("Ascéndens Christus in altum", see +# vespers-responsory-ascension.yml). Live-verified against Divinum +# Officium (Monastic Tridentinum 1617), Lauds live query, 2026-05-14 +# (Ascension Day itself). +id: lauds-responsory-ascension +text: + la: | + R.br. Ascéndit Deus in jubilatióne, * Allelúja, allelúja. + R. Ascéndit Deus in jubilatióne, * Allelúja, allelúja. + V. Et Dóminus in voce tubæ. + R. Allelúja, allelúja. + V. Glória Patri, et Fílio, * et Spirítui Sancto. + R. Ascéndit Deus in jubilatióne, * Allelúja, allelúja. + en: | + R.br. God is gone up with jubilation. * Alleluia, alleluia. + R. God is gone up with jubilation. * Alleluia, alleluia. + V. And the Lord with the sound of trumpet. + R. Alleluia, alleluia. + V. Glory be to the Father, and to the Son, * and to the Holy Ghost. + R. God is gone up with jubilation. * Alleluia, alleluia. +status: + la: verified + en: verified diff --git a/src/data/propers/common/lauds-versicle-ascension.yml b/src/data/propers/common/lauds-versicle-ascension.yml new file mode 100644 index 0000000..14a183e --- /dev/null +++ b/src/data/propers/common/lauds-versicle-ascension.yml @@ -0,0 +1,11 @@ +# The Ascension's own versicle — identical at both Lauds and Vespers +# (see vespers-versicle-ascension.yml). Live-verified against Divinum +# Officium (Monastic Tridentinum 1617), Lauds/Vespers live query, +# 2026-05-14 (Ascension Day itself). +id: lauds-versicle-ascension +text: + la: "V. Dóminus in cælo, allelúja.\nR. Parávit sedem suam, allelúja." + en: "V. The Lord in heaven, alleluia.\nR. Hath prepared his throne, alleluia." +status: + la: verified + en: verified diff --git a/src/data/propers/common/matins-hymn-ascension.yml b/src/data/propers/common/matins-hymn-ascension.yml new file mode 100644 index 0000000..7aaa396 --- /dev/null +++ b/src/data/propers/common/matins-hymn-ascension.yml @@ -0,0 +1,117 @@ +# The Ascension's own Matins hymn ("Jesu nostra redémptio"), shared +# identically with its own Lauds and Vespers (see lauds-hymn-ascension.yml/ +# vespers-hymn-ascension.yml). Live-verified against Divinum Officium +# (Monastic Tridentinum 1617), Matins/Lauds/Vespers live query, 2026-05-14 +# (Ascension Day itself). +# +# Closes a real gap found while fixing the generic Matins seasonal-hymn +# fallback (see matins-hymn-paschaltide.yml's own header): 'ascension' is +# a temporal-feast id (calendar/data/calendar/temporal-feasts/ +# ascension.yml), not a saint record, so it was never picked up by +# `resolveOfficeWinner`'s octave-synthesis (which only ever checked +# `getSaintRecord`) — Ascension Day itself and its whole octave were both +# silently rendering the generic Paschaltide hymn instead of their own +# proper one, at every hour. Fixed by (1) adding `ascension` to +# `ALWAYS_OVERRIDE_TEMPORAL_IDS` (covers Ascension Day itself) and (2) +# teaching `resolveOfficeWinner` to also synthesize a `{kind: 'temporal'}` +# winner from a temporal-feast-only active octave when no saint record +# exists (covers the interior octave days, e.g. St. Ubald's or St. +# Bernardine's own days still correctly win outright and keep their own +# hymn — this only ever fires on a day nothing else claims). +# +# Self-contained — this hymn's own natural ending ("Per cuncta semper +# sǽcula. Amen.") has no separate trailing doxology stanza at all in the +# source, unlike most other Matins hymns in this store. +id: matins-hymn-ascension +text: + la: >- + Jesu nostra redémptio, + + Amor et desidérium, + + Deus Creátor ómnium, + + Homo in fine témporum: + + + Quæ te vicit cleméntia, + + Ut ferres nostra crímina, + + Crudélem mortem pátiens, + + Ut nos a morte tólleres! + + + Inférni claustra pénetrans, + + Tuos captívos rédimens, + + Victor triúmpho nóbili + + Ad dextram Patris résidens. + + + Ipsa te cogat píetas, + + Ut mala nostra súperes + + Parcéndo, et voti cómpotes + + Nos tuo vultu sáties. + + + Tu esto nostrum gáudium, + + Qui es futúrus prǽmium, + + Sit nostra in te glória, + + Per cuncta semper sǽcula. Amen. + en: >- + Hail, thou who man's Redeemer art, + + Jesu, the joy of every heart; + + Great Maker of the world's wide frame, + + And purest love's delight and flame: + + + What nameless mercy thee o'ercame, + + To bear our load of sin and shame? + + For guiltless, thou thy life didst give, + + That sinful erring man might live. + + + The realms of woe are forced by thee, + + Its captives from their chains set free; + + And thou, amid thy ransomed train, + + At God's right hand dost victor reign. + + + Let mercy sweet with thee prevail, + + To cure the wounds we now bewail; + + Oh, bless us with thy holy sight, + + And fill us with eternal light. + + + Our guide, our way to heavenly rest, + + Be thou the aim of every breast; + + Be thou the soother of our tears, + + Our sweet reward above the spheres. Amen. +status: + la: verified + en: verified diff --git a/src/data/propers/common/vespers-hymn-ascension.yml b/src/data/propers/common/vespers-hymn-ascension.yml new file mode 100644 index 0000000..76f9fb4 --- /dev/null +++ b/src/data/propers/common/vespers-hymn-ascension.yml @@ -0,0 +1,98 @@ +# The Ascension's own Vespers hymn ("Jesu nostra redémptio") — identical +# text to Matins' own (matins-hymn-ascension.yml) and Lauds' own +# (lauds-hymn-ascension.yml). Live-verified against Divinum Officium +# (Monastic Tridentinum 1617), Vespers live query, 2026-05-14 (Ascension +# Day itself). +id: vespers-hymn-ascension +text: + la: >- + Jesu nostra redémptio, + + Amor et desidérium, + + Deus Creátor ómnium, + + Homo in fine témporum: + + + Quæ te vicit cleméntia, + + Ut ferres nostra crímina, + + Crudélem mortem pátiens, + + Ut nos a morte tólleres! + + + Inférni claustra pénetrans, + + Tuos captívos rédimens, + + Victor triúmpho nóbili + + Ad dextram Patris résidens. + + + Ipsa te cogat píetas, + + Ut mala nostra súperes + + Parcéndo, et voti cómpotes + + Nos tuo vultu sáties. + + + Tu esto nostrum gáudium, + + Qui es futúrus prǽmium, + + Sit nostra in te glória, + + Per cuncta semper sǽcula. Amen. + en: >- + Hail, thou who man's Redeemer art, + + Jesu, the joy of every heart; + + Great Maker of the world's wide frame, + + And purest love's delight and flame: + + + What nameless mercy thee o'ercame, + + To bear our load of sin and shame? + + For guiltless, thou thy life didst give, + + That sinful erring man might live. + + + The realms of woe are forced by thee, + + Its captives from their chains set free; + + And thou, amid thy ransomed train, + + At God's right hand dost victor reign. + + + Let mercy sweet with thee prevail, + + To cure the wounds we now bewail; + + Oh, bless us with thy holy sight, + + And fill us with eternal light. + + + Our guide, our way to heavenly rest, + + Be thou the aim of every breast; + + Be thou the soother of our tears, + + Our sweet reward above the spheres. Amen. +status: + la: verified + en: verified diff --git a/src/data/propers/common/vespers-responsory-ascension.yml b/src/data/propers/common/vespers-responsory-ascension.yml new file mode 100644 index 0000000..7965085 --- /dev/null +++ b/src/data/propers/common/vespers-responsory-ascension.yml @@ -0,0 +1,24 @@ +# The Ascension's own Vespers responsory ("Ascéndens Christus in +# altum") — distinct from Lauds' own ("Ascéndit Deus in jubilatióne", +# see lauds-responsory-ascension.yml). Live-verified against Divinum +# Officium (Monastic Tridentinum 1617), Vespers live query, 2026-05-14 +# (Ascension Day itself). +id: vespers-responsory-ascension +text: + la: | + R.br. Ascéndens Christus in altum, * Allelúja, allelúja. + R. Ascéndens Christus in altum, * Allelúja, allelúja. + V. Captívam duxit captivitátem. + R. Allelúja, allelúja. + V. Glória Patri, et Fílio, * et Spirítui Sancto. + R. Ascéndens Christus in altum, * Allelúja, allelúja. + en: | + R.br. When Christ ascended up on high. * Alleluia, alleluia. + R. When Christ ascended up on high. * Alleluia, alleluia. + V. He led captivity captive. + R. Alleluia, alleluia. + V. Glory be to the Father, and to the Son, * and to the Holy Ghost. + R. When Christ ascended up on high. * Alleluia, alleluia. +status: + la: verified + en: verified diff --git a/src/data/propers/common/vespers-versicle-ascension.yml b/src/data/propers/common/vespers-versicle-ascension.yml new file mode 100644 index 0000000..ff1d35a --- /dev/null +++ b/src/data/propers/common/vespers-versicle-ascension.yml @@ -0,0 +1,11 @@ +# The Ascension's own versicle — identical at both Vespers and Lauds +# (see lauds-versicle-ascension.yml). Live-verified against Divinum +# Officium (Monastic Tridentinum 1617), Vespers live query, 2026-05-14 +# (Ascension Day itself). +id: vespers-versicle-ascension +text: + la: "V. Dóminus in cælo, allelúja.\nR. Parávit sedem suam, allelúja." + en: "V. The Lord in heaven, alleluia.\nR. Hath prepared his throne, alleluia." +status: + la: verified + en: verified diff --git a/src/hours/lauds.ts b/src/hours/lauds.ts index ba626b1..d6aa4de 100644 --- a/src/hours/lauds.ts +++ b/src/hours/lauds.ts @@ -1,6 +1,7 @@ import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types'; import type { LiturgicalDay, Weekday } from '../calendar/types'; -import { resolveDay } from '../calendar'; +import { resolveDay, resolveActiveOctave } from '../calendar'; +import { getSaintRecord } from '../calendar/feasts'; import { getDayLabel } from '../calendar/day-label'; import { getPsalmVerses } from '../psalter'; import { getCanticle, type CanticleVerse } from './lauds-canticles'; @@ -282,6 +283,25 @@ function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] { * exact original text. */ function resolveLaudsHymn(day: LiturgicalDay, key: string): ResolvedText { + // A temporal-feast-only octave (Ascension's own — no saint record, so + // resolveOfficeWinner's own octave-synthesis can't help here on a + // privileged Sunday, whose `octaveGoverningPrivilegedDay` gate is + // correctly closed to a real *saint* overriding it but wrongly also + // closes the *octave's own* hymn — live-verified wrong: "Dominica infra + // Octavam Ascensionis" still uses Ascension's own hymn/responsory, just + // with its own distinct proper chapter) still needs its own hymn + // recognized here directly via resolveActiveOctave (no privileged- + // Sunday gate) — checked before the plain season/weekday fallback, but + // deliberately not touched at the chapter/responsory/versicle level: + // this Sunday's own chapter genuinely differs from Ascension Day's own, + // so folding the whole bundle through the octave id would be wrong. + const octave = resolveActiveOctave(day.date); + if (octave && !getSaintRecord(octave.id)) { + const octaveHymn = resolveCommon(`lauds-hymn-${octave.id}`); + if (octaveHymn.status.la !== 'missing' || octaveHymn.status.en !== 'missing') { + return octaveHymn; + } + } const body = resolveCommon(`lauds-hymn-${key}`); const doxology = resolveCommon(getHymnDoxologyId(day, `lauds-hymn-${key}-doxology-per-annum`)); return appendDoxology(body, doxology); diff --git a/src/hours/resolve-common.ts b/src/hours/resolve-common.ts index 39d9441..a052418 100644 --- a/src/hours/resolve-common.ts +++ b/src/hours/resolve-common.ts @@ -60,6 +60,7 @@ export const ALWAYS_OVERRIDE_TEMPORAL_IDS = new Set([ 'christ-the-king', 'christmas-octave-sunday', 'immaculate-heart-of-mary', + 'ascension', 'circumcision', 'holy-name-of-jesus', 'vigil-of-christmas', @@ -124,6 +125,16 @@ export function resolveOfficeWinner(day: LiturgicalDay): DayWinner { if (saint) { return { kind: 'sanctoral', id: saint.id, name: saint.name, rank: saint.rank }; } + // A temporal-feast-only octave (Ascension's own — no saint record to + // synthesize from, unlike Assumption's/St. Lawrence's) still needs its + // own content recognized on interior octave days, the same way its own + // feast day already is via the `ALWAYS_OVERRIDE_TEMPORAL_IDS` check + // above — found 2026-09-04 via the Matins seasonal-hymn fallback + // silently rendering the generic Paschaltide text throughout the whole + // Ascension octave, not just a missing-content gap on the day itself. + if (ALWAYS_OVERRIDE_TEMPORAL_IDS.has(activeOctave.id) && getTemporalFeastRecord(activeOctave.id)) { + return { kind: 'temporal', id: activeOctave.id }; + } } return day.winner; } diff --git a/src/hours/vespers.ts b/src/hours/vespers.ts index 7c68e0a..40e93e7 100644 --- a/src/hours/vespers.ts +++ b/src/hours/vespers.ts @@ -2,6 +2,8 @@ import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText import type { LiturgicalDay, Weekday } from '../calendar/types'; import { resolveEveningDay } from '../calendar/vespers'; import { weekdayOf } from '../calendar/weekday'; +import { resolveActiveOctave } from '../calendar'; +import { getSaintRecord } from '../calendar/feasts'; import { getDayLabel } from '../calendar/day-label'; import { getPsalmVerses } from '../psalter'; import { getOpeningVersicleId } from './opening-versicle'; @@ -190,6 +192,19 @@ function vespersCapitulumForOverride(id: string): ResolvedText { * Advent, Lent — the source itself never swaps those) reproduces the * exact original text. See lauds.ts's identical resolveLaudsHymn. */ function resolveVespersHymn(day: LiturgicalDay, key: string): ResolvedText { + // A temporal-feast-only octave (Ascension's own) still needs its own + // hymn recognized directly here, bypassing octaveGoverningPrivilegedDay's + // privileged-Sunday gate — see lauds.ts's identical resolveLaudsHymn for + // the full story. Guarded to saint-less octaves only: a saint-based + // octave (Assumption's, St. Lawrence's) already reaches its own content + // through resolveOfficeWinner's existing synthesis without this. + const octave = resolveActiveOctave(day.date); + if (octave && !getSaintRecord(octave.id)) { + const octaveHymn = resolveCommon(`vespers-hymn-${octave.id}`); + if (octaveHymn.status.la !== 'missing' || octaveHymn.status.en !== 'missing') { + return octaveHymn; + } + } const body = resolveCommon(`vespers-hymn-${key}`); const doxology = resolveCommon(getHymnDoxologyId(day, `vespers-hymn-${key}-doxology-per-annum`)); return appendDoxology(body, doxology);