Relocate Immaculate Heart of Mary off Aug 22 to Easter+69

Fixed Aug 22 collided outright with the Assumption's own octave-closing
day (both this project's blended calendar tracks are kept deliberately,
so one shouldn't permanently suppress the other). Moved IHM to the
Saturday after the Feast of the Sacred Heart -- its real diocesan date
from 1914 until Pius XII's 1944 fixed-date decree, and also the date the
1969 reform returned to.

Required real new mechanism, not just a data move: a new bespoke
calendar/index.ts override (applyImmaculateHeart, following the existing
applyMarianSaturday/applyChristTheKing precedent), a move from the
sanctoral saints store to the temporal-feasts store (different propers
lookup entirely), and a real bug fix in matins.ts's nocturnReadingIds,
which only ever picked up a *sanctoral* winner's own reading file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZSAgRi4QE4XRTqVto93zA
This commit is contained in:
2026-08-22 05:59:33 -04:00
parent 025d0e3a08
commit 224f34908c
12 changed files with 190 additions and 29 deletions
+49
View File
@@ -105,6 +105,7 @@ export function resolveDay(isoDate: string): LiturgicalDay {
winner = applyMarianSaturday(isoDate, weekday, temporalCategory, winner, commemorations);
winner = applyChristTheKing(isoDate, winner, commemorations);
winner = applyChristmasOctaveSunday(isoDate, weekday, winner, commemorations);
winner = applyImmaculateHeart(isoDate, winner, commemorations);
applyEpiphany6Commemoration(isoDate, commemorations);
return { date: isoDate, weekday, season, temporalCategory, winner, commemorations };
@@ -244,6 +245,54 @@ function applyChristmasOctaveSunday(
return winner;
}
/**
* User decision, 2026-08-22: relocated off the fixed Aug 22 date (which
* collided outright with the Assumption's own octave-closing day —
* Pius XII's 1944 decree fixed it there, but this project keeps the
* older Tridentine "Octave Day of the Assumption" too) to the *other*
* well-documented historical assignment: the Saturday after the Feast
* of the Sacred Heart. Real history (not a guess): this was the
* feast's actual diocesan date from 1914 until the 1944 fixed-date
* decree, and is also the date Paul VI's 1969 reform returned to — so
* it has continuity on both sides of the Aug-22 interlude, unlike
* inventing a "Saturday after the octave of Sacred Heart" pattern (no
* such rule was ever real; Sacred Heart itself doesn't even have an
* octave modeled in this codebase). Sacred Heart is Easter+68 (always
* a Friday, see data/calendar/easter-offsets.yml's own derivation
* comment) — this is the very next day, Easter+69.
*
* Runs last, after applyMarianSaturday, deliberately: Easter+69 is
* always a Saturday, so absent this override the day would otherwise
* just fall to the generic "Our Lady's Saturday" default — a specific
* named Marian feast should always supersede that generic filler, not
* lose to whichever ran first. Any temporal winner reaching here
* (marian-saturday or a plain post-Pentecost Sunday-of-the-week id)
* has no real standing of its own and is simply superseded, same as
* applyChristTheKing's unconditional-when-temporal branch. Rank
* compared via compareFeastClass, same as every other override in this
* file — a duplex-2-classis feast displaces anything weaker, but a
* higher-ranked sanctoral saint who happens to land on the same
* Saturday keeps the day and Immaculate Heart is commemorated instead.
*/
function applyImmaculateHeart(isoDate: string, winner: DayWinner, commemorations: Commemoration[]): DayWinner {
const year = Number(isoDate.slice(0, 4));
const targetDate = addDays(toIsoDate(easterSunday(year)), 69);
if (isoDate !== targetDate) {
return winner;
}
const IMMACULATE_HEART: DayWinner = { kind: 'temporal', id: 'immaculate-heart-of-mary' };
if (winner.kind === 'temporal') {
commemorations.push({ kind: 'temporal', id: winner.id });
return IMMACULATE_HEART;
}
if (compareFeastClass(winner.rank, 'duplex-2-classis') >= 0) {
commemorations.push({ kind: 'temporal', id: 'immaculate-heart-of-mary' });
return winner;
}
commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, rank: winner.rank });
return IMMACULATE_HEART;
}
/**
* Layered on top of everything above, same spirit as applyOctaves: never
* changes decideOccurrence's own rules, just relabels the result on a free
@@ -1,13 +0,0 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/08-22.txt` -- "Immaculati Cordis
# Beatæ Mariæ Virginis;;Duplex II classis;;5.1;;ex C11". Own real
# [Oratio] and own [Ant 1]. Own Nocturn 2-3 content (Nocturn 3 is St.
# Robert Bellarmine's own homily on the seven words of Christ on the
# Cross, not a cross-reference). Instituted 1942 by Pius XII (see the
# feast's own Lectio6, quoted in nocturn-readings, drawn from
# ecclesiastical documents).
id: immaculate-heart-of-mary
name: "The Immaculate Heart of the Blessed Virgin Mary"
rank: duplex-2-classis
common: common-of-the-bvm
propers: "immaculate-heart-of-mary"
-1
View File
@@ -269,7 +269,6 @@ days:
"08-19": [st-john-eudes]
"08-20": [st-bernard]
"08-21": [st-jane-frances-de-chantal]
"08-22": [immaculate-heart-of-mary]
"08-23": [vigil-of-st-bartholomew]
"08-24": [st-bartholomew]
"08-25": [st-louis]
@@ -0,0 +1,14 @@
# Relocated off the fixed Aug 22 date (2026-08-22 user decision) -- that
# date collided outright with the Assumption's own octave-closing day,
# which this project also deliberately keeps (see saints/assumption.yml).
# Moved to the Saturday after the Feast of the Sacred Heart (Easter+69,
# see calendar/index.ts's applyImmaculateHeart) -- the feast's own real
# diocesan date from 1914 until Pius XII's 1944 fixed-date decree, and
# also the date Paul VI's 1969 reform returned to, so it has continuity
# on both sides of the Aug-22 interlude. Rank/name still sourced from
# `web/www/horas/Latin/Sancti/08-22.txt`'s own [Rank] block, same as
# before the move -- only the date assignment changed, not the office's
# own content.
id: immaculate-heart-of-mary
name: "The Immaculate Heart of the Blessed Virgin Mary"
rank: duplex-2-classis
@@ -1,7 +1,11 @@
# Read directly from web/www/horas/Latin/Sancti/08-22.txt's own
# [Ant 1] and its English counterpart -- a real, proper antiphon of
# this feast, not a Common substitution.
id: immaculate-heart-of-mary-antiphon
# this feast, not a Common substitution. Duplicated as both Benedictus
# and Magnificat antiphon (see the -magnificat- sibling file): the
# source only carries this single [Ant 1], and the temporal propers
# store keys the two separately, unlike the sanctoral store's single
# shared `${propers}-antiphon`.
id: immaculate-heart-of-mary-benedictus-antiphon
text:
la: "Exsultávit cor meum * in Dómino, et exaltátum est cornu meum in Deo meo, quia lætáta sum in salutári tuo."
en: "My heart hath rejoiced in the Lord, * and my horn is exalted in my God: because I have joyed in thy salvation."
@@ -0,0 +1,14 @@
# Read directly from web/www/horas/Latin/Sancti/08-22.txt's own
# [Ant 1] and its English counterpart -- a real, proper antiphon of
# this feast, not a Common substitution. Duplicated as both Benedictus
# and Magnificat antiphon (see the -benedictus- sibling file): the
# source only carries this single [Ant 1], and the temporal propers
# store keys the two separately, unlike the sanctoral store's single
# shared `${propers}-antiphon`.
id: immaculate-heart-of-mary-magnificat-antiphon
text:
la: "Exsultávit cor meum * in Dómino, et exaltátum est cornu meum in Deo meo, quia lætáta sum in salutári tuo."
en: "My heart hath rejoiced in the Lord, * and my horn is exalted in my God: because I have joyed in thy salvation."
status:
la: verified
en: verified
+7 -1
View File
@@ -300,7 +300,13 @@ function ferialPsalmodyThreeNocturns(day: LiturgicalDay): [ResolvedPart[], Resol
* user's own "be generous, not winner-takes-all" instruction (2026-08). */
function nocturnReadingIds(day: LiturgicalDay, temporalId: string): string[] {
const ids = new Set<string>();
if (day.winner.kind === 'sanctoral') ids.add(day.winner.id);
// Not gated to `kind === 'sanctoral'` -- a named temporal override (e.g.
// Immaculate Heart of Mary, calendar/index.ts's applyImmaculateHeart)
// has its own authored nocturn-readings file keyed by its own id too,
// distinct from the plain governing-Sunday `temporalId` added below.
// Harmless to include unconditionally: on an ordinary day `day.winner.id`
// already equals `temporalId`, so the Set just dedupes.
ids.add(day.winner.id);
for (const c of day.commemorations) {
if (c.kind === 'sanctoral') ids.add(c.id);
}
+6 -1
View File
@@ -35,7 +35,12 @@ export function resolveCommon(id: string): ResolvedText {
// override-eligible — see hours/lauds.ts's getPsalmodyOverrideFor, which
// shares this set (imported from here, not duplicated) since it's the
// same "does this temporal identity carry its own real office" question.
export const ALWAYS_OVERRIDE_TEMPORAL_IDS = new Set(['marian-saturday', 'christ-the-king', 'christmas-octave-sunday']);
export const ALWAYS_OVERRIDE_TEMPORAL_IDS = new Set([
'marian-saturday',
'christ-the-king',
'christmas-octave-sunday',
'immaculate-heart-of-mary',
]);
/**
* Which identity's own propers actually supply the office's content