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:
@@ -1278,6 +1278,42 @@ draft-English antiphon (`tests/hours/resolve-common.test.ts`).
|
|||||||
simplex", live-verified) both apply before any part runs. See
|
simplex", live-verified) both apply before any part runs. See
|
||||||
`data/hours/vespers.yml`'s header and `tests/hours/vespers.test.ts`.
|
`data/hours/vespers.yml`'s header and `tests/hours/vespers.test.ts`.
|
||||||
|
|
||||||
|
- **Vespers cross-day commemoration now labeled, and feast-side privilege
|
||||||
|
now has a mechanism (2026-08-26)**: a cross-day commemoration
|
||||||
|
(`Commemoration`'s `vespersNote: 'today' | 'tomorrow'`, set only by
|
||||||
|
`calendar/vespers.ts`'s two `commemorationOfDisplaced*` functions) now
|
||||||
|
renders as "(of today)"/"(of tomorrow)" in English, "de præcedenti"/"de
|
||||||
|
sequenti" in Latin (`calendar/day-label.ts`) — the real Divino Afflatu
|
||||||
|
phrasing, per `horascommon.pl`. Also added `forbidsSuccessorCommemoration`
|
||||||
|
to both `SaintRecord` (`calendar/feasts.ts`) and `TemporalFeastRecord`
|
||||||
|
(`calendar/temporal-feasts.ts`) — the feast-side mirror of the day-side
|
||||||
|
privilege `calendar/commemorations.ts` already modeled (a privileged
|
||||||
|
Sunday/major-feria can refuse a weak commemoration *back*; nothing
|
||||||
|
previously let a high-ranked feast refuse to commemorate *forward* at
|
||||||
|
its own kept Second Vespers). Deliberately a flag, not a ported
|
||||||
|
rank-threshold formula (the reference engine's own version is a messy
|
||||||
|
fractional-rank matrix) — set only after checking a specific date
|
||||||
|
against the live engine, same convention as `vigilOf`.
|
||||||
|
- The concrete case that motivated this: **Most Precious Blood (July 1,
|
||||||
|
Duplex I classis) vs. the Feast of the Sacred Heart** (Easter + 68
|
||||||
|
days), which only ever fall adjacent in a very-late-Easter year
|
||||||
|
(Easter in late April). Read the Perl variables carefully (initial
|
||||||
|
pass here got this backwards): it's **Sacred Heart's own kept Second
|
||||||
|
Vespers that refuses to commemorate Precious Blood** the next
|
||||||
|
evening ("Vespera de præcedenti; nihil de sequenti", `$winner` =
|
||||||
|
Sacred Heart/`Pent02-5` = *today* at that point in the branch,
|
||||||
|
`$cwinner` = Precious Blood/`07-01` = *tomorrow*), not the reverse.
|
||||||
|
The other direction — Precious Blood's own Vespers commemorating a
|
||||||
|
Sacred Heart that happens to fall the day *after* it, the other rare
|
||||||
|
late-Easter arrangement — has no matching carve-out in the reference
|
||||||
|
engine at all, so `forbidsSuccessorCommemoration` is deliberately set
|
||||||
|
only on `sacred-heart.yml`, not `most-precious-blood.yml`.
|
||||||
|
- Still open, separately: the reverse gap remains in daytime
|
||||||
|
`decideOccurrence` (`calendar/commemorations.ts`) — a sanctoral
|
||||||
|
winner there unconditionally gets its displaced Sunday/feria
|
||||||
|
commemorated back, regardless of the winner's own rank. Not addressed
|
||||||
|
here; this pass only covered the Vespers cross-day direction.
|
||||||
|
|
||||||
### Matins ordo — mechanism built, small content slice (2026-08)
|
### Matins ordo — mechanism built, small content slice (2026-08)
|
||||||
|
|
||||||
`src/hours/matins.ts` was the last stub hour. Built as a genuinely different
|
`src/hours/matins.ts` was the last stub hour. Built as a genuinely different
|
||||||
|
|||||||
@@ -376,6 +376,19 @@ function octaveCommemorationLabel(octave: ActiveOctave): Bi {
|
|||||||
return octaveCoreName(octave);
|
return octaveCoreName(octave);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Real Divino Afflatu phrasing for a commemoration that only exists
|
||||||
|
* because Vespers/Compline crossed a day boundary — "commemoratio de
|
||||||
|
* præcedenti" (today, riding along on tomorrow's anticipated First
|
||||||
|
* Vespers) or "commemoratio de sequenti" (tomorrow, riding along on
|
||||||
|
* today's kept Second Vespers), per horascommon.pl. See
|
||||||
|
* `Commemoration`'s `vespersNote` field (calendar/types.ts) and
|
||||||
|
* calendar/vespers.ts's two `commemorationOfDisplaced*` functions, the
|
||||||
|
* only place that sets it. */
|
||||||
|
const CROSS_DAY_VESPERS_LABELS: Record<'today' | 'tomorrow', Bi> = {
|
||||||
|
today: bi('of today', 'de præcedenti'),
|
||||||
|
tomorrow: bi('of tomorrow', 'de sequenti'),
|
||||||
|
};
|
||||||
|
|
||||||
/** Every commemoration on `day`, resolved to display strings and grouped
|
/** Every commemoration on `day`, resolved to display strings and grouped
|
||||||
* by kind — the single place that decides both "is this commemoration
|
* by kind — the single place that decides both "is this commemoration
|
||||||
* eligible to show at all here" and "how is it formatted", so every
|
* eligible to show at all here" and "how is it formatted", so every
|
||||||
@@ -425,7 +438,12 @@ function collectCommemorations(
|
|||||||
const octaves: Bi[] = [];
|
const octaves: Bi[] = [];
|
||||||
for (const c of day.commemorations) {
|
for (const c of day.commemorations) {
|
||||||
if (c.kind === 'sanctoral') {
|
if (c.kind === 'sanctoral') {
|
||||||
sanctoral.push(biFallback(c.name));
|
const name = biFallback(c.name);
|
||||||
|
sanctoral.push(
|
||||||
|
c.vespersNote
|
||||||
|
? bi(`${name.en} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].en})`, `${name.la} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].la})`)
|
||||||
|
: name,
|
||||||
|
);
|
||||||
} else if (c.kind === 'temporal') {
|
} else if (c.kind === 'temporal') {
|
||||||
if (c.id === temporalId) {
|
if (c.id === temporalId) {
|
||||||
temporal.push(anchorDayName(day, false) ?? temporalLabel(day));
|
temporal.push(anchorDayName(day, false) ?? temporalLabel(day));
|
||||||
|
|||||||
@@ -88,6 +88,27 @@ export interface SaintRecord {
|
|||||||
* (rather than guessing from date adjacency alone) and skip
|
* (rather than guessing from date adjacency alone) and skip
|
||||||
* commemorating the Vigil redundantly alongside its own feast. */
|
* commemorating the Vigil redundantly alongside its own feast. */
|
||||||
vigilOf?: string;
|
vigilOf?: string;
|
||||||
|
/** This feast, when it keeps its own Second Vespers (see
|
||||||
|
* calendar/vespers.ts's keepsOwnSecondVespers), refuses to commemorate
|
||||||
|
* whatever wins the following day there — the feast-side mirror of a
|
||||||
|
* privileged Sunday/major-feria refusing a weak commemoration *back*
|
||||||
|
* (calendar/commemorations.ts), which vu had no equivalent of at all
|
||||||
|
* until this field. Modeled as an explicit per-feast flag rather than
|
||||||
|
* porting the reference engine's fractional-rank privilege matrix
|
||||||
|
* (crank/rank thresholds, a "Festum Domini" distinction, an outright
|
||||||
|
* hardcoded pair exception for Most Precious Blood vs. the Sacred
|
||||||
|
* Heart on the rare late-Easter date they collide) — that matches this
|
||||||
|
* repo's own "explicit per-feast fact, verified against the live
|
||||||
|
* engine one date at a time" convention (same as `vigilOf` above)
|
||||||
|
* rather than a general formula applied blanket and hoped correct.
|
||||||
|
* Absent (the default) preserves today's status quo: any sanctoral
|
||||||
|
* successor gets commemorated regardless of rank. Not yet set on any
|
||||||
|
* saint — add only after checking a specific date against the live
|
||||||
|
* reference engine, not speculatively for every `duplex-1-classis`
|
||||||
|
* feast. NOT a fit for the Precious Blood/Sacred Heart case itself
|
||||||
|
* (that exclusion is specific to one neighbor, not "never commemorate
|
||||||
|
* anyone" — see this field's own doc comment above) — see TODO.md. */
|
||||||
|
forbidsSuccessorCommemoration?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanctoralCalendar = sanctoralCalendarData as { days: Record<string, string[]> };
|
const sanctoralCalendar = sanctoralCalendarData as { days: Record<string, string[]> };
|
||||||
|
|||||||
@@ -37,6 +37,22 @@ export interface TemporalFeastRecord {
|
|||||||
* every other movable feast instead loses to an equal-or-higher-ranked
|
* every other movable feast instead loses to an equal-or-higher-ranked
|
||||||
* sanctoral winner and is commemorated alongside it instead. */
|
* sanctoral winner and is commemorated alongside it instead. */
|
||||||
unconditional?: boolean;
|
unconditional?: boolean;
|
||||||
|
/** The temporal-feast sibling of calendar/feasts.ts's SaintRecord field
|
||||||
|
* of the same name -- see that field's own doc comment for the full
|
||||||
|
* rationale (a feast-side mirror of the day-side privilege
|
||||||
|
* calendar/commemorations.ts already models, deliberately a flag
|
||||||
|
* rather than a ported rank-threshold formula). Set on sacred-heart.yml
|
||||||
|
* specifically -- live source (horascommon.pl, github #4586): on the
|
||||||
|
* rare late-Easter date the movable Feast of the Sacred Heart falls the
|
||||||
|
* day immediately before the fixed Most Precious Blood (July 1), Sacred
|
||||||
|
* Heart's own kept Second Vespers explicitly does *not* commemorate
|
||||||
|
* Precious Blood the next evening ("Vespera de præcedenti; nihil de
|
||||||
|
* sequenti") -- the reverse direction (Precious Blood's own Vespers
|
||||||
|
* commemorating a Sacred Heart that falls *after* it, the other rare
|
||||||
|
* late-Easter arrangement) has no such carve-out in the reference
|
||||||
|
* engine, so this flag is intentionally one-directional, not mirrored
|
||||||
|
* onto most-precious-blood.yml. */
|
||||||
|
forbidsSuccessorCommemoration?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const temporalFeastModules = import.meta.glob<{ default: TemporalFeastRecord }>(
|
const temporalFeastModules = import.meta.glob<{ default: TemporalFeastRecord }>(
|
||||||
|
|||||||
+12
-1
@@ -168,7 +168,18 @@ export type DayWinner =
|
|||||||
*/
|
*/
|
||||||
export type Commemoration =
|
export type Commemoration =
|
||||||
| { kind: 'temporal'; id: string }
|
| { kind: 'temporal'; id: string }
|
||||||
| ({ kind: 'sanctoral' } & SanctoralIdentity)
|
| ({
|
||||||
|
kind: 'sanctoral';
|
||||||
|
// Set by calendar/vespers.ts's resolveEveningDay when this
|
||||||
|
// commemoration exists only because Vespers/Compline crossed a day
|
||||||
|
// boundary (First Vespers anticipation, or a kept Second Vespers
|
||||||
|
// still noting the displaced neighbor) — 'today' for the real
|
||||||
|
// Divino Afflatu "commemoratio de præcedenti" case, 'tomorrow' for
|
||||||
|
// "commemoratio de sequenti". Left unset for an ordinary same-day
|
||||||
|
// commemoration (a real collision, an octave day, etc.), which
|
||||||
|
// carries no such qualifier.
|
||||||
|
vespersNote?: 'today' | 'tomorrow';
|
||||||
|
} & SanctoralIdentity)
|
||||||
| { kind: 'octave'; id: string; name: string };
|
| { kind: 'octave'; id: string; name: string };
|
||||||
|
|
||||||
export interface LiturgicalDay {
|
export interface LiturgicalDay {
|
||||||
|
|||||||
+21
-4
@@ -14,6 +14,7 @@ import { addDays } from './date-math';
|
|||||||
import { easterOffsetOf } from './temporal';
|
import { easterOffsetOf } from './temporal';
|
||||||
import { isAtLeast } from './commemorations';
|
import { isAtLeast } from './commemorations';
|
||||||
import { getSaintRecord } from './feasts';
|
import { getSaintRecord } from './feasts';
|
||||||
|
import { getTemporalFeastRecord } from './temporal-feasts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fixed/movable "feasts of the Lord" load-bearing elsewhere in this app
|
* 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') {
|
if (loser.kind !== 'sanctoral' || loser.rank === 'simplex') {
|
||||||
return undefined;
|
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
|
* non-Simplex loser: Jul 25 -> 26 (St. James, Duplex II. classis, kept
|
||||||
* second Vespers regardless; St. Anne, Duplex, loses the evening but is
|
* second Vespers regardless; St. Anne, Duplex, loses the evening but is
|
||||||
* still commemorated).
|
* 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') {
|
if (loser.kind !== 'sanctoral') {
|
||||||
return undefined;
|
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 {
|
function tagVespersFrom(tomorrow: LiturgicalDay): LiturgicalDay {
|
||||||
@@ -196,7 +213,7 @@ export function resolveEveningDay(isoDate: string): LiturgicalDay {
|
|||||||
// long as it's a real sanctoral winner — see
|
// long as it's a real sanctoral winner — see
|
||||||
// commemorationOfDisplacedSuccessor's own doc comment for why this
|
// commemorationOfDisplacedSuccessor's own doc comment for why this
|
||||||
// direction doesn't exclude Simplex the way the reverse one does.
|
// 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;
|
return commemoration ? { ...today, commemorations: [...today.commemorations, commemoration] } : today;
|
||||||
}
|
}
|
||||||
if (!hasFirstVespers(tomorrow)) {
|
if (!hasFirstVespers(tomorrow)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user