diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 17:15:03 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 17:15:03 +0200 |
| commit | 3b32c002d3eddda5ece9422442717657b9fee63b (patch) | |
| tree | 5dd82811c100de6daa995bebd115a12434296508 /internal/calendar/precedence_ef_repro_test.go | |
| parent | 2386a4551aec94252168a8554269ee423d516882 (diff) | |
| parent | c655c525ef07deafb113a291489b396c81d7ce4a (diff) | |
| download | lectio-3b32c002d3eddda5ece9422442717657b9fee63b.tar.gz lectio-3b32c002d3eddda5ece9422442717657b9fee63b.zip | |
Merge branch 'fix/ef-precedence-defects': seven EF calendar defects
Fixes seven defects in the EF calendar, each found by differential and
oracle comparison against an independently-written engine and each
backed by a cited paragraph of the Rubricae Generales verified against
the 1962 Missal:
RG 96 transfers must skip II-class days (the Precious Blood was
landing on the Visitation)
RG 91 a II-class privileged feria yields to an equal-class feast
(St Matthew was losing to the September Ember Wednesday)
RG 11 all Sundays of Advent, Lent and Passiontide are I class
(only Advent I was), which also settles St Joseph on a Lent
Sunday
RG 131 Rose on Gaudete and Laetare, which the EF path never emitted
RG 122 Holy Thursday is white
and the sanctoral generator itself, whose rank inference had tagged 15
III-class feasts as commemorations, dropped four entries to slug
collisions, and mis-tagged three class fields.
The EF oracle test asserted Season only, which is why none of this ever
failed. It now asserts rank and colour, with a cited allow-list.
Two commemorations had been deleted on a primary-source claim the
primary source contradicts: the Archivum Liturgicum transcription
silently drops commemoration lines where the photographic scans carry
them. Six of the seven affected dates are now present; the seventh
(25 December) is blocked by an ingestion limit that reads only the
first of three Christmas Masses, recorded at its cause.
Known gap, recorded in source: RG 95/97/98 chained transfers, where two
I-class feasts translate to the same day.
Diffstat (limited to 'internal/calendar/precedence_ef_repro_test.go')
| -rw-r--r-- | internal/calendar/precedence_ef_repro_test.go | 160 |
1 files changed, 160 insertions, 0 deletions
diff --git a/internal/calendar/precedence_ef_repro_test.go b/internal/calendar/precedence_ef_repro_test.go new file mode 100644 index 0000000..11fd6ca --- /dev/null +++ b/internal/calendar/precedence_ef_repro_test.go @@ -0,0 +1,160 @@ +package calendar_test + +// Reproduction tests for three named EF precedence defects (colitur's +// differential/oracle audit against lectio's ~66-line EF precedence +// approximation, internal/calendar/precedence_ef.go). Each test fails before +// its fix and passes after -- see the report for the exact pre-fix failure +// message. These exercise the REAL tridentine sanctoral data +// (caldata.Tridentine()) through the public Compute entry point, since the +// bugs are about how real fixed-date feasts interact with the temporal +// calendar, not about precedenceEF's arithmetic in isolation (that is +// covered separately, package-internal, in precedence_ef_test.go). + +import ( + "testing" + "time" + + "github.com/lukaszkasprzak/lectio/internal/caldata" + "github.com/lukaszkasprzak/lectio/internal/calendar" +) + +func efCompute(date string) calendar.LiturgicalDay { + sel := calendar.DefaultSelection() + sel.Form = "old" + d, _ := time.Parse("2006-01-02", date) + return calendar.Compute(d.UTC(), sel, []calendar.Layer{caldata.Tridentine()}) +} + +// TestJosephYieldsToSundayOfLent: RG 91 entry 6 (Sundays of Lent, I class) +// vs entry 11 (I-class feasts of the universal Church not above) -- the +// Sunday holds; St Joseph (19 March) transfers to the next free day (RG 95, +// RG 96), landing on the 20th. Before the fix (defect 4: Sunday ranks), Lent +// Sundays were wrongly II class, so Joseph (I class) won outright every time +// this collision occurred. +func TestJosephYieldsToSundayOfLent(t *testing.T) { + for _, year := range []string{"2006", "2017", "2023", "2028", "2034", "2045"} { + sunday := efCompute(year + "-03-19") + if sunday.Observed.Slug == "joseph-spouse-of-the-bl-virgin-mary" { + t.Errorf("%s-03-19 observed = %q want the Lent Sunday (RG 91 entry 6 beats entry 11)", year, sunday.Observed.Slug) + } + next := efCompute(year + "-03-20") + if next.Observed.Slug != "joseph-spouse-of-the-bl-virgin-mary" { + t.Errorf("%s-03-20 observed = %q want joseph-spouse-of-the-bl-virgin-mary (transferred one day, RG 96)", year, next.Observed.Slug) + } + } +} + +// TestTransferSkipsBothIAndIIClass: RG 96 -- an impeded I-class feast +// transfers to the next day that is NOT ITSELF I OR II CLASS (not merely "not +// I class"). 2011: the Sacred Heart (Friday after the Corpus Christi octave) +// falls on 1 July and impedes the Precious Blood (also 1 July, fixed). 2 +// July is the Visitation (II class, fixed); 3 July is an ordinary II-class +// Sunday. Both must be skipped; the Precious Blood lands on 4 July, and the +// Visitation is observed, undisplaced, on its own day. +func TestTransferSkipsBothIAndIIClass(t *testing.T) { + if got := efCompute("2011-07-01").Observed.Slug; got != "ef-sacred-heart" { + t.Fatalf("2011-07-01 observed = %q want ef-sacred-heart", got) + } + if got := efCompute("2011-07-02").Observed.Slug; got != "visitation-of-the-blessed-virgin-mary" { + t.Errorf("2011-07-02 observed = %q want visitation-of-the-blessed-virgin-mary (RG 96: the Precious Blood must skip past it, not displace it)", got) + } + if got := efCompute("2011-07-03").Observed.Slug; got != "ef-time-after-pentecost-sunday-3" { + t.Errorf("2011-07-03 observed = %q want ef-time-after-pentecost-sunday-3 (an ordinary II-class Sunday also blocks a I-class transfer)", got) + } + if got := efCompute("2011-07-04").Observed.Slug; got != "precious-blood-of-our-lord-jesus-christ" { + t.Errorf("2011-07-04 observed = %q want precious-blood-of-our-lord-jesus-christ (RG 96: first day that is neither I nor II class)", got) + } +} + +// TestMatthewBeatsSeptemberEmberWednesday: RG 91 -- St Matthew (21 September, +// II class) and the September Ember Wednesday are both II class; the table +// decides the tie. Entry 16 (II-class feasts of the universal Church) sits +// ABOVE entry 18 (II-class ferias, including the Ember days) -- the feast +// wins, the Ember feria is only commemorated. +func TestMatthewBeatsSeptemberEmberWednesday(t *testing.T) { + // 21 September falls on the September Ember Wednesday whenever the third + // Sunday of September is the 18th -- 2016 and 2022 both qualify. + for _, date := range []string{"2016-09-21", "2022-09-21"} { + day := efCompute(date) + if day.Observed.Slug != "matthew" { + t.Errorf("%s observed = %q want matthew (RG 91 entry 16 beats entry 18)", date, day.Observed.Slug) + } + } +} + +// TestImmaculateConceptionBeatsAdventSunday: found, not named among the +// seven, while verifying defect 4 (Sunday ranks) against the real sanctoral +// data. RG 91 entry 4 (Immaculate Conception, Assumption BVM) sits ABOVE +// entry 6 (Sundays of Advent/Lent/Passiontide) -- unlike an ORDINARY +// I-class feast (entry 11, e.g. St Joseph), the Immaculate Conception +// (8 December) is not impeded by the Advent Sunday it falls on at all. Before +// defect 4's own fix, Advent Sundays were wrongly II class, so this was +// accidentally right (I class beats II class outright); once Sundays became +// I class the tie mattered for the first time. +func TestImmaculateConceptionBeatsAdventSunday(t *testing.T) { + for _, year := range []string{"2013", "2019", "2024"} { + day := efCompute(year + "-12-08") + if day.Observed.Slug != "immaculate-conception-of-the-blessed-virgin-mary" { + t.Errorf("%s-12-08 observed = %q want immaculate-conception-of-the-blessed-virgin-mary (RG 91 entry 4 beats entry 6)", year, day.Observed.Slug) + } + } +} + +// TestVigilOfChristmasSurvivesAdventSunday: found, not named among the +// seven, while verifying defect 4 against the real sanctoral data -- and +// worse than a wrong winner. RG 91 entry 5 (Vigil & Octave day of the +// Nativity) also sits above entry 6, so the Vigil of Christmas (24 December) +// is not impeded by falling on Advent IV either. Without this, defect 4 +// alone would have made the Advent Sunday win a genuine tie, sending the +// Vigil into transferIfImpededEF's forward walk -- which has no way to +// re-place a transfer that crosses the Dec31/Jan1 boundary (celebrationDate +// re-resolves a fixed date using the year of whatever day is being queried, +// so a walk landing in the following January can never match the query that +// produced it). The Vigil did not move to the wrong day; it vanished for the +// whole year. +func TestVigilOfChristmasSurvivesAdventSunday(t *testing.T) { + for _, year := range []string{"2006", "2017", "2023", "2028"} { + day := efCompute(year + "-12-24") + if day.Observed.Slug != "vigil-of-christmas" { + t.Errorf("%s-12-24 observed = %q want vigil-of-christmas (RG 91 entry 5 beats entry 6; must not vanish)", year, day.Observed.Slug) + } + } +} + +// TestPurificationBeatsFebruarySunday: the fixture behind classOf's own +// Purification decision (scripts/gen-sanctoral-ef.go), committed here rather +// than left as an assertion only, because the deciding years (2 February on +// a Sunday: 2014, 2020, 2025, 2031, 2042, 2048) fall entirely outside this +// repo's own missalemeum oracle snapshot (sources/snapshot.tar.gz, +// 2026-01-01 .. 2027-12-31), so nothing else in this repository can +// reproduce the evidence classOf's decision rests on. (2036, sometimes +// quoted alongside 2025/2031 as a third example: 2 February 2036 is in fact +// a SATURDAY, `date -d 2036-02-02 +%A` -- checked here rather than repeated; +// 2042 is the correct next instance after 2031.) +// +// RG 91 entry 14 ("Festa Domini II classis") sits above entry 15 +// ("Dominicae II classis"), which sits above entry 16 ("Festa II classis +// Ecclesiae universae, quae non [sunt Domini]") -- entry 14 takes an +// occurring Sunday's place outright, entry 16 is merely commemorated on +// one. The Purification (2 February) matches entry 14's pattern, not entry +// 16's, independently fetched live from missalemeum for five different +// Sundays: 2014-02-02, 2020-02-02, 2025-02-02, 2031-02-02, 2042-02-02 (all +// "id":"sancti:02-02:2:w", "commemorations":[]). Control: +// nativity-of-the-blessed-virgin-mary (8 September, an undisputed ordinary +// Marian feast, entry 16's own pattern) on a Sunday -- 2019-09-08 -- shows +// the SUNDAY observed, the feast commemorated instead: the opposite shape, +// proving the Purification's own treatment is a deliberate pattern in the +// oracle, not a gap. This is a decision AGAINST the calendarium's own title +// ("IN PURIFICATIONE B. MARIAE VIRG.") and RG 120(b) (which files 2 +// February under the white-colour rule's "B. Mariae Virg." heading, kept +// separate from 120(a)'s "Domini" heading) -- both point BVM; the +// occurrence-behaviour evidence here points Domini. See classOf's own doc +// comment for the fuller account; not re-argued here. +func TestPurificationBeatsFebruarySunday(t *testing.T) { + for _, year := range []string{"2014", "2020", "2025", "2031", "2042"} { + day := efCompute(year + "-02-02") + if day.Observed.Slug != "purification-of-the-blessed-virgin-mary" { + t.Errorf("%s-02-02 observed = %q want purification-of-the-blessed-virgin-mary (RG 91 entry 14 beats entry 15)", year, day.Observed.Slug) + } + } +} |
