calendar: add Christ the King, last Sunday of October
Deploy / deploy (push) Successful in 51s

A 1925 addition, so nowhere in the Monastic Tridentinum 1617 base this
project otherwise targets -- content sourced from "Monastic Divino
1930" instead, the Monastic-lineage version of the era that actually
carries it. Always wins outright (Duplex I. classis, confirmed live),
commemorating whichever Sunday after Pentecost it displaces -- same
additive-layering shape as applyOctaves/applyMarianSaturday, run last
so nested commemorations (e.g. a Simplex saint already commemorated
under that Sunday) survive underneath it.

Extended the Lauds psalmody-override mechanism (both the psalm groups
and, newly, the chapter/responsory/hymn/versicle bundle) to recognize
named temporal winners as unconditionally override-eligible, not just
duplex-majus+ sanctoral ones -- generalizing what had been a Marian-
Saturday-only special case. Suffrages needed their own explicit check
too: isDoubleOrHigher only ever looks at a sanctoral rank, and a named
temporal winner has no FeastClass to check at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 14:13:22 -04:00
parent 747a078580
commit 0404642614
13 changed files with 338 additions and 20 deletions
+3 -1
View File
@@ -11,7 +11,9 @@
export interface LaudsPsalmodyOverride {
id: string;
groups: { psalms: number[]; antiphon: Partial<Record<string, string>> }[];
canticle: { id: string; antiphon: Partial<Record<string, string>> };
// `split`: see hours/lauds.ts's PsalmodyBlock — no current override
// needs it, but the shape stays aligned with the plain weekday default.
canticle: { id: string; antiphon: Partial<Record<string, string>>; split?: number };
laudate: { antiphon: Partial<Record<string, string>> };
}
+50 -18
View File
@@ -4,7 +4,7 @@ import { resolveDay, isAtLeast } from '../calendar';
import { getDayLabel } from '../calendar/day-label';
import { getPsalmVerses } from '../psalter';
import { getCanticle } from './lauds-canticles';
import { getLaudsPsalmodyOverride } from './lauds-psalmody-overrides';
import { getLaudsPsalmodyOverride, type LaudsPsalmodyOverride } from './lauds-psalmody-overrides';
import { getOpeningVersicleId } from './opening-versicle';
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
import { isDoubleOrHigher } from './antiphon';
@@ -34,19 +34,25 @@ interface PsalmodyBlock {
}
const laudsAntiphons = laudsAntiphonsData as Record<Weekday, PsalmodyBlock>;
// Named temporal winners with real standing of their own, unconditionally
// override-eligible (no rank check — they aren't "a saint" competing with
// the weekday default the way one is, and neither carries a FeastClass at
// all since DayWinner's temporal variant never does). Grows one entry at
// a time as each is built, same as everywhere else in this codebase.
const ALWAYS_OVERRIDE_TEMPORAL_IDS = new Set(['marian-saturday', 'christ-the-king']);
/**
* A duplex-majus+ saint's own proper psalmody (per-feast, not per-Common —
* explicit choice), or Marian Saturday's (unconditional whenever it wins,
* no rank threshold — it isn't "a saint" competing with the weekday
* default the way one is). Falls back to `undefined` — the plain weekday
* default — for a duplex-majus+ saint with no override authored yet, same
* honest incremental-content convention as everywhere else in this
* codebase; it's not gated behind whether content exists, just behind
* whether it's *eligible* to override at all.
* explicit choice), or one of the named temporal feasts above. Falls back
* to `undefined` — the plain weekday default — for a duplex-majus+ saint
* with no override authored yet, same honest incremental-content
* convention as everywhere else in this codebase; it's not gated behind
* whether content exists, just behind whether it's *eligible* to override
* at all.
*/
function getPsalmodyOverrideFor(day: LiturgicalDay): PsalmodyBlock | undefined {
if (day.winner.kind === 'temporal' && day.winner.id === 'marian-saturday') {
return getLaudsPsalmodyOverride('marian-saturday');
function getPsalmodyOverrideFor(day: LiturgicalDay): LaudsPsalmodyOverride | undefined {
if (day.winner.kind === 'temporal' && ALWAYS_OVERRIDE_TEMPORAL_IDS.has(day.winner.id)) {
return getLaudsPsalmodyOverride(day.winner.id);
}
if (day.winner.kind === 'sanctoral' && isAtLeast(day.winner.rank, 'duplex-majus')) {
return getLaudsPsalmodyOverride(day.winner.id);
@@ -166,12 +172,31 @@ function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] {
return parts;
}
function resolveOffice(weekday: Weekday): ResolvedPart[] {
/** A duplex-majus+/Marian-Saturday override's own capitulum/responsory/
* hymn/versicle (data/propers/common/lauds-{part}-${id}.yml), when
* authored — most overrides only bother with the psalmody (see
* getPsalmodyOverrideFor) and leave this bundle at its plain weekday
* default, which is exactly what falls back here when the id-keyed
* capitulum doesn't exist (same dual-missing-status "not authored"
* signal used throughout this codebase, not a special case). */
function resolveOffice(day: LiturgicalDay): ResolvedPart[] {
const overrideId = getPsalmodyOverrideFor(day)?.id;
if (overrideId) {
const chapter = resolveCommon(`lauds-capitulum-${overrideId}`);
if (chapter.status.la !== 'missing' || chapter.status.en !== 'missing') {
return [
{ kind: 'chapter', text: chapter },
{ kind: 'responsory', text: resolveCommon(`lauds-responsory-${overrideId}`) },
{ kind: 'hymn', text: resolveCommon(`lauds-hymn-${overrideId}`) },
{ kind: 'versicle', text: resolveCommon(`lauds-versicle-${overrideId}`) },
];
}
}
return [
{ kind: 'chapter', text: resolveCommon(capitulumId(weekday)) },
{ kind: 'responsory', text: resolveCommon(`lauds-responsory-${weekday}`) },
{ kind: 'hymn', text: resolveCommon(`lauds-hymn-${weekday}`) },
{ kind: 'versicle', text: resolveCommon(`lauds-versicle-${weekday}`) },
{ kind: 'chapter', text: resolveCommon(capitulumId(day.weekday)) },
{ kind: 'responsory', text: resolveCommon(`lauds-responsory-${day.weekday}`) },
{ kind: 'hymn', text: resolveCommon(`lauds-hymn-${day.weekday}`) },
{ kind: 'versicle', text: resolveCommon(`lauds-versicle-${day.weekday}`) },
];
}
@@ -190,7 +215,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
case 'lauds-psalmody':
return resolvePsalmody(day);
case 'lauds-office':
return resolveOffice(day.weekday);
return resolveOffice(day);
case 'benedictus': {
const { incipit, full } = splitNamedAntiphon(getBenedictusAntiphon(day).text);
const opening = isDoubleOrHigher(day.winner) ? full : incipit;
@@ -204,7 +229,14 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
case 'day-collects':
return getDayCollects(day).map((text) => ({ kind: 'prayer' as const, text }));
case 'suffrages': {
if (part.omitOnDouble && isDoubleOrHigher(day.winner)) {
// isDoubleOrHigher only ever looks at a sanctoral rank — Christ the
// King is modeled as a named temporal winner (see
// ALWAYS_OVERRIDE_TEMPORAL_IDS above) with no FeastClass to check at
// all, so it needs its own explicit inclusion here. Confirmed live:
// "Suffragium{omittitur}" — the whole set drops, same as any other
// Double-or-higher day.
const isChristTheKing = day.winner.kind === 'temporal' && day.winner.id === 'christ-the-king';
if (part.omitOnDouble && (isDoubleOrHigher(day.winner) || isChristTheKing)) {
return [];
}
// Confirmed live: Our Lady's Saturday drops both "Of the Holy Cross"