From 61a2d658fea1c9a1640cb54bb203a8441bbb17a9 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Wed, 12 Aug 2026 13:06:18 +0200 Subject: precedence_ef: a II-class privileged feria yields to a feast (defect 2) precedenceEF's equal-class tie-break only distinguished "ordinary III/IV-class feria yields" from "everything else wins" -- Sundays, named feasts, Lent/Passiontide ferias, AND II-class privileged ferias (the Ember days, the late-Advent 17-23 Dec ferias) were all bucketed into the same "wins its tie" branch. RG 91 disagrees at class 2: entry 15 (Sundays) sits ABOVE entry 16 (II-class feasts of the universal Church), so a Sunday wins -- but entry 16 sits ABOVE entry 18 (II-class ferias, including the Ember days), so those FERIAS yield instead, the opposite direction from a Sunday. St Matthew (21 September, II class) was losing to the September Ember Wednesday every time the two coincided; St Thomas (21 December, II class) was losing to an ordinary late-Advent feria the same way once the previous commit correctly promoted those ferias to II class. The fix distinguishes a II-class Sunday from a II-class feria using a Sunday flag that already existed on `candidate` (used by the OF path) but was never wired up for EF: temporal_ef.go's efCel always set Sunday: false, even on an actual Sunday. A new efSunday helper (efCel plus the flag) replaces the three efCel calls inside temporalEF's Sunday branch, and computeEF now propagates td.Sunday into the day's temporal candidate. precedence_ef_test.go's own pre-existing witness ("at equal class, the temporal office wins") encoded exactly the bug: a bare class-2 temporal candidate with no Sunday/season information, standing in for "the temporal office" in general. It is rewritten into two explicit cases (Sunday wins its tie; a privileged feria yields) plus the existing Lent/Passiontide-vs-ordinary III/IV-class case restated explicitly rather than left implicit. Witness (precedence_ef_repro_test.go): TestMatthewBeatsSeptemberEmberWednesday. Fails before this commit with: 2016-09-21 observed = "ef-september-ember-wed" want matthew (RG 91 entry 16 beats entry 18) 2022-09-21 observed = "ef-september-ember-wed" want matthew (RG 91 entry 16 beats entry 18) With this commit, all seven named defects are fixed: LECTIO_EF_ORACLE_STRICT=1 go test ./internal/calendar/... -run TestOracleEF passes (0 unallow-listed rank/colour mismatches over 730 days). The gate stays in place for this commit; a following commit removes it. --- internal/calendar/precedence_ef.go | 49 ++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 13 deletions(-) (limited to 'internal/calendar/precedence_ef.go') diff --git a/internal/calendar/precedence_ef.go b/internal/calendar/precedence_ef.go index 4fda626..a1788be 100644 --- a/internal/calendar/precedence_ef.go +++ b/internal/calendar/precedence_ef.go @@ -22,24 +22,47 @@ func efRankOrder(r Rank) int { } // precedenceEF ranks an EF candidate for occurrence (lower = higher precedence): -// by class first, then a tie-break at equal class (1960 occurrence table). +// by class first, then a tie-break at equal class (RG 91's Table of +// Precedence, read entry-by-entry within each class). // -// The tie-break is asymmetric by season. An ORDINARY feria (per annum, Advent, -// Septuagesima) yields to an equal-class feast — the feast is celebrated and the -// feria commemorated (e.g. St Francis Xavier, Dec 3, on an Advent feria). The -// ferias of Lent and Passiontide are privileged: they outrank an equal-class -// feast, which is only commemorated. Sundays and named temporal feasts likewise -// win their ties. The *2 class spacing means the ±1 tie-break never crosses a -// class boundary. +// The tie-break is asymmetric by season AND, at class 2, by whether the +// temporal day is a Sunday or a privileged FERIA: +// - III/IV class: an ORDINARY feria (per annum, Advent to 16 Dec, +// Septuagesima) yields to an equal-class feast (entries 24/25 above the +// feria) -- the feast is celebrated, the feria commemorated (e.g. St +// Francis Xavier, Dec 3). The ferias of Lent and Passiontide are +// privileged (entry 22, ABOVE entry 24): they outrank an equal-class +// feast, which is only commemorated. +// - II class: an ordinary SUNDAY wins its tie (entry 15, above entry 16's +// feasts) -- but a II-class privileged FERIA (the Ember days, the +// late-Advent 17-23 Dec ferias, entry 18) sits BELOW entry 16 and yields +// to an equal-class feast, the opposite of a Sunday's own tie. This is +// the same table shape as III class's Advent/per-annum ferias, just +// inverted for which II-class temporal days count as "privileged". +// - I class: Sundays (entry 6) and I-class ferias (entry 7, Ash +// Wednesday/Holy Week) both sit above entry 11's feasts, so they always +// win -- there is no I-class "ordinary feria yields" case at all. +// +// The *2 class spacing means the ±1 tie-break never crosses a class boundary. func precedenceEF(c candidate) int { p := (6 - efRankOrder(c.Cel.Rank)) * 2 // class-1 -> 2, ferial -> 12 if c.Temporal { - ordinaryFeria := (c.Cel.Rank == RankClass3 || c.Cel.Rank == RankClass4) && - c.Season != Lent && c.Season != Passiontide - if ordinaryFeria { - p++ // an ordinary feria yields to an equal-class feast + yieldsToFeast := false + switch c.Cel.Rank { + case RankClass3, RankClass4: + yieldsToFeast = c.Season != Lent && c.Season != Passiontide + case RankClass2: + // A II-class Sunday (entry 15) wins; a II-class privileged feria -- + // Ember days, the late-Advent 17-23 Dec ferias (entry 18) -- yields + // to an equal-class feast (entry 16), e.g. St Matthew (21 Sep) + // beating the September Ember Wednesday, or St Thomas (21 Dec) + // beating an Advent late feria. + yieldsToFeast = !c.Sunday + } + if yieldsToFeast { + p++ } else { - p-- // Sundays, named feasts, and penitential ferias win their ties + p-- // Sundays, I-class ferias, and penitential (Lent/Passiontide) ferias win their ties } } else if c.Cel.Class == ClassLord && c.Cel.Rank == RankClass2 { // A II class feast of the Lord takes the place of a II class Sunday it -- cgit v1.3