Compare commits

...

2 Commits

Author SHA1 Message Date
will 5ae1f3a3e0 Fix Vespers status wrongly saying "Second Vespers" when a Sunday anticipates it
Deploy / deploy (push) Successful in 1m29s
getVespersStatusLabel detected First-Vespers anticipation only via
winner.vespersFrom, a tag only ever set for a sanctoral tomorrow-winner
(tagVespersFrom). When Vespers anticipates a Sunday that wins its own
day outright (a temporal winner, e.g. 2026-08-29 anticipating 2026-08-30
since the Decollation of St. John Baptist is below the duplex-2-classis
floor that would let it keep its own evening), the tag never gets set
and the status wrongly read "Second Vespers (of today)" even though the
whole page was showing tomorrow's identity.

Comparing the resolved day's date against the originally-requested date
detects anticipation uniformly regardless of winner kind, so this drops
the vespersFrom dependency entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 06:10:52 -04:00
will 95901ef56a Always list the day's actual winner first in getDayLabel
The anchor-day and plain-temporal-fallback branches were listing any
commemorated saint before the winning identity, so a lower-ranked saint
merely riding along as a commemoration (e.g. St. Andrew on Advent I, or
Ss. Tryphon/Respicius/Nympha on an ordinary Sunday) read as if it were
the day's own winner, with the actual winner's rank label dangling at
the end with no name attached. The winner should always lead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 06:10:14 -04:00
5 changed files with 76 additions and 19 deletions
+14 -8
View File
@@ -500,11 +500,17 @@ const TEMPORAL_CATEGORY_RANK_LABELS: Record<TemporalCategory, Bi> = {
* silent about *why* a given identity governs tonight (a reader has to
* already know, e.g., that St. Zephyrinus falls on tomorrow's date to
* realize this evening is anticipating him). Pass the result of
* `calendar/vespers.ts`'s `resolveEveningDay`, not a plain `resolveDay`
* only that carries the `vespersFrom` tag this reads.
* `calendar/vespers.ts`'s `resolveEveningDay`, not a plain `resolveDay`,
* plus the originally-requested date — comparing the two directly (rather
* than checking `winner.vespersFrom`, which only ever gets set for a
* *sanctoral* tomorrow-winner, see `vespers.ts`'s `tagVespersFrom`) is
* what makes this correct even when tomorrow's winner is `temporal` (e.g.
* an ordinary Sunday winning outright over a merely-commemorated Simplex
* saint) — a case `winner.vespersFrom` can never detect since that field
* doesn't exist on the temporal variant of `DayWinner` at all.
*/
export function getVespersStatusLabel(day: LiturgicalDay): Bi {
if (day.winner.kind === 'sanctoral' && day.winner.vespersFrom === 'firstVespersOfTomorrow') {
export function getVespersStatusLabel(day: LiturgicalDay, requestedDate: string): Bi {
if (day.date !== requestedDate) {
return bi('First Vespers (of tomorrow)', 'Vesperæ de sequenti');
}
return bi('Second Vespers (of today)', 'Vesperæ de hodierno');
@@ -561,12 +567,12 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
// anchorDayName's own doc comment — checked before the octave case
// below since Trinity Sunday, e.g., also happens to be day 8 of
// Pentecost's octave, and the anchor name is what actually governs.
// Existing convention: a commemorated saint *leads* here, before the
// anchor name — not "winner first" like every branch above.
// Winner first, same as every other branch here — the anchor day is
// what's actually being celebrated even when a lesser saint rides along.
const anchorName = anchorDayName(day);
if (anchorName) {
const { sanctoral } = collectCommemorations(day, { showOctaves: false });
return joinBi([...sanctoral, anchorName]);
return joinBi([anchorName, ...sanctoral]);
}
// An active octave (St. Lawrence's, ...) is this day's real primary
@@ -609,5 +615,5 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
const label = temporalLabel(day);
const temporal = bi(`${label.en} (${temporalRank.en})`, `${label.la} (${temporalRank.la})`);
const { sanctoral } = collectCommemorations(day, { showOctaves: false });
return joinBi([...sanctoral, temporal]);
return joinBi([temporal, ...sanctoral]);
}
+1 -1
View File
@@ -23,7 +23,7 @@ export function renderDayNav(container: HTMLElement): void {
const day = isEveningHour ? resolveEveningDay(date) : resolveDay(date);
const dateLabel = formatDateLong(date);
const dayLabel = getDayLabel(day);
const vespersStatus = selectedHour === 'vespers' ? getVespersStatusLabel(day) : undefined;
const vespersStatus = selectedHour === 'vespers' ? getVespersStatusLabel(day, date) : undefined;
const prevUrl = urlFor(addDays(date, -1), selectedHour, languages);
const nextUrl = urlFor(addDays(date, 1), selectedHour, languages);
const todayUrl = urlFor(todayIso(), todayTargetHour(), languages);
+42 -9
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import { resolveDay } from '../../src/calendar';
import { getDayLabel } from '../../src/calendar/day-label';
import { resolveEveningDay } from '../../src/calendar/vespers';
import { getDayLabel, getVespersStatusLabel } from '../../src/calendar/day-label';
import type { LiturgicalDay } from '../../src/calendar/types';
describe('getDayLabel — ordinal temporal label', () => {
@@ -60,9 +61,9 @@ describe('getDayLabel — ordinal temporal label', () => {
// the November sanctoral pull, he's correctly commemorated alongside
// Advent I rather than simply absent (confirmed against the live
// reference engine, which shows the same pairing), so the label
// reflects both, feast name first, same pattern as Trinity Sunday's
// own St. Felix I case above.
expect(getDayLabel(resolveDay('2025-11-30')).en).toBe('St. Andrew, Apostle — The 1st Sunday of Advent (Semiduplex)');
// reflects both, winner (Advent I, so privileged nothing can displace
// it) first, commemorated saint after.
expect(getDayLabel(resolveDay('2025-11-30')).en).toBe('The 1st Sunday of Advent (Semiduplex) — St. Andrew, Apostle');
// Advent drops the redundant leading weekday word now that the header
// always shows the weekday on its own (see temporalLabel's
// `dropWeekdayPrefix`).
@@ -98,10 +99,10 @@ describe('getDayLabel — resumed post-Epiphany Sunday (overflow years)', () =>
// Sundays) -- see calendar/temporal-id.test.ts for the id-level
// coverage this label check builds on. Nov 10 itself also carries a
// Simplex commemoration (Ss. Tryphon, Respicius, and Nympha), shown
// appended to the Sunday's own label per this app's usual
// commemorated-Simplex display.
// trailing after the Sunday's own winning label per this app's usual
// winner-first display.
expect(getDayLabel(resolveDay('2024-11-10')).en).toBe(
'Ss. Tryphon, Respicius, and Nympha, Martyrs — The 5th Sunday after Epiphany (Semiduplex)',
'The 5th Sunday after Epiphany (Semiduplex) — Ss. Tryphon, Respicius, and Nympha, Martyrs',
);
// 2024-11-11 is St. Martin of Tours (Duplex, pre-existing content) --
// an ordinary (non-privileged) Monday fully yields to a real winning
@@ -368,15 +369,47 @@ describe('getDayLabel — feast name combination', () => {
expect(getDayLabel(day).en).toBe('St. Ereden (Duplex)');
});
it('shows both, feast first, when the feast is merely commemorated', () => {
it('shows both, winner first, when a feast is merely commemorated', () => {
const day: LiturgicalDay = {
...base,
commemorations: [{ kind: 'sanctoral', id: 'x', name: 'St. Ereden', rank: 'duplex-2-classis' }],
};
expect(getDayLabel(day).en).toBe('St. Ereden — Monday in the 3rd week after Trinity (Feria)');
expect(getDayLabel(day).en).toBe('Monday in the 3rd week after Trinity (Feria) — St. Ereden');
});
it('shows just the temporal label when nothing is commemorated at all', () => {
expect(getDayLabel(base).en).toBe('Monday in the 3rd week after Trinity (Feria)');
});
});
describe('getDayLabel/getVespersStatusLabel — Vespers evening with a temporal (Sunday) winner', () => {
it('2026-08-29 (Decollation of St. John Baptist, plain Duplex, below keepsOwnSecondVespers threshold) anticipates the following ordinary Sunday, which wins outright over Ss. Felix and Adauctus (Simplex) -- the Sunday must lead the label, with both real commemorations (Felix/Adauctus and the displaced Decollation) trailing, and the status must read First, not Second, Vespers', () => {
// Regression test: getDayLabel's temporal-fallback branch used to list
// commemorations *before* the winner, so this read "Ss. Felix and
// Adauctus, Martyrs -- The Beheading of St. John the Baptist (of
// today) -- The 13th Sunday after Trinity (Semiduplex)", making the
// actual winner (the Sunday) look like an afterthought and the
// Sunday's own rank label ("Semiduplex") look like it belonged to
// Felix and Adauctus instead. And getVespersStatusLabel used to say
// "Second Vespers (of today)" here since its old detection
// (`winner.vespersFrom`) only ever fires for a *sanctoral*
// tomorrow-winner, never a temporal one like this Sunday.
const day = resolveEveningDay('2026-08-29');
expect(day.date).toBe('2026-08-30');
expect(day.winner).toEqual({ kind: 'temporal', id: 'post-pentecost-14' });
expect(getDayLabel(day).en).toBe(
'The 13th Sunday after Trinity (Semiduplex) — Ss. Felix and Adauctus, Martyrs — The Beheading of St. John the Baptist (of today)',
);
expect(getVespersStatusLabel(day, '2026-08-29').en).toBe('First Vespers (of tomorrow)');
});
it("a plain kept evening (no anticipation) still reads Second Vespers", () => {
// 2026-08-17, St. Lawrence's own octave closing day (Duplex), keeps
// its own evening outright -- sanity check that the date-comparison
// rewrite of getVespersStatusLabel didn't flip the non-anticipating
// case too.
const day = resolveEveningDay('2026-08-17');
expect(day.date).toBe('2026-08-17');
expect(getVespersStatusLabel(day, '2026-08-17').en).toBe('Second Vespers (of today)');
});
});
+1 -1
View File
@@ -114,7 +114,7 @@ describe('resolveOrdo("compline", ...)', () => {
const eve = resolveOrdo('compline', '2025-11-29');
const last = eve.parts[eve.parts.length - 1];
expect(last?.kind === 'preces' ? last.label : undefined).toBe('Alma Redemptoris Mater');
expect(eve.dayLabel?.en).toBe('St. Andrew, Apostle — The 1st Sunday of Advent (Semiduplex)');
expect(eve.dayLabel?.en).toBe('The 1st Sunday of Advent (Semiduplex) — St. Andrew, Apostle');
const dayBefore = resolveOrdo('compline', '2025-11-28');
const lastBefore = dayBefore.parts[dayBefore.parts.length - 1];
+18
View File
@@ -69,6 +69,24 @@ describe('shell', () => {
expect(root.querySelector('.day-nav-vespers-status')?.textContent).toContain('Second Vespers');
});
it('shows First Vespers and the Sunday leading the header when Vespers anticipates a Sunday that wins outright (temporal, not sanctoral, winner)', () => {
// Regression test: getVespersStatusLabel used to detect anticipation
// only via a tag that's only ever set for a *sanctoral* tomorrow-
// winner (see the equivalent calendar-level test in
// tests/calendar/day-label.test.ts) -- 2026-08-29 (the Decollation of
// St. John Baptist, Duplex) anticipates 2026-08-30's ordinary Sunday,
// which wins outright over Ss. Felix and Adauctus (Simplex), so
// tomorrow's winner is `temporal`, not `sanctoral`.
setDate('2026-08-29');
setSelectedHour('vespers');
window.history.replaceState(null, '', '/vu/2026-08-29/vespers');
const root = document.getElementById('app')!;
mountShell(root);
expect(root.querySelector('.day-nav-label')?.textContent).toContain('13th Sunday after Trinity');
expect(root.querySelector('.day-nav-vespers-status')?.textContent).toContain('First Vespers');
});
it('switches to Matins and shows its real content, not the pending message', () => {
const root = document.getElementById('app')!;
mountShell(root);