Add Dec 26-30's own Sunday rule -- a deliberate remix, not a live-verified fix
Deploy / deploy (push) Successful in 54s

Per direct instruction, and explicitly framed as this project's own
departure from Monastic 1617 itself: within Dec 26-30 inclusive (exactly
one real Sunday falls somewhere in that window every year), whichever
date is the actual Sunday always wins as Nat1-0 outright, with no rank
check at all -- even Stephen/John/Holy Innocents (Duplex II. classis)
get fully displaced, not just commemorated. The displaced saint isn't
commemorated in place either; he reappears in full as Dec 30's own
winner instead (the block's one date with no fixed saint of its own).
Rank never matters for this rule -- confirmed with Becket (Semiduplex)
transferring the same way as the Duplex-II-classis saints.

This deliberately diverges both from what Monastic 1617 itself does on
Dec 26-29 (live-verified two commits ago: the saint wins there, Sunday
merely commemorated) and from the general privileged-sunday transfer
rule elsewhere (which only transfers ranks below duplex-majus and
commemorates everything else in place) -- this window transfers every
rank and commemorates nothing in place, a genuinely new, third
behavior specific to this 5-day block.

Implemented as calendar/index.ts's applyChristmasOctaveSunday, a
narrowly-scoped layer in the same spirit as applyMarianSaturday/
applyChristTheKing, rather than by changing the shared
decideOccurrence/transfer machinery every other Sunday relies on. Dec 31
and everything outside Dec 26-30 is untouched -- St. Silvester still
beats the Sunday there via the plain, unchanged ordinary-sunday rule.

Verified against all five possible Sunday positions within the window
(Dec 26/27/28/29/30) plus the "nothing displaces anything" control case,
each with its own dedicated test in the new
tests/calendar/christmas-octave-sunday.test.ts. Updates two existing
tests whose assertions were correct under the old rule and are now
superseded specifically within this window (Stephen/Innocents no longer
win their own Sunday-collision dates there) while confirming the
underlying ordinary-sunday rule is still live and real outside it (Dec
31/St. Silvester).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 10:59:34 -04:00
parent 397f31f4fc
commit ea29e5ee45
5 changed files with 231 additions and 33 deletions
@@ -0,0 +1,119 @@
import { describe, expect, it } from 'vitest';
import { resolveDay } from '../../src/calendar';
// Per direct instruction: within Dec 26-30 inclusive, exactly one date is
// genuinely the Sunday within the Octave of the Nativity each year. That
// date always wins outright as Nat1-0 (christmas-octave-sunday), no rank
// check at all -- a deliberate departure from Monastic 1617 itself, which
// lets a Duplex-II-classis saint (Stephen/John/Innocents) beat the Sunday
// there (see tests/calendar/december-sanctoral.test.ts's own Dec 31/St.
// Silvester test for where that live-verified rule is still real, outside
// this window). The displaced saint isn't commemorated in place -- he
// reappears in full on Dec 30, the block's one date with no fixed saint of
// its own.
describe('applyChristmasOctaveSunday', () => {
it('Sunday on Dec 26 (St. Stephen, Duplex II. classis): Nat1-0 wins outright, Stephen not even commemorated, and reappears in full on Dec 30', () => {
const sunday = resolveDay('2021-12-26');
expect(sunday.weekday).toBe('sunday');
expect(sunday.winner).toEqual({ kind: 'temporal', id: 'christmas-octave-sunday' });
expect(sunday.commemorations.some((c) => c.kind === 'sanctoral')).toBe(false);
const dec30 = resolveDay('2021-12-30');
expect(dec30.winner).toEqual({
kind: 'sanctoral',
id: 'st-stephen-protomartyr',
name: 'St. Stephen, Protomartyr',
rank: 'duplex-2-classis',
});
// The now-redundant "Octave of St. Stephen" commemoration (which
// would otherwise have been pushed by applyOctaves, using Dec 30's
// *original* temporal winner) is dropped -- he's the day's own
// winner now, not merely commemorated alongside himself.
expect(dec30.commemorations.some((c) => c.kind === 'octave' && c.id === 'st-stephen-protomartyr')).toBe(false);
// The other, still-genuinely-separate octaves remain.
expect(dec30.commemorations.map((c) => (c.kind === 'octave' ? c.id : undefined))).toEqual([
'christmas-day',
'st-john-apostle',
'holy-innocents',
]);
});
it('Sunday on Dec 27 (St. John, Duplex II. classis): same shape', () => {
const sunday = resolveDay('2020-12-27');
expect(sunday.winner).toEqual({ kind: 'temporal', id: 'christmas-octave-sunday' });
const dec30 = resolveDay('2020-12-30');
expect(dec30.winner).toEqual({
kind: 'sanctoral',
id: 'st-john-apostle',
name: 'St. John, Apostle and Evangelist',
rank: 'duplex-2-classis',
});
expect(dec30.commemorations.some((c) => c.kind === 'octave' && c.id === 'st-john-apostle')).toBe(false);
});
it('Sunday on Dec 28 (Holy Innocents, Duplex II. classis): same shape', () => {
const sunday = resolveDay('2025-12-28');
expect(sunday.winner).toEqual({ kind: 'temporal', id: 'christmas-octave-sunday' });
const dec30 = resolveDay('2025-12-30');
expect(dec30.winner).toEqual({
kind: 'sanctoral',
id: 'holy-innocents',
name: 'The Holy Innocents, Martyrs',
rank: 'duplex-2-classis',
});
expect(dec30.commemorations.some((c) => c.kind === 'octave' && c.id === 'holy-innocents')).toBe(false);
});
it('Sunday on Dec 29 (St. Thomas Becket, only Semiduplex): still fully displaced to Dec 30, same as the Duplex-II-classis saints -- rank never matters for this rule', () => {
const sunday = resolveDay('2024-12-29');
expect(sunday.winner).toEqual({ kind: 'temporal', id: 'christmas-octave-sunday' });
const dec30 = resolveDay('2024-12-30');
expect(dec30.winner).toEqual({
kind: 'sanctoral',
id: 'st-thomas-becket',
name: 'St. Thomas of Canterbury, Bishop and Martyr',
rank: 'semiduplex',
});
});
it('Sunday on Dec 30 itself: nothing to displace, resolves clean', () => {
const dec30 = resolveDay('2029-12-30');
expect(dec30.weekday).toBe('sunday');
expect(dec30.winner).toEqual({ kind: 'temporal', id: 'christmas-octave-sunday' });
// All four octaves still commemorated normally -- nothing was ever
// assigned to Dec 30 itself, so there's nothing to drop.
expect(dec30.commemorations.map((c) => (c.kind === 'octave' ? c.id : undefined))).toEqual([
'christmas-day',
'st-stephen-protomartyr',
'st-john-apostle',
'holy-innocents',
]);
});
it('Dec 26-29 keep their own fixed saint outright on a year none of them is the Sunday, unaffected by this rule at all', () => {
// 2029: the Sunday is Dec 30, so Dec 26-29 each just resolve normally.
expect(resolveDay('2029-12-26').winner).toEqual({
kind: 'sanctoral',
id: 'st-stephen-protomartyr',
name: 'St. Stephen, Protomartyr',
rank: 'duplex-2-classis',
});
expect(resolveDay('2029-12-29').winner).toEqual({
kind: 'sanctoral',
id: 'st-thomas-becket',
name: 'St. Thomas of Canterbury, Bishop and Martyr',
rank: 'semiduplex',
});
});
it('never touches Dec 31 or Jan 1, outside its own Dec 26-30 scope', () => {
// St. Silvester (Dec 31) still wins outright via the plain
// ordinary-sunday rule even in a year Dec 31 is the Sunday -- see
// tests/calendar/december-sanctoral.test.ts for this one directly.
const dec31 = resolveDay('2023-12-31');
expect(dec31.winner.kind).toBe('sanctoral');
});
});