Label Vespers cross-day commemorations and add feast-side privilege

A commemoration that only exists because Vespers/Compline crossed a day
boundary now renders as "(of today)"/"(of tomorrow)" in English and the
real Divino Afflatu "de præcedenti"/"de sequenti" in Latin, instead of
showing silently with no indication it's a cross-day artifact.

Also adds forbidsSuccessorCommemoration to SaintRecord and
TemporalFeastRecord: a feast can now refuse to commemorate whatever wins
the next day at its own kept Second Vespers, the feast-side mirror of the
day-side privilege calendar/commemorations.ts already modeled. Left unset
everywhere for now (a no-op) -- it exists so the next commit's Sacred
Heart record has somewhere to carry the real, live-sourced exclusion
against Most Precious Blood.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
This commit is contained in:
2026-08-26 15:20:10 -04:00
parent 9067faf1fd
commit 2f7b421558
6 changed files with 125 additions and 6 deletions
+21 -4
View File
@@ -14,6 +14,7 @@ import { addDays } from './date-math';
import { easterOffsetOf } from './temporal';
import { isAtLeast } from './commemorations';
import { getSaintRecord } from './feasts';
import { getTemporalFeastRecord } from './temporal-feasts';
/**
* Fixed/movable "feasts of the Lord" load-bearing elsewhere in this app
@@ -84,7 +85,7 @@ function commemorationOfDisplacedPredecessor(loser: DayWinner): Commemoration |
if (loser.kind !== 'sanctoral' || loser.rank === 'simplex') {
return undefined;
}
return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank };
return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank, vespersNote: 'today' };
}
/**
@@ -98,12 +99,28 @@ function commemorationOfDisplacedPredecessor(loser: DayWinner): Commemoration |
* non-Simplex loser: Jul 25 -> 26 (St. James, Duplex II. classis, kept
* second Vespers regardless; St. Anne, Duplex, loses the evening but is
* still commemorated).
*
* `winner` (today's own, the one actually keeping this evening) can
* refuse the commemoration outright via its own record's
* `forbidsSuccessorCommemoration` — the feast-side mirror of the
* day-side privilege calendar/commemorations.ts already models (see that
* field's own doc comment, on both SaintRecord and TemporalFeastRecord,
* for why this is a flag rather than a rank threshold). Checked on
* whichever kind of record `winner` actually is — set today only on
* `sacred-heart` (a `temporal` winner), the live-verified case that
* motivated this field; every sanctoral saint is still unset, a no-op
* until a specific date is verified and flagged.
*/
function commemorationOfDisplacedSuccessor(loser: DayWinner): Commemoration | undefined {
function commemorationOfDisplacedSuccessor(winner: DayWinner, loser: DayWinner): Commemoration | undefined {
if (loser.kind !== 'sanctoral') {
return undefined;
}
return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank };
const forbids =
winner.kind === 'sanctoral' ? getSaintRecord(winner.id)?.forbidsSuccessorCommemoration : getTemporalFeastRecord(winner.id)?.forbidsSuccessorCommemoration;
if (forbids) {
return undefined;
}
return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank, vespersNote: 'tomorrow' };
}
function tagVespersFrom(tomorrow: LiturgicalDay): LiturgicalDay {
@@ -196,7 +213,7 @@ export function resolveEveningDay(isoDate: string): LiturgicalDay {
// long as it's a real sanctoral winner — see
// commemorationOfDisplacedSuccessor's own doc comment for why this
// direction doesn't exclude Simplex the way the reverse one does.
const commemoration = commemorationOfDisplacedSuccessor(tomorrow.winner);
const commemoration = commemorationOfDisplacedSuccessor(today.winner, tomorrow.winner);
return commemoration ? { ...today, commemorations: [...today.commemorations, commemoration] } : today;
}
if (!hasFirstVespers(tomorrow)) {