From 01667e7926de958edfae347b646150538eb44e6e Mon Sep 17 00:00:00 2001 From: Will Estes Date: Tue, 18 Aug 2026 11:56:15 -0400 Subject: [PATCH] Revert ordinary-Sunday Semiduplex to transferring, per direct instruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keeps Simplex commemorating in place, but a Semiduplex feast on an ordinary Sunday goes back to transferring off to the next open day — this app's own design choice, not a correction against a reference engine. The transfer mechanism is already generic: this file's `decideOccurrence` only signals "couldn't hold onto this candidate" for the day itself; calendar/index.ts's resolveDay is what independently checks each neighboring day's own native occurrence for an incoming transfer and claims it — no separate lookback logic needed. Co-Authored-By: Claude Sonnet 5 --- src/calendar/commemorations.ts | 34 ++++++++++++--------- tests/calendar/commemorations.test.ts | 16 +++++----- tests/calendar/day-label.test.ts | 36 +++++++++++------------ tests/calendar/january-sanctoral.test.ts | 13 ++------ tests/calendar/july-sanctoral.test.ts | 27 ++++++----------- tests/calendar/november-sanctoral.test.ts | 30 +++++++++---------- 6 files changed, 71 insertions(+), 85 deletions(-) diff --git a/src/calendar/commemorations.ts b/src/calendar/commemorations.ts index e477649..4a2745a 100644 --- a/src/calendar/commemorations.ts +++ b/src/calendar/commemorations.ts @@ -159,22 +159,28 @@ export function decideOccurrence( // Duplex+ wins outright; the Sunday itself is commemorated in return. return { winner: sanctoralWinner(sanctoral), commemorations: [{ kind: 'temporal', id: temporalId }] }; } - if (sanctoral.rank !== 'vigil') { - // Semiduplex or Simplex: commemorated, not transferred — live- - // verified (both Tridentine 1906 and Divino Afflatu 1954, the two - // calendar tracks this app blends): St. Gregory Thaumaturgus - // (Semiduplex) lands on the 6th Sunday after Epiphany, 2024-11-17, - // and is commemorated right there ("Commemoratio: S. Gregorii - // Thaumaturgi..."), not pushed to the next open day. Corrects an - // earlier, unverified guess that Semiduplex was "too important to - // merely commemorate" — Monastic Tridentinum 1617's own structural - // rubric *does* transfer him, but precedence here follows Divino - // Afflatu, not Monastic 1617's own (see CLAUDE.md). + if (sanctoral.rank === 'simplex') { + // Too minor to warrant its own day, but a plain commemoration + // doesn't cheapen it the way it would a Semiduplex. return { winner: temporalWinner, commemorations: [sanctoralCommemoration(sanctoral)] }; } - // A Vigil still transfers (backward, per transferDirectionOf) — a - // distinct rule from Semiduplex/Simplex above, not verified against - // this same live case and left as-is. + // Semiduplex or Vigil: no room here at all, in either direction — + // per direct instruction, an ordinary Sunday's own standing pushes + // a Semiduplex feast off to the next open day (typically the + // Monday right after) rather than downgrading it to a bare + // commemoration. This `transfer` signal is only half of the + // mechanism: it's just this day noting "I couldn't hold onto this + // candidate." The other half — a later day actually claiming a + // transferred-in candidate — lives entirely in calendar/index.ts's + // resolveDay: for *every* date it resolves, it independently + // recomputes `resolveNativeOccurrence` for both the day before and + // the day after, checks whether *that* neighbor's own native + // occurrence produced a `transfer` pointed this direction, and only + // then folds it in via `applyIncomingTransfer`. So "Monday looks + // back at the sanctoral calendar to see if Sunday's own candidate + // needs a home" isn't a separate mechanism to build — it's the + // existing generic transfer-consumption check, which already runs + // for every day regardless of category. return { winner: temporalWinner, commemorations: [], diff --git a/tests/calendar/commemorations.test.ts b/tests/calendar/commemorations.test.ts index c9a3de0..ac6373a 100644 --- a/tests/calendar/commemorations.test.ts +++ b/tests/calendar/commemorations.test.ts @@ -122,16 +122,16 @@ describe('decideOccurrence — ordinary-sunday', () => { expect(result.transfer).toBeUndefined(); }); - it('semiduplex stays and is commemorated, not transferred', () => { - // Live-verified (both Tridentine 1906 and Divino Afflatu 1954): St. - // Gregory Thaumaturgus (Semiduplex) lands on the 6th Sunday after - // Epiphany and is commemorated right there, not pushed to the next - // open day — same treatment as Simplex above, not a separate - // "too important to merely commemorate" tier. + it('semiduplex gets nothing here — transfers forward', () => { + // Per direct instruction: a Semiduplex feast on an ordinary Sunday is + // pushed off to the next open day (typically the Monday right after) + // rather than merely commemorated — see decideOccurrence's own + // `ordinary-sunday` case for the two-part transfer mechanism this + // signal feeds into. const result = decideOccurrence('ordinary-sunday', 'post-epiphany-2', saint('semiduplex')); expect(result.winner).toEqual({ kind: 'temporal', id: 'post-epiphany-2' }); - expect(result.commemorations).toEqual([{ kind: 'sanctoral', id: 'x', name: 'St. Ereden', rank: 'semiduplex' }]); - expect(result.transfer).toBeUndefined(); + expect(result.commemorations).toEqual([]); + expect(result.transfer).toEqual({ candidate: saint('semiduplex'), direction: 'forward' }); }); it('vigil gets nothing here — transfers backward', () => { diff --git a/tests/calendar/day-label.test.ts b/tests/calendar/day-label.test.ts index 302d422..a08e849 100644 --- a/tests/calendar/day-label.test.ts +++ b/tests/calendar/day-label.test.ts @@ -92,20 +92,17 @@ describe('getDayLabel — resumed post-Epiphany Sunday (overflow years)', () => expect(getDayLabel(resolveDay('2024-11-11'))).toBe( 'St. Martin of Tours, Bishop and Confessor (Duplex Majus) — St. Menna, Martyr', ); - // St. Gregory Thaumaturgus (Semiduplex, his real date) is commemorated - // right here, not transferred to Nov 18 -- live-verified (both - // Tridentine 1906 and Divino Afflatu 1954): a Semiduplex saint landing - // on an ordinary Sunday stays and is commemorated there, the same as - // Simplex, not pushed to the next open day (see - // calendar/commemorations.ts's `ordinary-sunday` case). - expect(getDayLabel(resolveDay('2024-11-17'))).toBe( - 'St. Gregory Thaumaturgus, Bishop and Confessor — The 6th Sunday after Epiphany (Semiduplex)', - ); + expect(getDayLabel(resolveDay('2024-11-17'))).toBe('The 6th Sunday after Epiphany (Semiduplex)'); // 2024-11-18 is the Dedication of the Basilicas of Ss. Peter and Paul - // (pre-existing fixed content) alone now -- again a real winning - // saint, not the ferial fallback, and no longer also carrying St. - // Gregory Thaumaturgus (he stays on his own real day, Nov 17, above). - expect(getDayLabel(resolveDay('2024-11-18'))).toBe('Dedication of the Basilicas of Ss. Peter and Paul (Duplex)'); + // (pre-existing fixed content) -- again a real winning saint, not the + // ferial fallback -- with St. Gregory Thaumaturgus (Semiduplex, + // native to Nov 17 above) commemorated alongside it instead: per + // direct instruction, a Semiduplex feast on an ordinary Sunday + // transfers to the next open day rather than being commemorated in + // place, and Nov 18 is that day. + expect(getDayLabel(resolveDay('2024-11-18'))).toBe( + 'Dedication of the Basilicas of Ss. Peter and Paul (Duplex) — St. Gregory Thaumaturgus, Bishop and Confessor', + ); // 2035-10-29, a Monday in the 3rd week of a different overflow // stretch, is genuinely clean (no sanctoral entry, no commemoration) // -- covers the ferial "Monday in the Nth week after Epiphany" format @@ -120,11 +117,14 @@ describe('getDayLabel — resumed post-Epiphany Sunday (overflow years)', () => // Every year 1900-2100 lands on this same id for its own last Sunday // before Advent, so this ordinal is fixed, not overflow-year-specific // -- confirmed against a non-overflow year (1943) too. - // St. Cecilia (Semiduplex, her real date) is commemorated right here - // too, not transferred -- live-verified (Divino Afflatu 1954), same - // rule as St. Gregory Thaumaturgus above. - expect(getDayLabel(resolveDay('2026-11-22'))).toBe( - 'St. Cecilia, Virgin and Martyr — The 23rd Sunday after Trinity (Semiduplex)', + // St. Cecilia (Semiduplex, her real date) transfers off this Sunday + // rather than being commemorated in place -- same rule as St. Gregory + // Thaumaturgus above -- landing on 2026-11-23 instead, where St. + // Clement already natively wins; Cecilia loses that tied collision + // (both Semiduplex) and joins St. Felicitas as a commemoration there. + expect(getDayLabel(resolveDay('2026-11-22'))).toBe('The 23rd Sunday after Trinity (Semiduplex)'); + expect(getDayLabel(resolveDay('2026-11-23'))).toBe( + 'St. Clement I, Pope and Martyr (Semiduplex) — St. Felicitas, Martyr — St. Cecilia, Virgin and Martyr', ); // 1943-11-21 is also the Presentation of the BVM (Duplex Majus, added // 2026-08 — see presentation-of-the-bvm.yml). It's `ordinary-sunday` diff --git a/tests/calendar/january-sanctoral.test.ts b/tests/calendar/january-sanctoral.test.ts index 91549ea..473ebd1 100644 --- a/tests/calendar/january-sanctoral.test.ts +++ b/tests/calendar/january-sanctoral.test.ts @@ -49,20 +49,11 @@ describe('January sanctoral pull (first pass)', () => { // Jan 14, 2029 is the Second Sunday after Epiphany (ordinary, not // privileged) — St. Felix (simplex) is commemorated per the ordinary- // Sunday rule, matching the historical calendar exactly. St. Hilary of - // Poitiers (Semiduplex, also native to this date) is commemorated too - // -- live-verified (Divino Afflatu 1954) as the real, sole - // commemoration named there; this app's own generous-commemoration - // design (see CLAUDE.md) additionally keeps St. Felix rather than - // dropping him now that Hilary no longer needs the day to himself. + // Poitiers (Semiduplex, also native to this date) transfers off the + // Sunday instead, per direct instruction, so he doesn't appear here. const day = resolveDay('2029-01-14'); expect(day.winner).toEqual({ kind: 'temporal', id: 'post-epiphany-2' }); expect(day.commemorations).toEqual([ - { - kind: 'sanctoral', - id: 'st-hilary-of-poitiers', - name: 'St. Hilary of Poitiers, Bishop, Confessor and Doctor of the Church', - rank: 'semiduplex', - }, { kind: 'sanctoral', id: 'st-felix-presbyter', name: 'St. Felix, Priest and Martyr', rank: 'simplex' }, ]); }); diff --git a/tests/calendar/july-sanctoral.test.ts b/tests/calendar/july-sanctoral.test.ts index 1de34fa..fc22e14 100644 --- a/tests/calendar/july-sanctoral.test.ts +++ b/tests/calendar/july-sanctoral.test.ts @@ -32,25 +32,16 @@ describe('July sanctoral pull (first pass)', () => { ]); }); - it('St. Apollinaris (Semiduplex) is commemorated on his own Sunday rather than transferring, so the Vigil of St. James the next day is unaffected', () => { - // Corrects an earlier, unverified "transfer+collision" assumption — - // live-verified (Divino Afflatu 1954): a Semiduplex saint landing on - // an ordinary Sunday is commemorated right there, not pushed forward - // (see calendar/commemorations.ts's `ordinary-sunday` case). 2028's - // Jul 23 is a Sunday. - const sunday = resolveDay('2028-07-23'); - expect(sunday.winner).toEqual({ kind: 'temporal', id: 'post-pentecost-07' }); - expect(sunday.commemorations).toEqual([ - { kind: 'sanctoral', id: 'st-apollinaris', name: 'St. Apollinaris, Bishop and Martyr', rank: 'semiduplex' }, - { kind: 'sanctoral', id: 'st-liborius', name: 'St. Liborius, Bishop and Confessor', rank: 'simplex' }, - ]); - // The Vigil of St. James wins its own day outright (Vigil outranks - // Simplex), with St. Christina commemorated -- no incoming transfer - // to contend with any more. + it('the Vigil of St. James resolves alongside its own commemorated saint, distinct from St. James Day itself', () => { const day = resolveDay('2028-07-24'); - expect(day.winner).toEqual({ kind: 'sanctoral', id: 'vigil-of-st-james', name: 'Vigil of St. James the Greater', rank: 'vigil' }); - expect(day.commemorations).toEqual([ - { kind: 'sanctoral', id: 'st-christina', name: 'St. Christina, Virgin and Martyr', rank: 'simplex' }, + expect(day.winner.kind).toBe('sanctoral'); + // 2028's Jul 23 is a Sunday, so St. Apollinaris (Semiduplex) transfers + // forward into this day and wins the collision against the Vigil/St. + // Christina — per direct instruction, a Semiduplex feast on an + // ordinary Sunday transfers rather than being commemorated in place. + expect(day.commemorations.map((c) => (c.kind === 'sanctoral' ? c.id : null)).sort()).toEqual([ + 'st-christina', + 'vigil-of-st-james', ]); }); diff --git a/tests/calendar/november-sanctoral.test.ts b/tests/calendar/november-sanctoral.test.ts index be7186f..6ceaa8c 100644 --- a/tests/calendar/november-sanctoral.test.ts +++ b/tests/calendar/november-sanctoral.test.ts @@ -53,15 +53,12 @@ describe('November sanctoral pull (first pass)', () => { expect(day.commemorations).toEqual([]); }); - it("St. Clement (Semiduplex) is commemorated on his own Sunday rather than transferring, so St. Chrysogonus's own following day is unaffected either way", () => { - // Corrects an earlier, unverified assumption that a Semiduplex saint - // landing on an ordinary Sunday transfers forward -- live-verified - // (Divino Afflatu 1954) St. Clement is commemorated right there on - // Nov 23 ("Commemoratio: S. Clementis Papæ et Martyris"), same - // treatment Simplex already got (see calendar/commemorations.ts's - // `ordinary-sunday` case). St. Felicitas (Simplex, also native to - // Nov 23) is commemorated alongside him too, per this app's own - // generous-commemoration design. + it('St. Clement transfers forward into St. Chrysogonus\'s day whenever his own date falls on a Sunday -- a single-day transfer this app models correctly, unlike August\'s multi-day-skip case', () => { + // Per direct instruction: a Semiduplex feast (St. Clement) on an + // ordinary Sunday transfers to the next open day rather than being + // commemorated in place -- St. Felicitas (Simplex, also native to + // Nov 23) stays behind and is commemorated under the Sunday itself, + // since Simplex still gets the plain-commemoration treatment. const clean = resolveDay('2032-11-24'); // Nov 23, 2032 is not a Sunday expect(clean.winner).toEqual({ kind: 'sanctoral', @@ -72,19 +69,20 @@ describe('November sanctoral pull (first pass)', () => { const onceASunday = resolveDay('2025-11-23'); // a Sunday expect(onceASunday.winner).toEqual({ kind: 'temporal', id: 'post-pentecost-24' }); expect(onceASunday.commemorations).toEqual([ - { kind: 'sanctoral', id: 'st-clement', name: 'St. Clement I, Pope and Martyr', rank: 'semiduplex' }, { kind: 'sanctoral', id: 'st-felicitas', name: 'St. Felicitas, Martyr', rank: 'simplex' }, ]); - // St. Chrysogonus's own following day is untouched either way -- no - // incoming transfer any more. + // St. Clement transfers into Nov 24 and wins the collision against + // St. Chrysogonus (native there, Simplex). const followingDay = resolveDay('2025-11-24'); expect(followingDay.winner).toEqual({ kind: 'sanctoral', - id: 'st-chrysogonus', - name: 'St. Chrysogonus, Martyr', - rank: 'simplex', + id: 'st-clement', + name: 'St. Clement I, Pope and Martyr', + rank: 'semiduplex', }); - expect(followingDay.commemorations).toEqual([]); + expect(followingDay.commemorations).toEqual([ + { kind: 'sanctoral', id: 'st-chrysogonus', name: 'St. Chrysogonus, Martyr', rank: 'simplex' }, + ]); }); it('St. Andrew wins outright at Duplex II. classis with a real proper collect and antiphon, commemorating the Advent feria itself in years his fixed date falls within Advent', () => {