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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
This commit is contained in:
2026-09-04 11:08:11 -04:00
parent ab8d1d452d
commit 44dd4bb47a
11 changed files with 451 additions and 1 deletions
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+21 -1
View File
@@ -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);
+11
View File
@@ -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;
}
+15
View File
@@ -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);