Compare commits

..

2 Commits

Author SHA1 Message Date
will 374d27858a Log the Ascension hymnody 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 11:08:16 -04:00
will 44dd4bb47a 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
2026-09-04 11:08:11 -04:00
12 changed files with 496 additions and 1 deletions
+45
View File
@@ -5461,3 +5461,48 @@ Updated 2 `FERIAL_DATE` (2026-12-01, a Tuesday *in Advent*) hymn assertions in
`tests/hours/matins.test.ts` that had been asserting Tuesday's own ferial text — Advent's own `tests/hours/matins.test.ts` that had been asserting Tuesday's own ferial text — Advent's own
seasonal hymn correctly outranks the weekday cycle now. `npm test` (2025 passed) and `tsc --noEmit` seasonal hymn correctly outranks the weekday cycle now. `npm test` (2025 passed) and `tsc --noEmit`
both pass. both pass.
### Ascension's own hymnody at Matins/Lauds/Vespers, plus the octave mechanism gap it exposed (2026-09-04)
User asked to actually close the Ascension-octave gap flagged in the entry just above. Turned out
to be a genuine mechanism gap, not just missing content: `ascension` is a *temporal*-feast id
(`data/calendar/temporal-feasts/ascension.yml`), not a saint record, so
`hours/resolve-common.ts`'s `resolveOfficeWinner` — which already synthesizes a sanctoral-shaped
winner from an active octave's own saint record (how Assumption's/St. Lawrence's octave already
gives every hour real content on interior days) — never recognized it at all. Ascension Day itself
and its whole octave were silently rendering the generic Paschaltide hymn at every hour, live-
verified against Divinum Officium (Monastic Tridentinum 1617, 2026-05-14, Ascension Day itself):
real content is "Jesu nostra redémptio" (identical at Matins/Lauds/Vespers), Act 1:1-2 (Lauds/
Vespers' shared chapter), and two genuinely distinct Lauds/Vespers responsories ("Ascéndit Deus in
jubilatióne" / "Ascéndens Christus in altum").
Fixed the mechanism two ways: (1) added `ascension` to `ALWAYS_OVERRIDE_TEMPORAL_IDS` (covers
Ascension Day itself, the same way `christ-the-king`/`circumcision`/etc. already do); (2) 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 saint
still correctly keeps priority when one wins, e.g. St. Venantius/St. Peter Celestine within the
octave, live-verified unaffected). A third gap surfaced testing this: `octaveGoverningPrivilegedDay`
(the *Sunday*-side half of that same synthesis) has its own, separate, correct-by-design rule that a
privileged Sunday never accepts even an octave override — but live-verified wrong specifically for
"Dominica infra Octavam Ascensionis," which really does keep the octave's own hymn/responsory (just
with its own distinct proper chapter, not touched here). Rather than loosen that shared gate (real
risk of wrongly re-admitting some other octave's override on some other privileged Sunday, unverified
case by case), gave Lauds' and Vespers' own hymn-resolution functions (`resolveLaudsHymn`/
`resolveVespersHymn`) a narrow, hymn-only intercept: check `resolveActiveOctave` directly (no
privileged-Sunday gate) but only for a saint-less octave, before falling to the season/weekday
default — leaves every saint-based octave's existing behavior (and every other hour/part) completely
untouched.
Authored `matins-hymn-ascension.yml`, `lauds-{capitulum,responsory,hymn,versicle}-ascension.yml`,
and `vespers-{responsory,hymn,versicle}-ascension.yml` (Vespers' own chapter already falls back to
Lauds' via `vespersCapitulumForOverride`'s existing cross-hour fallback, so no separate file needed)
— all live-verified. Confirmed the whole octave end-to-end via `resolveOrdo` directly (Ascension Day,
two plain ordinary-feria octave days, the Sunday within the octave, and 3 real winning saints within
the window, all correct) rather than just the reference engine. `npm test` (2025 passed, no test
changes needed) and `tsc --noEmit` both pass.
Flagged, not fixed: Ascension's own collect is still missing (`getTemporalProper('ascension-collect')`
— a pre-existing gap, unrelated to and unaffected by this fix, not scoped to "hymnody"). Also flagged:
Pentecost/Corpus Christi/Sacred Heart are the same shape (temporal-feast octaves with no saint
record) and likely have the same latent hymn gap this fix closed for Ascension specifically — not
checked or fixed here.
@@ -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 { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
import type { LiturgicalDay, Weekday } from '../calendar/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 { getDayLabel } from '../calendar/day-label';
import { getPsalmVerses } from '../psalter'; import { getPsalmVerses } from '../psalter';
import { getCanticle, type CanticleVerse } from './lauds-canticles'; import { getCanticle, type CanticleVerse } from './lauds-canticles';
@@ -282,6 +283,25 @@ function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] {
* exact original text. * exact original text.
*/ */
function resolveLaudsHymn(day: LiturgicalDay, key: string): ResolvedText { 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 body = resolveCommon(`lauds-hymn-${key}`);
const doxology = resolveCommon(getHymnDoxologyId(day, `lauds-hymn-${key}-doxology-per-annum`)); const doxology = resolveCommon(getHymnDoxologyId(day, `lauds-hymn-${key}-doxology-per-annum`));
return appendDoxology(body, doxology); return appendDoxology(body, doxology);
+11
View File
@@ -60,6 +60,7 @@ export const ALWAYS_OVERRIDE_TEMPORAL_IDS = new Set([
'christ-the-king', 'christ-the-king',
'christmas-octave-sunday', 'christmas-octave-sunday',
'immaculate-heart-of-mary', 'immaculate-heart-of-mary',
'ascension',
'circumcision', 'circumcision',
'holy-name-of-jesus', 'holy-name-of-jesus',
'vigil-of-christmas', 'vigil-of-christmas',
@@ -124,6 +125,16 @@ export function resolveOfficeWinner(day: LiturgicalDay): DayWinner {
if (saint) { if (saint) {
return { kind: 'sanctoral', id: saint.id, name: saint.name, rank: saint.rank }; 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; 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 type { LiturgicalDay, Weekday } from '../calendar/types';
import { resolveEveningDay } from '../calendar/vespers'; import { resolveEveningDay } from '../calendar/vespers';
import { weekdayOf } from '../calendar/weekday'; import { weekdayOf } from '../calendar/weekday';
import { resolveActiveOctave } from '../calendar';
import { getSaintRecord } from '../calendar/feasts';
import { getDayLabel } from '../calendar/day-label'; import { getDayLabel } from '../calendar/day-label';
import { getPsalmVerses } from '../psalter'; import { getPsalmVerses } from '../psalter';
import { getOpeningVersicleId } from './opening-versicle'; import { getOpeningVersicleId } from './opening-versicle';
@@ -190,6 +192,19 @@ function vespersCapitulumForOverride(id: string): ResolvedText {
* Advent, Lent — the source itself never swaps those) reproduces the * Advent, Lent — the source itself never swaps those) reproduces the
* exact original text. See lauds.ts's identical resolveLaudsHymn. */ * exact original text. See lauds.ts's identical resolveLaudsHymn. */
function resolveVespersHymn(day: LiturgicalDay, key: string): ResolvedText { 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 body = resolveCommon(`vespers-hymn-${key}`);
const doxology = resolveCommon(getHymnDoxologyId(day, `vespers-hymn-${key}-doxology-per-annum`)); const doxology = resolveCommon(getHymnDoxologyId(day, `vespers-hymn-${key}-doxology-per-annum`));
return appendDoxology(body, doxology); return appendDoxology(body, doxology);