diff options
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.ml | 149 | ||||
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.mli | 41 | ||||
| -rw-r--r-- | test/test_precedence_ef.ml | 232 |
3 files changed, 276 insertions, 146 deletions
diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index 87cdeeb..39251a7 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -48,33 +48,72 @@ let universal_layer = "ef-universal" let indult_prefix = "indult:" let unclassified = max_int -(* Not an RG citation -- an ordering device for RG 91 entry 14's own text - (scan-verified, docs/research/rules-register.md §4): "14. Festa Domini II - classis, PRIMUM MOBILIA, DEINDE FIXA" -- feasts of the Lord, II class, - MOVABLE FIRST, then fixed. Both halves are the SAME numbered table row, - but the primary text states a real priority between them, not merely two - disjoint categories -- [band]'s own entry-14 branches below must not - literally TIE at the shared integer 14, because a tie would fall to +(* CORRECTED, fix round 1 (coordinator finding 1) -- the ORIGINAL version of + this constant was [-14], justified only by "cannot collide with a real + table position (1..28) or [unclassified]". That is a COLLISION argument, + not an ORDERING one, and [band]'s own return value is compared by + {!Precedence.compare_by}/{!compare_precedence} as a plain [<] -- a + negative value is not merely "distinct from 1..28", it is LOWER than + every one of them, so a movable Class2 Lord feast (Holy Family) would + have beaten every I-class day (entries 1-13) outright, not merely the + fixed Baptism it was built to beat. Not live against the shipped + UNIVERSAL sanctoral data (no I-class universal feast falls 7-13 January), + which is why the original 1583-9999 blast radius (§6.2) showed only the + one intended shape -- but entries 12/13 admit PROPER and INDULT I-class + feasts too (patron/titular/dedication days, {!is_universal}/{!is_indult} + below), which arrive via `data/ef/adjustments.sexp`-style overlays, this + architecture's own advertised extension point -- a diocesan I-class + patronal feast landing 7-13 January would have made the inversion live + immediately, transferring the I-class feast as though a II-class Sunday + had impeded it. Demonstrated directly (fix round 1, reproduced then + reverted): a synthetic Class1 sanctoral candidate on 11 January was + OBSERVED as Holy Family (band -14) and the synthetic candidate itself + TRANSFERRED to the next day -- backwards on every count. + + FIXED: every {!band} branch now returns the real RG 91 entry number + TIMES TEN (entry 1 -> 10, entry 28 -> 280, {!unclassified} untouched at + [max_int]) instead of the bare 1..28 -- not an arbitrary rescale, a + deliberate one: RG 91's own text records a "primum mobilia, deinde + fixa" (movable-then-fixed) split at THREE rows, not only this one -- + entry 12's own ninth sub-item ("Festa indulta I classis, primum + mobilia, deinde fixa"), entry 14 (this one), and entry 20 ("indult + II-class feasts (movable then fixed)", register §4's own summary) -- + and only entry 14 has a live witness in this codebase's data today. The + ×10 scale reserves nine integers of genuine headroom immediately BEFORE + each real entry's own value for exactly this kind of sub-rank, without + requiring a second rescale if entry 12's or entry 20's own movable half + ever needs one too -- a value DERIVABLE from RG 91's own table structure, + not an arithmetic convenience chosen to dodge one collision. [band]'s own + entry-14 comments (below) restate this scale at each branch it touches. *) +let entry_14_fixed_band = 140 + +(* RG 91 entry 14's own text (scan-verified, docs/research/rules-register.md + §4): "14. Festa Domini II classis, PRIMUM MOBILIA, DEINDE FIXA" -- feasts + of the Lord, II class, MOVABLE FIRST, then fixed. Both halves are the + SAME numbered table row, but the primary text states a real priority + between them, not merely two disjoint categories -- [band]'s own + entry-14 branches below must not literally TIE at + {!entry_14_fixed_band}, because a tie would fall to {!Precedence.resolve}'s own kernel-level fallback ([compare_by], Slug.compare), which is deliberately NOT rubric-authoritative (the exact anti-pattern RG 113's own fix already corrected once for commemoration ordering, this file's own header) and would in fact pick the WRONG side here: "commemoration-of-the-baptism-of-the-lord" sorts before "ef-time-after-epiphany-sunday-1" alphabetically, backwards from "primum - mobilia". Negative, so it can never collide with any real table position - (1..28) or [unclassified] (positive [max_int]) by construction -- not a - citation to some entry "13.5" that does not exist in the primary text, - just an ordering fact this integer has to carry because RG 91's own two - half-rows must compare unequal here. The one real witness today: Holy - Family (RG 17(b), temporal_ef.ml's own [holy_family_sunday]) against the - fixed Commemoration of the Baptism of the Lord (13 January) -- the only - date they can ever coincide, since Holy Family only ever falls 7-13 - January and no OTHER fixed Class2-Lord sanctoral entry shares that - window (register §6.0's own subject audit: Transfiguration 6 Aug, - Exaltation of the Cross 14 Sep, Dedication of the Archbasilica 9 Nov, - the Purification 2 Feb, the Baptism 13 Jan -- the Baptism alone falls in - Holy Family's own window). *) -let entry_14_movable_band = -14 + mobilia". Derived from {!entry_14_fixed_band}, one better (lower) than + it, using the one integer of headroom the ×10 scale reserves immediately + before every real entry -- not a citation to some entry "13.5" that does + not exist in the primary text, just the ordering fact RG 91's own two + half-rows require, expressed the same way the whole table now is. The + one real witness today: Holy Family (RG 17(b), temporal_ef.ml's own + [holy_family_sunday]) against the fixed Commemoration of the Baptism of + the Lord (13 January) -- the only date they can ever coincide, since + Holy Family only ever falls 7-13 January and no OTHER fixed Class2-Lord + sanctoral entry shares that window (register §6.0's own subject audit: + Transfiguration 6 Aug, Exaltation of the Cross 14 Sep, Dedication of the + Archbasilica 9 Nov, the Purification 2 Feb, the Baptism 13 Jan -- the + Baptism alone falls in Holy Family's own window). *) +let entry_14_movable_band = entry_14_fixed_band - 1 let is_indult layer = String.starts_with ~prefix:indult_prefix layer let is_universal layer = String.equal layer universal_layer @@ -155,8 +194,12 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc (* Named so entry 8's Sunday exception below can read "one worse than the Sunday it must yield to" rather than a bare integer that happens to equal entry 15's own value; entry 15's own branch returns this same - binding, not a second literal, so the two can never drift apart. *) - let entry_15_band = 15 in + binding, not a second literal, so the two can never drift apart. + CORRECTED, fix round 1 (coordinator finding 1): every branch below now + returns the real RG 91 entry number TIMES TEN, not the bare 1..28 -- + see {!entry_14_fixed_band}'s own comment for why (a genuine ordering + bug the bare scale could not express, not a cosmetic rename). *) + let entry_15_band = 150 in let open Vocab_ef in (* CORRECTED (Task B fix round 1, ef-rg16a): a [Commemoration_only] celebration has NO row in RG 91's table at all, checked FIRST, ahead of @@ -199,9 +242,9 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc ea22ad2. The guard is right; its former justification was not. *) if status = Celebration.Commemoration_only then unclassified (* 1: Nativity, Easter Sunday, Pentecost Sunday (I class w/ octave). *) - else if is_temporal && rank = Class1 && ((m = 12 && d = 25) || off = 0 || off = 49) then 1 + else if is_temporal && rank = Class1 && ((m = 12 && d = 25) || off = 0 || off = 49) then 10 (* 2: Sacred Triduum (Thu-Sat of Holy Week). *) - else if is_temporal && rank = Class1 && off >= -3 && off <= -1 then 2 + else if is_temporal && rank = Class1 && off >= -3 && off <= -1 then 20 (* 3: Epiphany, Ascension, Holy Trinity, Corpus Christi, Sacred Heart, Christ the King. *) else if is_temporal && rank = Class1 @@ -209,25 +252,25 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc || off = 39 (* Ascension *) || off = 56 (* Trinity *) || off = 60 (* Corpus Christi *) || off = 68 (* Sacred Heart *) || Date.compare date (Temporal_ef.christ_the_king (Date.year date)) = 0) - then 3 + then 30 (* 4: Immaculate Conception, Assumption BVM. *) - else if (not is_temporal) && rank = Class1 && ((m = 12 && d = 8) || (m = 8 && d = 15)) then 4 + else if (not is_temporal) && rank = Class1 && ((m = 12 && d = 8) || (m = 8 && d = 15)) then 40 (* 5: Vigil & Octave day of the Nativity. *) - else if is_temporal && rank = Class1 && ((m = 12 && d = 24) || (m = 1 && d = 1)) then 5 + else if is_temporal && rank = Class1 && ((m = 12 && d = 24) || (m = 1 && d = 1)) then 50 (* 6: Sundays of Advent, Lent, Passiontide, and Low Sunday. *) else if is_temporal && rank = Class1 && is_sunday && (season = Advent || season = Lent || season = Passiontide || off = 7) - then 6 + then 60 (* 7: I-class ferias not above -- Ash Wednesday; Mon/Tue/Wed of Holy Week. Thu-Sat of Holy Week are the Triduum, entry 2 above, not this entry. *) - else if is_temporal && rank = Class1 && (off = -46 || (off >= -6 && off <= -4)) then 7 + else if is_temporal && rank = Class1 && (off = -46 || (off >= -6 && off <= -4)) then 70 (* 8: All Souls -- RG 91 entry 8's own text (§4) carries a qualifier this transcription must honour: "yields to an occurring Sunday". 2 November is always Time_after_pentecost (well clear of Advent/Lent/Passiontide and of every other entry's own Easter-relative or fixed date), so a Sunday landing on it is always an ordinary entry-15 II-class Sunday -- the one and only rival this exception ever has to lose to. On such a - Sunday this returns [entry_15_band + 1]: strictly worse than 15 (an + Sunday this returns [entry_15_band + 1]: strictly worse than 150 (an exact tie would fall to Precedence.resolve's slug tie-break, which for "ef-all-souls" against a "ef-time-after-pentecost-sunday-*" slug would make All Souls WIN -- the precise bug this guards against), but @@ -237,20 +280,20 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc feasts transfer) still sees the true I-class candidate it needs to move to 3 November. *) else if (not is_temporal) && rank = Class1 && m = 11 && d = 2 then - if is_sunday then entry_15_band + 1 else 8 + if is_sunday then entry_15_band + 1 else 80 (* 9: Vigil of Pentecost. *) - else if is_temporal && rank = Class1 && off = 48 then 9 + else if is_temporal && rank = Class1 && off = 48 then 90 (* 10: Days within the Octaves of Easter and Pentecost. *) else if is_temporal && rank = Class1 && ((off >= 1 && off <= 6) || (off >= 50 && off <= 55)) - then 10 + then 100 (* 11: I-class feasts of the universal Church not above. *) - else if (not is_temporal) && (not is_vigil) && rank = Class1 && is_universal layer then 11 + else if (not is_temporal) && (not is_vigil) && rank = Class1 && is_universal layer then 110 (* 12: Proper I-class feasts. *) - else if (not is_temporal) && (not is_vigil) && rank = Class1 && not (is_indult layer) then 12 + else if (not is_temporal) && (not is_vigil) && rank = Class1 && not (is_indult layer) then 120 (* 13: Indult I-class feasts. By elimination once 11 and 12 have failed: not the universal layer (11), and marked as an indult overlay (12's "not indult" test having just failed). *) - else if (not is_temporal) && (not is_vigil) && rank = Class1 then 13 + else if (not is_temporal) && (not is_vigil) && rank = Class1 then 130 (* 14, MOVABLE half -- RG 91 entry 14's own "primum mobilia, deinde fixa" ({!entry_14_movable_band}'s own comment has the full citation and reasoning). Checked before the FIXED half immediately below, @@ -272,30 +315,32 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc occurring II-class Sunday with no universal qualifier either). No layer test here, unlike 11/12/13 and 16/19/20: the register does not split this entry into universal/proper/indult, so a proper or - indult feast of the Lord still bands 14, not 19/20. *) - else if (not is_temporal) && (not is_vigil) && rank = Class2 && subject = Subject.Lord then 14 + indult feast of the Lord still bands {!entry_14_fixed_band}, not + 19/20. *) + else if (not is_temporal) && (not is_vigil) && rank = Class2 && subject = Subject.Lord then + entry_14_fixed_band (* 15: Sundays, II class (every Sunday not already named at 6). *) else if is_temporal && rank = Class2 && is_sunday then entry_15_band (* 16: II-class feasts of the universal Church, not of the Lord. *) - else if (not is_temporal) && (not is_vigil) && rank = Class2 && is_universal layer then 16 + else if (not is_temporal) && (not is_vigil) && rank = Class2 && is_universal layer then 160 (* 17: Days within the Octave of the Nativity (26-28 Dec are Stephen, John, the Innocents -- sanctoral, not this entry). *) - else if is_temporal && rank = Class2 && m = 12 && (d = 29 || d = 30 || d = 31) then 17 + else if is_temporal && rank = Class2 && m = 12 && (d = 29 || d = 30 || d = 31) then 170 (* 18: II-class ferias -- Advent 17-23 Dec; Ember days of Advent, Lent, September. *) else if is_temporal && rank = Class2 && ((season = Advent && m = 12 && d >= 17 && d <= 23) || is_ember_18 slug) - then 18 + then 180 (* 19: Proper II-class feasts. *) - else if (not is_temporal) && (not is_vigil) && rank = Class2 && not (is_indult layer) then 19 + else if (not is_temporal) && (not is_vigil) && rank = Class2 && not (is_indult layer) then 190 (* 20: Indult II-class feasts. By elimination, as at 13. *) - else if (not is_temporal) && (not is_vigil) && rank = Class2 then 20 + else if (not is_temporal) && (not is_vigil) && rank = Class2 then 200 (* 21: II-class vigils (Ascension, Assumption, John Baptist, Peter & Paul -- can be temporal- or sanctoral-origin, see the file comment above). *) - else if rank = Class2 && is_vigil then 21 + else if rank = Class2 && is_vigil then 210 (* 22: Ferias of Lent and Passiontide (Thursday after Ash Wednesday to the Saturday before Palm Sunday), except the Ember days (18 above). *) - else if is_temporal && rank = Class3 && (season = Lent || season = Passiontide) then 22 + else if is_temporal && rank = Class3 && (season = Lent || season = Passiontide) then 220 (* 23: III-class feasts in particular calendars. Unlike 11/12 and 14/16 above, the universal entry (24) is the HIGHER number here -- RG 91's own table ranks a particular-calendar III-class feast ahead of a @@ -303,13 +348,13 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc the register states it, not "corrected" into the other classes' pattern. RG 91 has no indult sub-rank at III class, so every non-base layer lands here, not split further. *) - else if (not is_temporal) && (not is_vigil) && rank = Class3 && not (is_universal layer) then 23 + else if (not is_temporal) && (not is_vigil) && rank = Class3 && not (is_universal layer) then 230 (* 24: III-class feasts in the universal calendar. *) - else if (not is_temporal) && (not is_vigil) && rank = Class3 then 24 + else if (not is_temporal) && (not is_vigil) && rank = Class3 then 240 (* 25: Ferias of Advent to 16 Dec, except the Ember days (18 above). *) - else if is_temporal && rank = Class3 && season = Advent then 25 + else if is_temporal && rank = Class3 && season = Advent then 250 (* 26: III-class vigils (St Lawrence). *) - else if rank = Class3 && is_vigil then 26 + else if rank = Class3 && is_vigil then 260 (* 27: Office of the BVM on Saturday -- every otherwise-unoccupied IV-class Saturday, per the historical default that fills it; ordinary Mass propers still make Rogation Mon/Tue/Wed proper without changing the @@ -318,12 +363,12 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc vigils for the same reason 11-13/14/16/19/20/23/24 do: RG 91 has no IV-class vigil at all (RG 91's own vigil list, §4 "Vigils", stops at III class), so one would be an anomaly, not this entry. *) - else if is_temporal && (not is_vigil) && rank = Class4 && weekday = Date.Sat then 27 + else if is_temporal && (not is_vigil) && rank = Class4 && weekday = Date.Sat then 270 (* 28: IV-class ferias -- the unqualified catch-all (temporal_ef.ml's own comment on [ferial_rank] cites the same primary text, "Feriae IV classis"). Excludes vigils for the same reason as 27 above: a IV-class "feria" that is also a vigil is not a feria RG 91 describes. *) - else if (not is_vigil) && rank = Class4 then 28 + else if (not is_vigil) && rank = Class4 then 280 else unclassified (* Task 8: what happens to the day's LOSING candidate (docs/research/ diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli index 86a0ead..239a254 100644 --- a/lib/rites/rite_ef/precedence_ef.mli +++ b/lib/rites/rite_ef/precedence_ef.mli @@ -62,25 +62,40 @@ val ember_prefixes : string list new rule, not a silently wrong one. *) val unclassified : int +(** {!band}'s value for RG 91 entry 14's FIXED half (a sanctoral, i.e. + fixed-date, Class2 feast of the Lord) -- see {!entry_14_movable_band}'s + own comment for why entry 14 needs two distinct values at all. *) +val entry_14_fixed_band : int + (** RG 91 entry 14's own text (§4): "Festa Domini II classis, primum mobilia, deinde fixa" -- feasts of the Lord, II class, MOVABLE first, then fixed. {!band}'s value for the movable half (Holy Family, RG - 17(b)) -- strictly better than the fixed half's own literal [14], - negative so it can never collide with a real table position or - {!unclassified}. See precedence_ef.ml's own comment for the full - citation and why the two halves must not literally tie. *) + 17(b)) -- [entry_14_fixed_band - 1], strictly better than the fixed + half without colliding with entry 13 or any other real table position. + CORRECTED, fix round 1 (coordinator finding 1): this used to be a + negative sentinel, justified only by "cannot collide with a real table + position" -- a COLLISION argument, not an ORDERING one; since {!band} + values are compared by plain [<], a negative value beats every I-class + entry (1-13) as well as the fixed half, not merely the fixed half it + was built to beat. {!band}'s own entries now use the real RG 91 number + TIMES TEN throughout, reserving genuine headroom before every entry -- + see precedence_ef.ml's own comment for the full citation, the counter- + example that found this, and why entries 12 and 20 may need the same + treatment if their own "movable then fixed" halves ever get a witness. *) val entry_14_movable_band : int (** [band ctx c]: RG 91's Table of Precedence. Returns the table's own entry - number -- I class 1-13, II class 14-21, III class 22-26, IV class 27-28; - lower wins (see {!Precedence.rules.band}) -- EXCEPT where the table's own - text states an exception: entry 8 (All Souls) reads "yields to an - occurring Sunday", so on a Sunday this returns a value that - loses to entry 15 rather than the literal integer 8 (see the comment on - entry 8 in precedence_ef.ml for the exact value and why). ALSO - {!unclassified} for any [Cel.Commemoration_only] candidate, checked - first, ahead of every rank-keyed branch -- CORRECTED, Task B fix round - 1 (coordinator finding 1): RG 91's own table enumerates only "dies + number TIMES TEN -- I class 10-130, II class 140-210, III class 220-260, + IV class 270-280; lower wins (see {!Precedence.rules.band}) -- EXCEPT + where the table's own text states an exception: entry 8 (All Souls) + reads "yields to an occurring Sunday", so on a Sunday this returns a + value that loses to entry 15 (150) rather than 80 (see the comment on + entry 8 in precedence_ef.ml for the exact value and why); entry 14 + (Feasts of the Lord, II class) splits into {!entry_14_movable_band} and + {!entry_14_fixed_band} per the table's own "primum mobilia, deinde + fixa". ALSO {!unclassified} for any [Cel.Commemoration_only] candidate, + checked first, ahead of every rank-keyed branch -- CORRECTED, Task B fix + round 1 (coordinator finding 1): RG 91's own table enumerates only "dies liturgici" (entry 24's own text, "Festa III classis..." -- FEASTS), so a celebration reduced to a bare commemoration was never a row in the table to begin with, confirmed by the calendarium's own notation (a diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index 01a54c0..98ae0c8 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -48,96 +48,121 @@ let easter = Comp.gregorian_easter 2026 let off n = D.add_days easter n (* (description, date, candidate, expected RG 91 entry). *) +(* CORRECTED, fix round 1 (coordinator finding 1): every expected [int] + below is now the real RG 91 entry number TIMES TEN (entry 1 -> 10, entry + 28 -> 280), matching [PE.band]'s own rescale -- see precedence_ef.ml's + own [entry_14_fixed_band] comment for why the bare 1..28 scale was a real + ordering bug, not a style choice. The description strings still say the + bare entry number ("14 Feast of the Lord...") -- that names the RG 91 + ROW, not the band integer, and is unaffected by the scale. *) let cases = [ (* Entry 1 -- RG 91 entry 1 (§4): Nativity, Easter Sunday, Pentecost Sunday. *) - ("1 Nativity", mk 2026 12 25, cand "ef-nativity", 1); - ("1 Easter Sunday", off 0, cand "ef-easter-sunday", 1); - ("1 Pentecost Sunday", off 49, cand "ef-pentecost", 1); + ("1 Nativity", mk 2026 12 25, cand "ef-nativity", 10); + ("1 Easter Sunday", off 0, cand "ef-easter-sunday", 10); + ("1 Pentecost Sunday", off 49, cand "ef-pentecost", 10); (* Entry 2 -- RG 91 entry 2 (§4): Sacred Triduum. Thu-Sat of Holy Week, NOT entry 7 (which stops at Wednesday -- see entry 7 below). *) - ("2 Holy Thursday", off (-3), cand "ef-holy-thursday", 2); - ("2 Good Friday", off (-2), cand "ef-good-friday", 2); - ("2 Holy Saturday", off (-1), cand "ef-holy-saturday", 2); + ("2 Holy Thursday", off (-3), cand "ef-holy-thursday", 20); + ("2 Good Friday", off (-2), cand "ef-good-friday", 20); + ("2 Holy Saturday", off (-1), cand "ef-holy-saturday", 20); (* Entry 3 -- RG 91 entry 3 (§4). *) - ("3 Epiphany", mk 2026 1 6, cand "ef-epiphany", 3); - ("3 Ascension", off 39, cand "ef-ascension", 3); - ("3 Trinity", off 56, cand "ef-trinity", 3); - ("3 Corpus Christi", off 60, cand "ef-corpus-christi", 3); - ("3 Sacred Heart", off 68, cand "ef-sacred-heart", 3); - ("3 Christ the King", T.christ_the_king 2026, cand "ef-christ-the-king", 3); + ("3 Epiphany", mk 2026 1 6, cand "ef-epiphany", 30); + ("3 Ascension", off 39, cand "ef-ascension", 30); + ("3 Trinity", off 56, cand "ef-trinity", 30); + ("3 Corpus Christi", off 60, cand "ef-corpus-christi", 30); + ("3 Sacred Heart", off 68, cand "ef-sacred-heart", 30); + ("3 Christ the King", T.christ_the_king 2026, cand "ef-christ-the-king", 30); (* Entry 4 -- RG 91 entry 4 (§4). Sanctoral-origin: neither feast is part of temporal_ef's movable cycle. *) ( "4 Immaculate Conception", mk 2026 12 8, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-immaculate-conception", - 4 ); - ("4 Assumption", mk 2026 8 15, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-assumption", 4); + 40 ); + ("4 Assumption", mk 2026 8 15, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-assumption", 40); (* Entry 5 -- RG 91 entry 5 (§4). *) - ("5 Nativity Vigil", mk 2026 12 24, cand "ef-nativity-vigil", 5); - ("5 Octave day (Circumcision)", mk 2026 1 1, cand "ef-circumcision", 5); + ("5 Nativity Vigil", mk 2026 12 24, cand "ef-nativity-vigil", 50); + ("5 Octave day (Circumcision)", mk 2026 1 1, cand "ef-circumcision", 50); (* Entry 6 -- RG 91 entry 6 (§4). *) - ("6 Advent Sunday", T.advent_start 2026, cand "ef-advent-sunday-1", 6); - ("6 Lent Sunday", off (-42), cand "ef-lent-sunday-1", 6); - ("6 Passion Sunday (I Passiontide)", off (-14), cand "ef-passion-sunday", 6); - ("6 Palm Sunday (II Passiontide)", off (-7), cand "ef-palm-sunday", 6); - ("6 Low Sunday", off 7, cand "ef-low-sunday", 6); + ("6 Advent Sunday", T.advent_start 2026, cand "ef-advent-sunday-1", 60); + ("6 Lent Sunday", off (-42), cand "ef-lent-sunday-1", 60); + ("6 Passion Sunday (I Passiontide)", off (-14), cand "ef-passion-sunday", 60); + ("6 Palm Sunday (II Passiontide)", off (-7), cand "ef-palm-sunday", 60); + ("6 Low Sunday", off 7, cand "ef-low-sunday", 60); (* Entry 7 -- RG 91 entry 7 (§4): Ash Wednesday and Mon/Tue/Wed of Holy Week ONLY -- Thu-Sat are entry 2 above, not this entry. *) - ("7 Ash Wednesday", off (-46), cand "ef-ash-wednesday", 7); - ("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 7); - ("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 7); - ("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 7); + ("7 Ash Wednesday", off (-46), cand "ef-ash-wednesday", 70); + ("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 70); + ("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 70); + ("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 70); (* Entry 8 -- RG 91 entry 8 (§4). 2 Nov 2026 is a Monday (verified independently below the table), so this row is the plain case. The register's own qualifying case -- "yields to an occurring Sunday" -- gets its own row and its own end-to-end test after this table (2 Nov 2025 is a real Sunday). *) - ("8 All Souls (non-Sunday)", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 8); + ("8 All Souls (non-Sunday)", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 80); (* Entry 8's qualifier: "(yields to an occurring Sunday)". 2 Nov 2025 is - a Sunday, so this must NOT be 8 -- it must lose to entry 15 (16 = - entry 15's own value + 1, the exact value precedence_ef.ml documents - and justifies at entry 8's branch). The end-to-end resolve-level - proof that the Sunday actually wins the day is + a Sunday, so this must NOT be 80 -- it must lose to entry 15 (151 = + entry 15's own value, 150, + 1, the exact value precedence_ef.ml + documents and justifies at entry 8's branch). The end-to-end + resolve-level proof that the Sunday actually wins the day is [test_all_souls_yields_to_sunday] below; this row pins the specific integer [band] returns. *) - ("8 All Souls (yields to a Sunday, 2 Nov 2025)", mk 2025 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 16); + ("8 All Souls (yields to a Sunday, 2 Nov 2025)", mk 2025 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 151); (* Entry 9 -- RG 91 entry 9 (§4). *) - ("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 9); + ("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 90); (* Entry 10 -- RG 91 entry 10 (§4): both range boundaries, to guard the off-by-one an inclusive Easter-offset window invites. *) - ("10 Easter octave, day+1", off 1, cand "ef-easter-1-mon", 10); - ("10 Easter octave, day+6", off 6, cand "ef-easter-1-sat", 10); - ("10 Pentecost octave, day+50", off 50, cand "ef-pentecost-1-mon", 10); - ("10 Pentecost octave, day+55", off 55, cand "ef-pentecost-1-sat", 10); + ("10 Easter octave, day+1", off 1, cand "ef-easter-1-mon", 100); + ("10 Easter octave, day+6", off 6, cand "ef-easter-1-sat", 100); + ("10 Pentecost octave, day+50", off 50, cand "ef-pentecost-1-mon", 100); + ("10 Pentecost octave, day+55", off 55, cand "ef-pentecost-1-sat", 100); (* Entry 11 -- RG 91 entry 11 (§4). *) ( "11 Universal I-class feast", mk 2026 6 29, cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-ss-peter-paul", - 11 ); + 110 ); (* Entry 12 -- RG 91 entry 12 (§4). The one non-base-layer case the brief asks for explicitly: same date/rank/subject as 11, only the layer differs, so this row isolates the layer test as the deciding factor. *) ( "12 Proper I-class feast (non-base layer)", mk 2026 6 29, cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:"diocese-warsaw" "ef-local-patron", - 12 ); + 120 ); (* Entry 13 -- RG 91 entry 13 (§4). *) ( "13 Indult I-class feast", mk 2026 6 29, cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:(PE.indult_prefix ^ "local-grant") "ef-indult-feast-1", - 13 ); + 130 ); + (* fix round 1 (coordinator finding 1), NEW: a synthetic I-CLASS + candidate landing INSIDE Holy Family's own 7-13 January window -- + the exact counter-example that found the original [-14] sentinel's + inversion (Holy Family used to beat every I-class entry outright, + not merely the fixed Baptism it was built to beat). Must still + outrank the movable-14 entry below (110 < entry_14_movable_band), + proving the fix, not merely the absence of the old bug's OWN + witness (13 January). Not live against the shipped universal data + (register §6.2) -- this is exactly the kind of I-class PROPER/ + INDULT feast an overlay could add, entries 11-13's own [not + is_temporal] tests notwithstanding. *) + ( "11 SYNTHETIC: a universal I-class feast inside Holy Family's own window \ + (fix round 1, coordinator finding 1) must still outrank the movable half", + mk 2026 1 11, + cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer + "ef-synthetic-class1-probe", + 110 ); (* Entry 14 -- RG 91 entry 14 (§4), deliberately UNQUALIFIED (contrast entry 16, which explicitly says "not of the Lord"). *) ( "14 Feast of the Lord, II class", mk 2026 7 1, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "ef-precious-blood", - 14 ); + PE.entry_14_fixed_band ); (* Entry 14, non-base layer: unlike 11-13/16/19/20/23/24, entry 14 draws - no universal/proper/indult line at all, so this must STILL be 14, not - 19 -- the exact restriction review finding 2 flagged and this row - exists to keep from silently coming back. *) + no universal/proper/indult line at all, so this must STILL be + [PE.entry_14_fixed_band], not 19 -- the exact restriction review + finding 2 flagged and this row exists to keep from silently coming + back. *) ( "14 Feast of the Lord, II class (non-base layer)", mk 2026 7 2, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:"diocese-warsaw" "ef-local-feast-of-the-lord", - 14 ); + PE.entry_14_fixed_band ); (* Entry 14, MOVABLE half -- RG 91 entry 14's own text (§4, scan-verified): "Festa Domini II classis, PRIMUM MOBILIA, DEINDE FIXA" -- movable FIRST, then fixed. Holy Family (RG 17(b), temporal_ef.ml's own @@ -147,35 +172,38 @@ let cases = 18's own Ember row above uses), not a hand-typed slug, so a rename or a dropped [~subject:Lord] on temporal_ef.ml's own side fails this row too, not only test_temporal_ef.ml's. Must be a value STRICTLY - BETTER than the fixed half's own literal 14 immediately above (not - merely different) -- the two are structurally disjoint by [is_temporal] - alone so they can never literally collide on any one candidate, but - {!compare_by}'s own kernel-level tie-break (Slug.compare, deliberately - NOT rubric-authoritative -- the exact anti-pattern RG 113's own fix - already corrected once, this file's own header) is what would decide - between them if they returned the SAME band on the one date they can - coincide (13 January, when it is itself Holy Family's date) -- - "commemoration-of-the-baptism-of-the-lord" sorts before - "ef-time-after-epiphany-sunday-1" alphabetically, backwards from RG - 91's own stated order. See [PE.entry_14_movable_band]'s own comment - in precedence_ef.ml for why the value is negative rather than some - other distinct integer. *) + BETTER than the fixed half ([PE.entry_14_fixed_band] immediately + above, not merely different) -- the two are structurally disjoint by + [is_temporal] alone so they can never literally collide on any one + candidate, but {!compare_by}'s own kernel-level tie-break + (Slug.compare, deliberately NOT rubric-authoritative -- the exact + anti-pattern RG 113's own fix already corrected once, this file's + own header) is what would decide between them if they returned the + SAME band on the one date they can coincide (13 January, when it is + itself Holy Family's date) -- "commemoration-of-the-baptism-of-the- + lord" sorts before "ef-time-after-epiphany-sunday-1" alphabetically, + backwards from RG 91's own stated order. CORRECTED, fix round 1 + (coordinator finding 1): [PE.entry_14_movable_band] used to be a + negative sentinel, which beat every I-class entry too, not merely + entry 14's own fixed half -- see the SYNTHETIC I-class probe row + above, and [PE.entry_14_movable_band]'s own comment in + precedence_ef.ml, for the fix and the counter-example that found it. *) ( "14 Feast of the Lord, II class, MOVABLE half (Holy Family, RG 17(b))", T.holy_family_sunday 2026, of_temporal (T.holy_family_sunday 2026), PE.entry_14_movable_band ); (* Entry 15 -- RG 91 entry 15 (§4): an ordinary Sunday not named at entry 6 -- Septuagesima is II class (RG 11-12 names only Advent/Lent/ Passiontide/Easter/Low/Pentecost as I class). *) - ("15 II-class Sunday (Septuagesima)", off (-63), cand ~rank:V.Class2 "ef-septuagesima-sunday", 15); + ("15 II-class Sunday (Septuagesima)", off (-63), cand ~rank:V.Class2 "ef-septuagesima-sunday", 150); (* Entry 16 -- RG 91 entry 16 (§4). *) ( "16 Universal II-class feast, not of the Lord", mk 2026 1 20, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:PE.universal_layer "ef-some-saint", - 16 ); + 160 ); (* Entry 17 -- RG 91 entry 17 (§4): days WITHIN the Nativity octave (26-28 Dec are Stephen/John/Innocents -- sanctoral, not this entry; 1 Jan is entry 5's Octave DAY, not this entry either). *) - ("17 Nativity octave, 29 Dec", mk 2026 12 29, cand ~rank:V.Class2 "ef-nativity-octave-day-5", 17); - ("17 Nativity octave, 31 Dec", mk 2026 12 31, cand ~rank:V.Class2 "ef-nativity-octave-day-7", 17); + ("17 Nativity octave, 29 Dec", mk 2026 12 29, cand ~rank:V.Class2 "ef-nativity-octave-day-5", 170); + ("17 Nativity octave, 31 Dec", mk 2026 12 31, cand ~rank:V.Class2 "ef-nativity-octave-day-7", 170); (* Entry 18 -- RG 91 entry 18 (§4): Advent 17-23 Dec ferias AND the Ember days of Advent/Lent/September share this one entry. The second row is deliberately a Lent date (season Lent, NOT Advent) to prove the @@ -183,21 +211,21 @@ let cases = to fall in the Dec 17-23 window -- the exact trap the brief warns about, worked the other way round: this Ember day must NOT be mistaken for an ordinary entry-22 Lent feria either. *) - ("18 Advent 17-23 Dec feria", mk 2026 12 21, cand ~rank:V.Class2 "ef-advent-4-mon", 18); + ("18 Advent 17-23 Dec feria", mk 2026 12 21, cand ~rank:V.Class2 "ef-advent-4-mon", 180); (* Sourced from Temporal_ef.temporal's own output (see [of_temporal]) rather than a hand-typed "ef-lent-ember-wed" -- closes review finding 3's coupling concern for the Ember prefixes specifically. *) - ("18 Lent Ember Wednesday (from Temporal_ef.temporal)", off (-39), of_temporal (off (-39)), 18); + ("18 Lent Ember Wednesday (from Temporal_ef.temporal)", off (-39), of_temporal (off (-39)), 180); (* Entry 19 -- RG 91 entry 19 (§4). *) ( "19 Proper II-class feast", mk 2026 1 20, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:"diocese-warsaw" "ef-local-saint-2", - 19 ); + 190 ); (* Entry 20 -- RG 91 entry 20 (§4). *) ( "20 Indult II-class feast", mk 2026 1 20, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:(PE.indult_prefix ^ "local-grant-2") "ef-indult-feast-2", - 20 ); + 200 ); (* Entry 21 -- RG 91 entry 21 (§4, RG 28-34). Two rows: the Ascension Vigil is the one II-class vigil temporal_ef already produces today (temporal-origin); the Assumption Vigil stands in for the @@ -207,22 +235,22 @@ let cases = (* Sourced from Temporal_ef.temporal's own output (see [of_temporal]) rather than a hand-typed "ef-ascension-vigil" -- closes review finding 3's coupling concern for [vigil_suffix]. *) - ("21 Ascension Vigil (from Temporal_ef.temporal)", off 38, of_temporal (off 38), 21); + ("21 Ascension Vigil (from Temporal_ef.temporal)", off 38, of_temporal (off 38), 210); ( "21 Assumption Vigil (sanctoral-origin)", mk 2026 8 14, cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-assumption-vigil", - 21 ); + 210 ); (* Also review finding 3 / "worth doing": a UNIVERSAL-layer Class2 vigil whose subject is the Lord must still be 21, not 14 -- pins entry 14's [not is_vigil] guard even after finding 2 dropped its layer test. *) ( "21 Universal II-class vigil of the Lord", mk 2026 6 23, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "ef-precious-blood-vigil", - 21 ); + 210 ); (* Entry 22 -- RG 91 entry 22 (§4) (corrected: ends at Palm Sunday, not Passion Sunday). Both a Lent and a Passiontide feria, clear of Ash Wednesday, Holy Week and the Ember days. *) - ("22 Lent feria", off (-41), cand ~rank:V.Class3 "ef-lent-1-mon", 22); - ("22 Passiontide feria", off (-12), cand ~rank:V.Class3 "ef-passiontide-1-tue", 22); + ("22 Lent feria", off (-41), cand ~rank:V.Class3 "ef-lent-1-mon", 220); + ("22 Passiontide feria", off (-12), cand ~rank:V.Class3 "ef-passiontide-1-tue", 220); (* Entry 23 -- RG 91 entry 23 (§4). NOTE the table's own order here is the REVERSE of 11/12 and 14/16/19/20 above: entry 23 (particular calendars) is numbered BELOW entry 24 (universal), so a proper @@ -230,45 +258,45 @@ let cases = register states it, not "corrected" to match the other classes. *) ( "23 Proper III-class feast (non-base layer)", mk 2026 6 30, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-saint-3", - 23 ); + 230 ); (* Entry 24 -- RG 91 entry 24 (§4). *) ( "24 Universal III-class feast", mk 2026 6 30, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3", - 24 ); + 240 ); (* Entry 25 -- RG 91 entry 25 (§4). *) - ("25 Advent feria to 16 Dec", mk 2026 12 1, cand ~rank:V.Class3 "ef-advent-1-tue", 25); + ("25 Advent feria to 16 Dec", mk 2026 12 1, cand ~rank:V.Class3 "ef-advent-1-tue", 250); (* Entry 26 -- RG 91 entry 26 (§4). *) ( "26 III-class vigil", mk 2026 8 9, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil", - 26 ); + 260 ); (* Also worth doing: a NON-universal-layer Class3 vigil must still be 26, not 23 -- pins entry 23's [not is_vigil] guard. *) ( "26 III-class vigil (non-base layer)", mk 2026 8 10, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-patron-vigil", - 26 ); + 260 ); (* Task 11, issue (a): the sanctoral bootstrap (data/ef/sanctoral.sexp) names its vigils with lectio's OWN "vigil-of-X" PREFIX convention, not [PE.vigil_suffix]'s "-vigil" SUFFIX every row above uses -- exactly the mismatch Task 7's review predicted. These two rows use the real bootstrapped slugs verbatim (data/ef/sanctoral.sexp: 28 Jun, 9 Aug), proving [band] recognises the prefix convention too: without it, both - would misfile at 16/24 (an ordinary feast of the same rank) instead - of 21/26. *) + would misfile at 160/240 (an ordinary feast of the same rank) instead + of 210/260. *) ( "21 II-class vigil via the sanctoral data's own \"vigil-of-X\" prefix", mk 2026 6 28, cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "vigil-of-sts-peter-paul", - 21 ); + 210 ); ( "26 III-class vigil via the sanctoral data's own \"vigil-of-X\" prefix", mk 2026 8 9, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "vigil-of-st-lawrence", - 26 ); + 260 ); (* Entry 27 -- RG 91 entry 27 (§4): an otherwise-unoccupied IV-class Saturday. *) ( "27 Office of the BVM on Saturday", off 62, cand ~rank:V.Class4 "ef-time-after-pentecost-1-sat", - 27 ); + 270 ); (* Entry 28 -- RG 91 entry 28 (§4): the unqualified IV-class catch-all. *) - ("28 IV-class feria", off 65, cand ~rank:V.Class4 "ef-time-after-pentecost-1-tue", 28); + ("28 IV-class feria", off 65, cand ~rank:V.Class4 "ef-time-after-pentecost-1-tue", 280); (* Not an RG 91 row at all: a I-class candidate marked as a vigil, which is not the Nativity or Pentecost (entries 5/9, the only I-class vigils the table names) and so has no entry to fall into. Proves the @@ -1343,6 +1371,44 @@ let test_holy_family_excludes_baptism_end_to_end () = [ "commemoration-of-the-baptism-of-the-lord" ] (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.omitted) +(* Fix round 1 (coordinator finding 1), end-to-end proof of the + [entry_14_movable_band] fix itself: a genuine I-class candidate landing + inside Holy Family's own 7-13 January window (a synthetic proper/indult + feast, standing in for the diocesan-overlay shape {!band}'s own new + comment names as the live risk) must be OBSERVED and must NOT transfer + -- reproducing, at the [Precedence.resolve] level, the exact regression + the coordinator demonstrated against the pre-fix [-14] sentinel ("the + I-class feast displaced and transferred to the next day"). With the fix, + {!band}'s entry 11 (110) beats {!PE.entry_14_movable_band} (139) + outright, so the I-class candidate wins the day, and Holy Family itself + -- losing, is_temporal, its own slug carrying the "-sunday" marker -- + falls to RG 109(a)'s ordinary privileged-Sunday-commemoration path + (disposition's final catch-all, [privilege_of]'s (a) branch), the same + treatment ANY genuine feast outranking an ordinary Sunday gets: neither + omitted nor transferred, a Privileged commemoration. *) +let test_class1_feast_inside_holy_familys_window_end_to_end () = + let date = mk 2026 1 11 in + Alcotest.(check string) "sanity: this is Holy Family's own date in 2026" "2026-01-11" (D.to_iso8601 date); + let day_ctx = ctx date in + let holy_family = of_temporal date in + let synthetic_class1 = + cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-synthetic-class1-probe" + in + let resolution = P.resolve real_rules day_ctx ~temporal:holy_family ~sanctoral:[ synthetic_class1 ] in + Alcotest.(check string) "the I-class candidate is observed, NOT Holy Family (RG91 e11 beats e14-movable)" + "ef-synthetic-class1-probe" (S.to_string resolution.P.observed.P.cel.Cel.slug); + Alcotest.(check (list string)) "nothing transfers -- the pre-fix bug moved the I-class feast to the next day" + [] (List.map (fun c -> S.to_string c.P.cel.Cel.slug) resolution.P.deferred); + (match resolution.P.commemorations with + | [ (c, P.Privileged) ] -> + Alcotest.(check string) + "Holy Family itself is privileged-commemorated (RG109(a), a Sunday-shaped office \ + outranked by a genuine feast), neither omitted nor transferred" + "ef-time-after-epiphany-sunday-1" (S.to_string c.P.cel.Cel.slug) + | other -> + Alcotest.failf "expected exactly one Privileged commemoration (Holy Family), got %d" + (List.length other)) + (* Completes Task 7's carried fix (RG 91 entry 8, §4): on a real Sunday landing on 2 November, All Souls does not merely lose (that was Task 7's [band] fix, proved by [test_all_souls_yields_to_sunday] above) -- it must @@ -1594,6 +1660,10 @@ let suite = "RG112(a) + RG91 e14 \"primum mobilia\": Holy Family excludes the Baptism of the Lord \ entirely, end-to-end (13 Jan 2019)" `Quick test_holy_family_excludes_baptism_end_to_end; + Alcotest.test_case + "fix round 1, finding 1: a synthetic I-class candidate inside Holy Family's own window is \ + observed and does not transfer (2026-01-11)" + `Quick test_class1_feast_inside_holy_familys_window_end_to_end; Alcotest.test_case "transfer_target: general RG96 search walks past more than one blocked day" `Quick test_transfer_target_general_multi_step_search; Alcotest.test_case "transfer_target: Annunciation exception starts at Monday after Low Sunday" |
