export type Weekday = | 'sunday' | 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday'; // Deliberately an open string, not a hardcoded union. Which temporal-id a // given day resolves to — and what it's *called* ("trinitytide" vs "time // after pentecost") — is a property of data/calendar/easter-offsets.yml and // fixed-date-calendar.yml, not a compile-time decision. This is what // 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; // The old (pre-1955) rank scale, low to high. Deliberately a closed union // rather than an open string like Season — the whole point of // calendar/commemorations.ts's decideOccurrence is to compare two of these // with explicit, readable rules, which only works if the set of values is // fixed and known. See calendar/types.ts's TemporalCategory doc comment for // the other half of that comparison. // // `vigil` sits between `simplex` and `semiduplex` on purpose — it's the // same strength as `simplex` for the "does this win against a Sunday" // question (both lose and get transferred rather than fighting the day // directly), but ranks strictly above `simplex` for the *separate* // "two saints collide on the same landing day" comparison // (calendar/collision.ts). One ordering serves both; see the design // discussion in project history for why that isn't a coincidence. export type FeastClass = | 'simplex' | 'vigil' | 'semiduplex' | 'duplex' | 'duplex-majus' | 'duplex-2-classis' | 'duplex-1-classis'; // A day's own precedence class *before* any sanctoral feast is considered — // i.e. what the temporal cycle alone says this day is entitled to. This is // coarser than `season` on purpose: several different seasons share the // same precedence behavior (Advent/Septuagesima/Lent/Passiontide Sundays // are all "privileged" in the same way; Epiphanytide/Trinitytide Sundays // are all "ordinary" in the same way), and decideOccurrence only cares // about that behavior, not which season produced it. See // data/calendar/temporal-categories.yml for which season maps to which // category — reconstructed from general knowledge of the pre-1955 // tradition, not yet verified against a primary source, so expect // corrections. // `privileged-feria` and `privileged-feria-major` are two real, distinct // tiers (confirmed by finding a plain Duplex saint, St. Gregory the Great, // outright winning against a Lenten Ember Saturday in the real Monastic // 1617 engine, which the original single-tier model wrongly forbade) — // see calendar/commemorations.ts's rules for `privileged-feria` (behaves // like `ordinary-sunday`: Duplex+ wins outright) vs `privileged-feria-major` // (behaves like `privileged-sunday`: never displaced at all, confirmed by // checking St. Mark, Duplex II. classis, merely commemorated rather than // winning within the Easter Octave). See data/calendar/temporal- // categories.yml for exactly which days fall in which tier — some of that // split (e.g. whether Pentecost's own Ember days share Lent's lesser tier // or Easter's major one) is still a reconstructed guess, not verified. // `privileged-feria-minor` is a third, weaker tier, added during the // December sanctoral pull after finding Advent's own ordinary (non-Ember) // ferias have real standing of their own in the live engine — a Simplex // saint (e.g. St. Bibiana, Dec 2) is merely commemorated there, while a // Semiduplex+ saint (e.g. St. Nicholas, Dec 6) wins outright with the // feria itself commemorated in return — neither of which the original // `ordinary-feria` fallback (any saint wins, nothing ever commemorated) // could represent. Lent's own ordinary ferias are documented as sharing // this same real-world property but are NOT switched to this tier yet — // unverified this session, left as `ordinary-feria` pending a future check. export type TemporalCategory = | 'ordinary-feria' | 'privileged-feria-minor' | 'privileged-feria' | 'privileged-feria-major' | 'ordinary-sunday' | 'privileged-sunday'; export interface SanctoralIdentity { id: string; name: string; rank: FeastClass; } export type DayWinner = | { kind: 'temporal'; id: string } | ({ kind: 'sanctoral'; // Set by calendar/vespers.ts's resolveEveningDay when this feast's // First Vespers is being anticipated this evening (i.e. this feast // belongs to *tomorrow*, but is winning tonight's Vespers/Compline). vespersFrom?: 'firstVespersOfTomorrow'; } & SanctoralIdentity); /** * A day can have more than one of these at once (a transferred feast can * displace a native saint who then also gets commemorated, alongside the * Sunday whose own occurrence pushed the transfer in the first place) — * hence a list, not a single flag. Extensible on purpose: a future * `{ kind: 'octave'; id: string }` variant joins this union once octaves * are modeled, without changing the shape callers already rely on. */ export type Commemoration = { kind: 'temporal'; id: string } | ({ kind: 'sanctoral' } & SanctoralIdentity); export interface LiturgicalDay { /** ISO date, e.g. "2026-08-09" */ date: string; weekday: Weekday; /** Real temporal-cycle season, computed via calendar/temporal.ts. */ season: Season; /** This day's own precedence class, before any sanctoral feast wins or loses against it. */ temporalCategory: TemporalCategory; /** Whichever office actually governs the day. */ winner: DayWinner; /** Everything else commemorated alongside the winner — see the doc comment on Commemoration. */ commemorations: Commemoration[]; }