From 74ddd61aee318f72aaadc764fd8bfd6d3fdd8324 Mon Sep 17 00:00:00 2001 From: Will Estes Date: Sat, 29 Aug 2026 07:35:22 -0400 Subject: [PATCH] Derive Prime/Terce/Sext/None antiphons from a saint's own Lauds set resolveMinorHourAntiphon fell straight from a (rarely-authored) dedicated per-hour proper file to the saint's generic Common-category antiphon, skipping the real proper text a saint may already have as its 5-antiphon Lauds/Vespers set. The reference engine's own getanthoras() mechanism derives exactly these four minor-hour antiphons from that same 5-antiphon set by fixed index (Prime/Terce/ Sext/None <- antiphons 1/2/3/5) whenever no dedicated per-hour proper exists; vu had no equivalent, so any minor hour fell back to the generic Common antiphon even when the feast's own text was already sitting right there in its Lauds override. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VFGHb4XMe6Wya4pGpPhEEi --- src/hours/resolve-common.ts | 54 +++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/src/hours/resolve-common.ts b/src/hours/resolve-common.ts index ce42767..690a924 100644 --- a/src/hours/resolve-common.ts +++ b/src/hours/resolve-common.ts @@ -7,6 +7,7 @@ import { resolveActiveOctave, activeOctavesFor, octaveGoverningPrivilegedDay, is import { getTemporalFeastRecord } from '../calendar/temporal-feasts'; import { isInTriduum } from '../calendar/temporal'; import { splitAntiphon, isDoubleOrHigher } from './antiphon'; +import { getLaudsSaintOverride } from './lauds-psalmody-overrides'; import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml'; type BilingualText = Partial>; @@ -356,19 +357,52 @@ function withPaschaltideAlleluia(resolved: ResolvedText, day: LiturgicalDay): Re return appendPaschaltideAlleluia(resolved); } +// The reference engine's own `getanthoras()` (specials.pl): when a saint +// has no dedicated per-hour minor-hour antiphon of its own, one is +// derived from that saint's 5-antiphon Lauds/Vespers set (`[Ant Laudes]`, +// which for most saints is `@:Ant Vespera`) by fixed index — Prime takes +// the first (the same antiphon as the first psalm-group), Terce/Sext the +// second/third, None the fifth (the Laudate antiphon); the fourth +// (canticle) antiphon is never used for a minor hour. vu's own +// `LaudsPsalmodyOverride` already stores that same 5-antiphon set in that +// exact order (`groups[0..2]`, `canticle`, `laudate`), so once a saint's +// Lauds proper is authored, no separate per-hour file is needed for this +// tier — same "mechanism first, content incrementally" pattern as +// everywhere else in this codebase. +const MINOR_HOUR_LAUDS_ANTIPHON_INDEX: Record = { prime: 0, terce: 1, sext: 2, none: 4 }; + +function deriveMinorHourAntiphonFromLauds(hourId: string, overrideId: string): ResolvedText | undefined { + const index = MINOR_HOUR_LAUDS_ANTIPHON_INDEX[hourId]; + if (index === undefined) { + return undefined; + } + const lauds = getLaudsSaintOverride(overrideId); + if (!lauds) { + return undefined; + } + const antiphon = index === 4 ? lauds.laudate.antiphon : lauds.groups[index]?.antiphon; + if (!antiphon) { + return undefined; + } + return verifiedText(antiphon); +} + /** A Little Hour's (or Prime's) plain per-weekday antiphon, overridden by * a duplex-majus+ feast's own proper (`${hourId}-antiphon-${id}`) when - * authored, then by that saint's shared Common (`${hourId}-antiphon- + * authored, then by that saint's own Lauds/Vespers antiphon set (see + * `deriveMinorHourAntiphonFromLauds` above — a real liturgical derivation, + * not a guess), then by that saint's shared Common (`${hourId}-antiphon- * ${minorHoursCommon}`, see SaintRecord's own doc comment) — tried first * in its Paschaltide-variant form (`${minorHoursCommon}-paschaltide`) * when the day falls in Paschaltide and that variant has been authored, - * per PASCHALTIDE_SEASONS's own doc comment — when a proper one hasn't - * been authored — same honest "not gated behind whether content exists, - * just eligible to override at all" fallback as every other override in - * this codebase: an eligible feast with none of these authored yet just - * falls through to the plain weekday default silently. The plain (non- - * `-paschaltide`) Common fallback itself still picks up a seasonal change - * in Paschaltide — see `withPaschaltideAlleluia`. + * per PASCHALTIDE_SEASONS's own doc comment — when neither of the first + * two has been authored — same honest "not gated behind whether content + * exists, just eligible to override at all" fallback as every other + * override in this codebase: an eligible feast with none of these + * authored yet just falls through to the plain weekday default silently. + * The plain (non-`-paschaltide`) Common fallback itself still picks up a + * seasonal change in Paschaltide — see `withPaschaltideAlleluia`. (The + * derived-from-Lauds tier does not get that treatment — see TODO.md.) */ export function resolveMinorHourAntiphon( hourId: string, @@ -381,6 +415,10 @@ export function resolveMinorHourAntiphon( if (proper.status.la !== 'missing' || proper.status.en !== 'missing') { return proper; } + const derived = deriveMinorHourAntiphonFromLauds(hourId, overrideId); + if (derived) { + return derived; + } const commonId = getSaintRecord(overrideId)?.minorHoursCommon; if (commonId) { if (PASCHALTIDE_SEASONS.has(day.season)) {