Model cross-day Vespers commemorations, fix First Vespers threshold
Deploy / deploy (push) Successful in 1m1s
Deploy / deploy (push) Successful in 1m1s
Whichever of today/tomorrow doesn't govern a given evening's Vespers
can still be commemorated there, unless it's Simplex (or a bare
temporal winner with no standing of its own) — live-verified in both
directions against the real Monastic 1617 engine: Ss. Cyriacus/Largus/
Smaragdus (Semiduplex) commemorated when Sunday claims First Vespers
over them; St. Anne commemorated even though St. James (Duplex II.
classis) keeps his own Second Vespers outright; St. Donatus (Simplex)
gets nothing ("nihil de præcedenti") in the same situation Cyriacus/
etc. did get a commemoration. The isMajorFixedFeastOfTheLord override
(Christmas/Epiphany/Candlemas/Easter/Ascension/Corpus Christi/Sacred
Heart) deliberately doesn't inherit this yet — no live data on whether
those suppress it the way privileged-sunday/privileged-feria-major do
during the day.
Also fixes hasFirstVespers's threshold, found wrong while gathering
the above evidence: it required duplex-majus+, but Ss. Cyriacus/etc.
(plain Semiduplex) actually claims First Vespers over St. Donatus in
the live engine. Lowered to semiduplex, matching the sibling
privileged-feria-minor threshold already used just below it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+69
-10
@@ -8,7 +8,7 @@
|
|||||||
// Same reconstruction caveat as commemorations.ts/temporal-categories.yml:
|
// Same reconstruction caveat as commemorations.ts/temporal-categories.yml:
|
||||||
// best-effort, not sourced from a primary text for this era, expect
|
// best-effort, not sourced from a primary text for this era, expect
|
||||||
// corrections.
|
// corrections.
|
||||||
import type { LiturgicalDay } from './types';
|
import type { Commemoration, DayWinner, LiturgicalDay } from './types';
|
||||||
import { resolveDay } from './index';
|
import { resolveDay } from './index';
|
||||||
import { addDays } from './date-math';
|
import { addDays } from './date-math';
|
||||||
import { easterOffsetOf } from './temporal';
|
import { easterOffsetOf } from './temporal';
|
||||||
@@ -37,7 +37,13 @@ function hasFirstVespers(day: LiturgicalDay): boolean {
|
|||||||
if (day.weekday === 'sunday' || isMajorFixedFeastOfTheLord(day.date)) {
|
if (day.weekday === 'sunday' || isMajorFixedFeastOfTheLord(day.date)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return day.winner.kind === 'sanctoral' && isAtLeast(day.winner.rank, 'duplex-majus');
|
// Live-verified (2026-08): Ss. Cyriacus/Largus/Smaragdus (Aug 8, plain
|
||||||
|
// Semiduplex) claims First Vespers over St. Donatus's Aug 7 — "Vespera
|
||||||
|
// de sequenti" in the real Monastic 1617 engine — so Semiduplex clears
|
||||||
|
// this bar, not duplex-majus as originally guessed (unverified at the
|
||||||
|
// time). Matches keepsOwnSecondVespers's own privileged-feria-minor
|
||||||
|
// threshold just below, which uses the same cutoff.
|
||||||
|
return day.winner.kind === 'sanctoral' && isAtLeast(day.winner.rank, 'semiduplex');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Does this day keep its own Second Vespers regardless of what tomorrow is? */
|
/** Does this day keep its own Second Vespers regardless of what tomorrow is? */
|
||||||
@@ -54,11 +60,55 @@ function keepsOwnSecondVespers(day: LiturgicalDay): boolean {
|
|||||||
return day.winner.kind === 'sanctoral' && isAtLeast(day.winner.rank, 'duplex-2-classis');
|
return day.winner.kind === 'sanctoral' && isAtLeast(day.winner.rank, 'duplex-2-classis');
|
||||||
}
|
}
|
||||||
|
|
||||||
function anticipated(tomorrow: LiturgicalDay): LiturgicalDay {
|
/**
|
||||||
if (tomorrow.winner.kind === 'sanctoral') {
|
* Whichever of today/tomorrow does *not* govern tonight's Vespers is still
|
||||||
return { ...tomorrow, winner: { ...tomorrow.winner, vespersFrom: 'firstVespersOfTomorrow' } };
|
* commemorated there, unless it's Simplex (or a bare temporal winner, with
|
||||||
|
* no standing of its own — same "no standing to defend" idea
|
||||||
|
* commemorations.ts's `ordinary-feria` case already uses during the day).
|
||||||
|
* Live-verified both directions, 2026-08:
|
||||||
|
* - Aug 8 -> 9 (Ss. Cyriaci/Largi/Smaragdi, Semiduplex, before an
|
||||||
|
* ordinary Sunday): today loses the evening, today is still
|
||||||
|
* commemorated ("commemoratio de præcedenti").
|
||||||
|
* - Jul 25 -> 26 (St. James, Duplex II. classis, kept second Vespers
|
||||||
|
* regardless): tomorrow (St. Anne) loses the evening but is still
|
||||||
|
* commemorated ("commemoratio de sequenti").
|
||||||
|
* - Aug 7 -> 8 (St. Donatus, Simplex, before Ss. Cyriaci/etc.): today
|
||||||
|
* loses the evening and is *not* commemorated ("nihil de
|
||||||
|
* præcedenti") — the one live case pinning down the Simplex
|
||||||
|
* exclusion specifically, not just inferred from the daytime rule.
|
||||||
|
* Not yet checked against a case where the losing side is Simplex on the
|
||||||
|
* *winning* side of the pair (i.e. today keeps its own Vespers and
|
||||||
|
* tomorrow is the Simplex one) — inferred to behave the same way by
|
||||||
|
* symmetry with the daytime rule, not independently live-verified.
|
||||||
|
*/
|
||||||
|
function commemorationOf(loser: DayWinner): Commemoration | undefined {
|
||||||
|
if (loser.kind !== 'sanctoral' || loser.rank === 'simplex') {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
return tomorrow;
|
return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank };
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagVespersFrom(tomorrow: LiturgicalDay): LiturgicalDay {
|
||||||
|
if (tomorrow.winner.kind !== 'sanctoral') {
|
||||||
|
return tomorrow;
|
||||||
|
}
|
||||||
|
return { ...tomorrow, winner: { ...tomorrow.winner, vespersFrom: 'firstVespersOfTomorrow' } };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The ordinary anticipation path: tag tomorrow's identity, then layer in
|
||||||
|
* today's commemoration if it's eligible (see commemorationOf). Not used
|
||||||
|
* by the isMajorFixedFeastOfTheLord override below — whether Christmas/
|
||||||
|
* Easter/etc. commemorate the day they're displacing is a separate,
|
||||||
|
* unverified question (these may well suppress it the way
|
||||||
|
* privileged-sunday/privileged-feria-major do during the day — see
|
||||||
|
* commemorations.ts), so it deliberately doesn't inherit this behavior. */
|
||||||
|
function anticipated(today: LiturgicalDay, tomorrow: LiturgicalDay): LiturgicalDay {
|
||||||
|
const tagged = tagVespersFrom(tomorrow);
|
||||||
|
const commemoration = commemorationOf(today.winner);
|
||||||
|
if (!commemoration) {
|
||||||
|
return tagged;
|
||||||
|
}
|
||||||
|
return { ...tagged, commemorations: [...tagged.commemorations, commemoration] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,7 +117,11 @@ function anticipated(tomorrow: LiturgicalDay): LiturgicalDay {
|
|||||||
* Vespers *and* today doesn't keep its own Second Vespers regardless — in
|
* Vespers *and* today doesn't keep its own Second Vespers regardless — in
|
||||||
* which case returns tomorrow's `LiturgicalDay`, with `vespersFrom:
|
* which case returns tomorrow's `LiturgicalDay`, with `vespersFrom:
|
||||||
* 'firstVespersOfTomorrow'` set on its winner (if sanctoral) so callers
|
* 'firstVespersOfTomorrow'` set on its winner (if sanctoral) so callers
|
||||||
* can tell the difference from an ordinary day.
|
* can tell the difference from an ordinary day. Either way, whichever side
|
||||||
|
* doesn't govern the evening is folded into the returned day's own
|
||||||
|
* `commemorations` when it's eligible (see commemorationOf) — except
|
||||||
|
* under the isMajorFixedFeastOfTheLord override just below, which doesn't
|
||||||
|
* apply that at all yet.
|
||||||
*
|
*
|
||||||
* One deliberate absolute exception, caught by a failing test: a day on
|
* One deliberate absolute exception, caught by a failing test: a day on
|
||||||
* `isMajorFixedFeastOfTheLord`'s list always wins tomorrow's Vespers,
|
* `isMajorFixedFeastOfTheLord`'s list always wins tomorrow's Vespers,
|
||||||
@@ -83,13 +137,18 @@ export function resolveEveningDay(isoDate: string): LiturgicalDay {
|
|||||||
const tomorrow = resolveDay(addDays(isoDate, 1));
|
const tomorrow = resolveDay(addDays(isoDate, 1));
|
||||||
|
|
||||||
if (isMajorFixedFeastOfTheLord(tomorrow.date)) {
|
if (isMajorFixedFeastOfTheLord(tomorrow.date)) {
|
||||||
return anticipated(tomorrow);
|
return tagVespersFrom(tomorrow);
|
||||||
}
|
}
|
||||||
if (keepsOwnSecondVespers(today)) {
|
if (keepsOwnSecondVespers(today)) {
|
||||||
return today;
|
// Today wins outright, but tomorrow can still be commemorated here if
|
||||||
|
// it clears its own bar (live-verified: St. James, Duplex II.
|
||||||
|
// classis, keeps his own Second Vespers on Jul 25 and still
|
||||||
|
// commemorates St. Anne's Jul 26 — see commemorationOf).
|
||||||
|
const commemoration = commemorationOf(tomorrow.winner);
|
||||||
|
return commemoration ? { ...today, commemorations: [...today.commemorations, commemoration] } : today;
|
||||||
}
|
}
|
||||||
if (!hasFirstVespers(tomorrow)) {
|
if (!hasFirstVespers(tomorrow)) {
|
||||||
return today;
|
return today;
|
||||||
}
|
}
|
||||||
return anticipated(tomorrow);
|
return anticipated(today, tomorrow);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,4 +39,37 @@ describe('resolveEveningDay', () => {
|
|||||||
expect(day.winner.vespersFrom).toBe('firstVespersOfTomorrow');
|
expect(day.winner.vespersFrom).toBe('firstVespersOfTomorrow');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The three cases below are live-verified against the real Monastic
|
||||||
|
// Tridentinum 1617 engine (see commemorationOf's own doc comment in
|
||||||
|
// calendar/vespers.ts) — "commemoratio de præcedenti/sequenti" vs.
|
||||||
|
// "nihil de præcedenti" in the raw output, not just inferred from the
|
||||||
|
// daytime rule.
|
||||||
|
it('a Semiduplex loser is still commemorated when tomorrow claims First Vespers', () => {
|
||||||
|
// Aug 8, 2026 (Ss. Cyriacus/Largus/Smaragdus, Semiduplex) loses the
|
||||||
|
// evening to Aug 9 (Sunday); still commemorated.
|
||||||
|
const day = resolveEveningDay('2026-08-08');
|
||||||
|
expect(day.date).not.toBe('2026-08-08');
|
||||||
|
expect(day.commemorations).toContainEqual(
|
||||||
|
expect.objectContaining({ kind: 'sanctoral', id: 'ss-cyriacus-largus-and-smaragdus' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a Simplex loser is not commemorated when tomorrow claims First Vespers', () => {
|
||||||
|
// Aug 7, 2026 (St. Donatus, Simplex) loses the evening to Aug 8
|
||||||
|
// (Ss. Cyriacus/Largus/Smaragdus, now Semiduplex-threshold-eligible
|
||||||
|
// for First Vespers — see hasFirstVespers); not commemorated.
|
||||||
|
const day = resolveEveningDay('2026-08-07');
|
||||||
|
expect(day.date).not.toBe('2026-08-07');
|
||||||
|
expect(day.commemorations.some((c) => c.kind === 'sanctoral' && c.id === 'st-donatus')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a winning day still commemorates a non-Simplex loser on the other side', () => {
|
||||||
|
// Jul 25, 2026 (St. James the Greater, Duplex II. classis) keeps its
|
||||||
|
// own Second Vespers outright, but still commemorates Jul 26
|
||||||
|
// (St. Anne).
|
||||||
|
const day = resolveEveningDay('2026-07-25');
|
||||||
|
expect(day.date).toBe('2026-07-25');
|
||||||
|
expect(day.commemorations).toContainEqual(expect.objectContaining({ kind: 'sanctoral', id: 'st-anne' }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user