calendar: compute real Easter dates and temporal-cycle seasons

resolveDay() no longer hardcodes season: 'trinitytide'. Easter is
computed via the Anonymous Gregorian algorithm (verified against 16
known reference dates spanning 1583-2100); Advent/Christmastide/
Epiphanytide follow the Christmas-anchored rules (including Advent's
"Sunday nearest Nov 30"); Septuagesima through Trinitytide come from
easter-offsets.yml, reshaped from single named anchor points into real
ranges plus single-day overrides for Corpus Christi and Sacred Heart.

occurring feasts (calendar/feasts.ts) are still a stub — the sanctoral
calendar and Double-vs-not ranking are a separate, larger project.
This commit is contained in:
2026-08-10 05:27:12 -04:00
parent 954edeb7b9
commit 67037469fc
8 changed files with 291 additions and 42 deletions
+21 -5
View File
@@ -14,6 +14,26 @@ export type Weekday =
// actually resolves the "opinionated but configurable" tension: the opinion
// (trinitytide) lives in editable data, not in a TS enum you'd recompile to
// change.
//
// A real limitation this doesn't fully solve: a single `season` string can
// only hold one mutually-exclusive value per day, but several independent
// liturgical windows overlap without sharing boundaries. For example, on a
// real day between Candlemas (Feb 2) and Ash Wednesday, Compline's Marian
// antiphon should already be Ave Regina Caelorum, but Lent's Alleluia
// suppression and hymn swap shouldn't have started yet — two things that
// are both true at once, which one `season` value can't represent.
//
// Real season resolution now exists (calendar/temporal.ts, calendar/easter.ts),
// and the Marian-antiphon case above is handled — but not by adding a
// `season` value for it. hours/marian-antiphon.ts checks the real date
// directly instead of going through `season` at all for that one window.
// That's a fine, scoped fix for one known overlap; it isn't a general
// solution. If another mechanism turns up with the same shape (a window
// that doesn't nest inside one `season` bucket), reach for the same
// pattern — a direct date check bypassing `season` — rather than trying to
// force `season` to hold two truths at once. Only worth generalizing into
// several independent named windows/flags on `LiturgicalDay` itself if a
// third case shows up and the duplication starts to hurt.
export type Season = string;
// Open-ended on purpose — the actual ranking scheme (double/semidouble/simple,
@@ -35,11 +55,7 @@ export interface LiturgicalDay {
/** ISO date, e.g. "2026-08-09" */
date: string;
weekday: Weekday;
/**
* Real season resolution depends on Easter's date (see calendar/easter.ts,
* not implemented until milestone 4). Until then this is a placeholder and
* must not be trusted by any hour's logic.
*/
/** Real temporal-cycle season, computed via calendar/temporal.ts. */
season: Season;
/** Always [] until milestone 4 wires up calendar/feasts.ts. */
occurring: OccurringFeast[];