Add Dec 26-30's own Sunday rule -- a deliberate remix, not a live-verified fix
Deploy / deploy (push) Successful in 54s
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:
@@ -116,6 +116,37 @@ instead of the correct plain temporal label. Fixed by having
|
||||
`resolveActiveOctave` call as `resolveOfficeWinner`, so the two can never
|
||||
disagree about which identity governs a day.
|
||||
|
||||
### Dec 26-30's own Sunday rule — done (2026-08), a deliberate remix
|
||||
|
||||
Per direct instruction — a real, admitted departure from Monastic 1617
|
||||
itself, not something independently verified live the way almost
|
||||
everything else here is: within Dec 26-30 inclusive (every year contains
|
||||
exactly one real Sunday somewhere in that 5-day window), whichever date
|
||||
is the actual Sunday always celebrates Nat1-0 (the Sunday within the
|
||||
Octave of the Nativity) outright, with **no rank check at all** — even a
|
||||
Duplex-II-classis saint (Stephen/John/Innocents) is fully displaced, not
|
||||
just commemorated. The displaced saint isn't commemorated in place
|
||||
either; he reappears in full as the day's own winner on Dec 30 instead —
|
||||
the block's one date with no fixed saint of its own. On a year Dec 30
|
||||
itself is the actual Sunday, nothing is displaced and it resolves clean.
|
||||
|
||||
This intentionally diverges from what Monastic 1617 itself does on Dec
|
||||
26-29 (there, live-verified, the Duplex-II-classis saint wins and the
|
||||
Sunday is merely commemorated — see the December-sanctoral tests) and
|
||||
from the general `privileged-sunday` transfer rule elsewhere in this app
|
||||
(which only transfers ranks below duplex-majus and commemorates
|
||||
everything at or above it in place) — this window transfers *every*
|
||||
rank, full stop, and never commemorates the loser in place.
|
||||
|
||||
Implemented as its own dedicated, narrowly-scoped layer
|
||||
(`calendar/index.ts`'s `applyChristmasOctaveSunday`, same architectural
|
||||
pattern as `applyMarianSaturday`/`applyChristTheKing`) rather than by
|
||||
changing the shared `decideOccurrence`/transfer machinery other Sundays
|
||||
rely on — deliberately contained blast radius. Dec 31 and everything
|
||||
outside Dec 26-30 is untouched; christmastide's Sunday there still uses
|
||||
the plain `ordinary-sunday` category as before (St. Silvester still
|
||||
beats the Sunday on Dec 31, live-verified, unaffected by this).
|
||||
|
||||
### Benedictus antiphons for temporal (non-sanctoral) days — done
|
||||
|
||||
`<id>-benedictus-antiphon.yml` in `data/propers/temporal/` — all 54
|
||||
|
||||
@@ -103,6 +103,7 @@ export function resolveDay(isoDate: string): LiturgicalDay {
|
||||
winner = applyOctaves(isoDate, winner, commemorations);
|
||||
winner = applyMarianSaturday(isoDate, weekday, temporalCategory, winner, commemorations);
|
||||
winner = applyChristTheKing(isoDate, winner, commemorations);
|
||||
winner = applyChristmasOctaveSunday(isoDate, weekday, winner, commemorations);
|
||||
|
||||
return { date: isoDate, weekday, season, temporalCategory, winner, commemorations };
|
||||
}
|
||||
@@ -137,6 +138,65 @@ function applyChristTheKing(isoDate: string, winner: DayWinner, commemorations:
|
||||
return { kind: 'temporal', id: 'christ-the-king' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Per direct instruction, scoped to exactly Dec 26-30 — every year
|
||||
* contains exactly one real Sunday somewhere in that 5-day window.
|
||||
*
|
||||
* On whichever of Dec 26-29 it actually falls, Nat1-0 (the Sunday within
|
||||
* the Octave of the Nativity) always wins outright, full stop — no rank
|
||||
* check at all, a deliberate departure from what Monastic 1617 itself
|
||||
* does there (a Duplex-II-classis saint — Stephen/John/Innocents — beats
|
||||
* the Sunday there in the real engine; see TODO.md). The displaced
|
||||
* saint isn't even commemorated in place; he reappears in full on Dec
|
||||
* 30 instead — the block's one date with no fixed saint of its own —
|
||||
* rather than the general privileged-sunday transfer/commemorate split
|
||||
* used everywhere else. Dec 30 itself, on a year it's the *actual*
|
||||
* Sunday (nothing displaced), resolves normally: Nat1-0 wins clean since
|
||||
* nothing was ever assigned there to begin with.
|
||||
*/
|
||||
function applyChristmasOctaveSunday(
|
||||
isoDate: string,
|
||||
weekday: ReturnType<typeof weekdayOf>,
|
||||
winner: DayWinner,
|
||||
commemorations: Commemoration[],
|
||||
): DayWinner {
|
||||
const [year, monthStr, dayStr] = isoDate.split('-');
|
||||
if (monthStr !== '12') {
|
||||
return winner;
|
||||
}
|
||||
const day = Number(dayStr);
|
||||
|
||||
if (day >= 26 && day <= 29 && weekday === 'sunday') {
|
||||
return { kind: 'temporal', id: 'christmas-octave-sunday' };
|
||||
}
|
||||
|
||||
if (day === 30) {
|
||||
for (let d = 26; d <= 29; d++) {
|
||||
const candidateDate = `${year}-12-${String(d).padStart(2, '0')}`;
|
||||
if (weekdayOf(candidateDate) !== 'sunday') {
|
||||
continue;
|
||||
}
|
||||
const displaced = getSanctoralCandidatesFor(candidateDate)[0];
|
||||
if (!displaced) {
|
||||
break;
|
||||
}
|
||||
// Drop the now-redundant "Octave of <displaced>" commemoration —
|
||||
// applyOctaves already pushed one above, using Dec 30's *original*
|
||||
// temporal winner; now that the displaced saint is winning the day
|
||||
// outright instead, commemorating his own octave alongside himself
|
||||
// would be the same redundancy applyOctaves' own `isSelf` check
|
||||
// already avoids on a saint's actual day.
|
||||
const idx = commemorations.findIndex((c) => c.kind === 'octave' && c.id === displaced.id);
|
||||
if (idx !== -1) {
|
||||
commemorations.splice(idx, 1);
|
||||
}
|
||||
return { kind: 'sanctoral', id: displaced.id, name: displaced.name, rank: displaced.rank };
|
||||
}
|
||||
}
|
||||
|
||||
return winner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layered on top of everything above, same spirit as applyOctaves: never
|
||||
* changes decideOccurrence's own rules, just relabels the result on a free
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -108,34 +108,15 @@ describe('December sanctoral pull (first pass, 12/12)', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('the Christmas Octave Sunday loses outright to a Duplex+ Comites Christi feast when the two collide -- ordinary-sunday, not privileged-sunday', () => {
|
||||
// Corrects a real bug: christmastide's Sunday was originally coded
|
||||
// privileged-sunday (Advent's own tier), which would have kept the
|
||||
// Sunday primary here with the saint merely commemorated -- backwards
|
||||
// from the live Monastic 1617 engine, which was checked directly
|
||||
// against all three Duplex-II-classis collision dates and shows the
|
||||
// saint winning outright every time, the Sunday only commemorated.
|
||||
const stephenSunday = resolveDay('2021-12-26');
|
||||
expect(stephenSunday.winner).toEqual({
|
||||
kind: 'sanctoral',
|
||||
id: 'st-stephen-protomartyr',
|
||||
name: 'St. Stephen, Protomartyr',
|
||||
rank: 'duplex-2-classis',
|
||||
});
|
||||
expect(stephenSunday.commemorations).toContainEqual({ kind: 'temporal', id: 'christmas-octave-sunday' });
|
||||
|
||||
const innocentsSunday = resolveDay('2025-12-28');
|
||||
expect(innocentsSunday.winner).toEqual({
|
||||
kind: 'sanctoral',
|
||||
id: 'holy-innocents',
|
||||
name: 'The Holy Innocents, Martyrs',
|
||||
rank: 'duplex-2-classis',
|
||||
});
|
||||
expect(innocentsSunday.commemorations).toContainEqual({ kind: 'temporal', id: 'christmas-octave-sunday' });
|
||||
|
||||
// A plain Duplex (not just Duplex-II-classis) is still enough: St.
|
||||
// Silvester (Dec 31) wins outright the same way when it falls on the
|
||||
// Sunday, also live-verified.
|
||||
it('the underlying ordinary-sunday classification still governs Dec 31 (outside the Dec 26-30 window applyChristmasOctaveSunday owns) -- a Duplex still beats the Sunday there, live-verified', () => {
|
||||
// St. Silvester (Dec 31) wins outright when it falls on the Sunday --
|
||||
// unaffected by applyChristmasOctaveSunday, which is deliberately
|
||||
// scoped to exactly Dec 26-30 (see tests/calendar/
|
||||
// christmas-octave-sunday.test.ts for that window's own, different
|
||||
// rule). This is what confirms christmastide's own `ordinary-sunday`
|
||||
// temporal-category classification (see data/calendar/
|
||||
// temporal-categories.yml) is still real and still needed, not
|
||||
// superseded outright by the newer Dec 26-30 rule.
|
||||
const silvesterSunday = resolveDay('2023-12-31');
|
||||
expect(silvesterSunday.winner).toEqual({
|
||||
kind: 'sanctoral',
|
||||
@@ -146,11 +127,14 @@ describe('December sanctoral pull (first pass, 12/12)', () => {
|
||||
expect(silvesterSunday.commemorations).toContainEqual({ kind: 'temporal', id: 'christmas-octave-sunday' });
|
||||
});
|
||||
|
||||
it('the Christmas Octave Sunday still keeps a lesser feast from winning, same as before the ordinary-sunday fix', () => {
|
||||
it('the Christmas Octave Sunday still keeps a lesser feast from winning, now via applyChristmasOctaveSunday specifically', () => {
|
||||
// St. Thomas Becket (Semiduplex, Dec 29) transfers off the Sunday
|
||||
// rather than winning it -- unaffected by the privileged->ordinary
|
||||
// change, since both tiers transfer a Semiduplex here; this test
|
||||
// guards against a regression the other direction.
|
||||
// rather than winning it -- per calendar/index.ts's
|
||||
// applyChristmasOctaveSunday (see its own dedicated test file), which
|
||||
// now unconditionally wins the Sunday over *any* rank within Dec
|
||||
// 26-29, superseding the plain ordinary-sunday rule for this narrow
|
||||
// window specifically (that rule is still real outside it -- see the
|
||||
// Dec 31/St. Silvester test above).
|
||||
const beckettSunday = resolveDay('2024-12-29');
|
||||
expect(beckettSunday.winner.kind).toBe('temporal');
|
||||
expect(beckettSunday.commemorations.some((c) => c.kind === 'sanctoral')).toBe(false);
|
||||
|
||||
@@ -141,7 +141,11 @@ describe('resolveOrdo("lauds", ...)', () => {
|
||||
});
|
||||
|
||||
it('resolves a real Benedictus antiphon for christmas-octave-sunday too, pulled from the reference engine\'s own dedicated Tempora/Nat1-0 proper rather than any individual octave-day saint\'s page', () => {
|
||||
const ordo = resolveOrdo('lauds', '2026-12-30');
|
||||
// 2029-12-30 specifically: the year Dec 30 itself is the real Sunday
|
||||
// within the Octave, so nothing gets bumped there by
|
||||
// calendar/index.ts's applyChristmasOctaveSunday (see its own tests) --
|
||||
// a genuinely clean case for this content, unlike most years.
|
||||
const ordo = resolveOrdo('lauds', '2029-12-30');
|
||||
const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId === 'benedictus');
|
||||
expect(canticle?.kind === 'canticle' ? canticle.antiphon?.text.la : undefined).toContain('Dum médium siléntium');
|
||||
expect(canticle?.kind === 'canticle' ? canticle.antiphon?.status?.la : undefined).toBe('verified');
|
||||
|
||||
Reference in New Issue
Block a user