From 81440de8324b3b2ac9797c7985327114c62d46ff Mon Sep 17 00:00:00 2001 From: Will Estes Date: Mon, 10 Aug 2026 13:50:24 -0400 Subject: [PATCH] hours: add the day's-collect resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Terce/Sext/None's Oratio is the day's own Proper-of-season/-saints collect, not a fixed text — getDayCollect() resolves it from the LiturgicalDay's winner (temporal id or sanctoral saint's propers), falling through to an honest "missing" status for saints that don't have authored propers yet. Co-Authored-By: Claude Sonnet 5 --- src/calendar/feasts.ts | 9 ++++++++- src/hours/resolve-common.ts | 32 +++++++++++++++++++++++++++--- tests/hours/resolve-common.test.ts | 27 +++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 tests/hours/resolve-common.test.ts diff --git a/src/calendar/feasts.ts b/src/calendar/feasts.ts index 6a761f4..6aec46a 100644 --- a/src/calendar/feasts.ts +++ b/src/calendar/feasts.ts @@ -12,7 +12,7 @@ import type { FeastClass } from './types'; import sanctoralCalendarData from '../data/calendar/sanctoral-calendar.yml'; -interface SaintRecord { +export interface SaintRecord { id: string; name: string; rank: FeastClass; @@ -38,6 +38,13 @@ export interface SanctoralCandidate { } /** Which saint(s) (if any) are assigned to this date. `isoDate`'s year is ignored — the sanctoral cycle repeats every civil year. */ +/** The saint's full record — used when something more than id/name/rank is + * needed, e.g. hours/resolve-common.ts's getDayCollect looking up whether + * a real collect has been authored for a winning saint. */ +export function getSaintRecord(id: string): SaintRecord | undefined { + return saintsById.get(id); +} + export function getSanctoralCandidatesFor(isoDate: string): SanctoralCandidate[] { const monthDay = isoDate.slice(5); const ids = sanctoralCalendar.days[monthDay] ?? []; diff --git a/src/hours/resolve-common.ts b/src/hours/resolve-common.ts index 72e9c93..1d427d2 100644 --- a/src/hours/resolve-common.ts +++ b/src/hours/resolve-common.ts @@ -1,12 +1,38 @@ import type { ResolvedText } from './types'; -import { getCommonProper } from '../propers'; +import type { LiturgicalDay } from '../calendar/types'; +import type { ProperText } from '../propers'; +import { getCommonProper, getTemporalProper } from '../propers'; +import { getSaintRecord } from '../calendar/feasts'; import { splitAntiphon } from './antiphon'; +function toResolvedText(proper: ProperText): ResolvedText { + return { text: proper.text, status: proper.status, citation: proper.citation }; +} + /** Shared by every hour's resolver — looks up a common proper by id and * shapes it as a ResolvedText. */ export function resolveCommon(id: string): ResolvedText { - const proper = getCommonProper(id); - return { text: proper.text, status: proper.status, citation: proper.citation }; + return toResolvedText(getCommonProper(id)); +} + +/** + * The day's own collect — real for the vast majority of days (a temporal + * winner always resolves, since all 52 Sunday collects are authored and + * ferias inherit the governing Sunday's via calendar/temporal-id.ts), and + * honestly "missing" for the handful of days a saint wins outright until + * that saint's own propers are authored (`saints/.yml`'s `propers` + * field is still `null` for all of them today) — same "resolve as + * pending" convention as everywhere else, not a special case to handle. + */ +export function getDayCollect(day: LiturgicalDay): ResolvedText { + if (day.winner.kind === 'temporal') { + return toResolvedText(getTemporalProper(`${day.winner.id}-collect`)); + } + const saint = getSaintRecord(day.winner.id); + if (saint?.propers) { + return resolveCommon(`${saint.propers}-collect`); + } + return { text: {}, status: { la: 'missing', en: 'missing' } }; } export function verifiedText(text: Partial>): ResolvedText { diff --git a/tests/hours/resolve-common.test.ts b/tests/hours/resolve-common.test.ts new file mode 100644 index 0000000..c62f0a2 --- /dev/null +++ b/tests/hours/resolve-common.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { resolveDay } from '../../src/calendar'; +import { getDayCollect } from '../../src/hours/resolve-common'; + +describe('getDayCollect', () => { + it('resolves a real, verified collect for an ordinary Sunday', () => { + const day = resolveDay('2026-08-09'); // post-pentecost-11 + const collect = getDayCollect(day); + expect(collect.status.la).toBe('verified'); + expect(collect.status.en).toBe('verified'); + expect(collect.text.en).toContain('Let us pray.'); + }); + + it('resolves a real, verified collect for a ferial day via its governing Sunday', () => { + const day = resolveDay('2026-06-14'); // post-pentecost-03's Sunday + const collect = getDayCollect(day); + expect(collect.status.la).toBe('verified'); + }); + + it('resolves as honestly missing for a saint with no authored propers yet', () => { + const day = resolveDay('2026-08-10'); // St. Lawrence wins outright, propers: null + expect(day.winner.kind).toBe('sanctoral'); + const collect = getDayCollect(day); + expect(collect.status.la).toBe('missing'); + expect(collect.status.en).toBe('missing'); + }); +});