Generalize non-Sunday movable feasts into one Easter-offset resolver
Deploy / deploy (push) Successful in 1m21s

The bespoke applyImmaculateHeart function (following the applyMarianSaturday/
applyChristTheKing precedent of one hand-written override function per
feast) doesn't scale: several more feasts are planned that all reduce to
"weekday N days from Easter Sunday" (Ember/Rogation days, more
Sacred-Heart-family Marian devotions, St. Joseph's pre-1955 Eastertide
feast, Lenten Friday Passion devotions), and one-off functions invite
subtle ordering bugs -- applyImmaculateHeart had to be sequenced after
applyMarianSaturday specifically or it would have been silently clobbered.

Replaced with calendar/movable-feasts.ts's generic applyMovableFeasts,
which scans calendar/temporal-feasts.ts's existing per-feast YAML records
for a new optional `easterOffset` field. Adding the next movable feast is
now a new data/calendar/temporal-feasts/<id>.yml file with `rank` and
`easterOffset` set, not a new TypeScript function. Same rank-compared,
commemorate-the-loser semantics as before, just centralized instead of
duplicated per feast.

christ-the-king, marian-saturday, and christmas-octave-sunday stay as
their own functions -- they're structurally different rules (a fixed
month-position search, a date-less fallback default, and a fixed-date-
range Sunday search, respectively), not Easter offsets, so force-fitting
them into this table wouldn't actually simplify anything.

No behavior change -- the existing Immaculate Heart of Mary test suite
passes unchanged through the new generic path.

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 06:33:15 -04:00
parent cf4edfae2b
commit 5e79fb4edf
7 changed files with 135 additions and 55 deletions
+40
View File
@@ -2278,6 +2278,11 @@ common.ts`'s `ALWAYS_OVERRIDE_TEMPORAL_IDS` so its own chapter/responsory/hymn/v
antiphon overrides are actually eligible (a temporal-kind winner is otherwise ignored by
`getOfficeOverrideId`).
**Superseded the same day** — see "Generic Easter-offset movable-feast mechanism" below:
`applyImmaculateHeart` itself no longer exists; the same behavior now runs through
`calendar/movable-feasts.ts`'s generic `applyMovableFeasts`, with IHM as its first data-driven
entry (`easterOffset: 69` on its own temporal-feasts YAML record).
Propers content moved stores, not just files — temporal feasts resolve via a physically
different store than sanctoral saints (`getTemporalProper`/`data/propers/temporal/*.yml`, keyed
`${id}-${suffix}`, vs. sanctoral's `common`/`propers`-field indirection into `data/propers/
@@ -2328,6 +2333,41 @@ New tests in `tests/hours/matins.test.ts` cover the fallback across Aug 16-22, i
Aug 17 edge case (correctly still ferial, since that day is actually governed by St. Lawrence's
own octave, not the Assumption's). `npm test` (430 tests) and `tsc --noEmit` both pass.
### Generic Easter-offset movable-feast mechanism (2026-08-22)
User feedback, same day: a bespoke `calendar/index.ts` function per non-Sunday movable feast
(`applyImmaculateHeart`, following the `applyMarianSaturday`/`applyChristTheKing` precedent) was
flagged as fragile going forward — real plans exist for several more feasts that are all
"weekday N days from Easter Sunday": Ember/Rogation days, more Sacred-Heart-family Marian
devotions, St. Joseph's own pre-1955 Eastertide feast, and the Lenten Friday Passion devotions.
Writing a new function per feast doesn't scale and invites the same subtle ordering bugs
(`applyImmaculateHeart` had to be sequenced *after* `applyMarianSaturday` specifically, or a
Saturday-anchored feast would have been silently clobbered by the generic "Our Lady's Saturday"
default — easy to get wrong once there are several of these).
Replaced with a single generic resolver, `calendar/movable-feasts.ts`'s `applyMovableFeasts`,
run in `applyImmaculateHeart`'s old spot in `resolveDay`'s chain. No new data table: it scans
`calendar/temporal-feasts.ts`'s existing per-feast YAML records (`data/calendar/
temporal-feasts/*.yml`, already carrying `id`/`name`/`rank`/`octave`) for a new optional
`easterOffset` field, added to the `TemporalFeastRecord` type. Immaculate Heart of Mary's own
record gained `easterOffset: 69`; adding the next feast is a new YAML file with `rank` and
`easterOffset` set, not a new TypeScript function. Same rank-compared, commemorate-the-loser
semantics as before, applied generically instead of duplicated per feast.
Deliberately doesn't try to unify everything: `christ-the-king` (last Sunday of October),
`marian-saturday` (a generic "nothing else assigned" fallback, not date-anchored at all), and
`christmas-octave-sunday` (whichever of Dec 26-29 is a Sunday) are structurally different rules,
not Easter-offset ones — they stay as their own small functions in `calendar/index.ts` rather
than being force-fit into this table for the sake of a single mechanism. Ember days for the
September and Advent quarters are anchored to a *fixed civil date's* nearest Sunday, not Easter,
so they'll need a second anchor kind on `TemporalFeastRecord` when they're actually authored —
not modeled yet, deliberately: the other two Ember quarters (Lent I, Pentecost) and Rogation
days, on the other hand, *are* plain Easter offsets and are already covered by this mechanism
today.
No behavior change — `tests/calendar/immaculate-heart-of-mary.test.ts` (unchanged) still passes
end-to-end through the new generic path. `npm test` (430 tests) and `tsc --noEmit` both pass.
## Known, deliberate simplifications (not bugs — working as designed)
- `getDayCollects`: each collect in a multi-collect day renders as its own