From f4cc032d7e812d716ff6b5df8192f79a2560e8f0 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 22 Aug 2026 17:56:42 +0200 Subject: feat(ef): the Gloria in excelsis, RG 431-432, deferring to Breviary 237-238 Phase 2 of celebrant-rubrics-phase1: colitur rubrics gains a fifth column, whether the Gloria is said. Follows the Creed's own seam exactly -- Rite.t.gloria, Liturgical_day.t.gloria, wired through calendar.ml the same way. RG 431(a)/432(a) defer the Gloria to the Breviary's own Te Deum rule (nn. 237-238), so te_deum is implemented as its own named predicate, cited clause by clause, not collapsed into a colour heuristic. 431(c) (Holy Thursday, the Easter Vigil Mass) and 432(b)/(d) (violet; a Requiem) are independent overrides checked ahead of the Te Deum-derived answer. Every clause this engine has no dimension to model (votive Mass classes, the wider n.302 "Missa festiva" categories) is stated as N/A with its own reasoning, not silently dropped. Validated against the FIUV universal Ordo (Gloria and Te Deum) and all three LMS editions (Gloria). A first pass over-trusted a clean-looking 15-for-15 FIUV contradiction of 237(b)'s own Septuagesima exception and replaced it with a blanket "every Sunday" rule; the evidence was itself corrupted -- the FIUV extractor recognised only one of the source's two Te Deum negations ("non dicitur", not "sine"), so every "sine Te Deum" Sunday read wrongly true. Fixed in tools/extract_fiuv_ordo.ml, fixture re-extracted, and the literal 237(b) reading restored once the corrected data confirmed it. A second bug surfaced alongside it (Palm/ Passion Sunday wrongly reading true via Temporal_ef.named's own table membership, then Christ the King wrongly reading false from an over-broad fix) is closed with an explicit two-slug exclusion. Domain-wide 1583-9999: every violet or Rose day is gloria=false except the Easter Vigil (RG 431(c) lex specialis), every Requiem is gloria=false, both measured exhaustively, zero exceptions. Mutation- proved: disabling 431(c) reddens 8 tests including all four oracle comparisons; disabling 238(c)'s feria-I-classis exclusion reddens exactly the dedicated Ash Wednesday unit test, a genuine blind spot in both oracle layers, reported rather than hidden. Two open, cited findings, neither fixed here (out of this task's "follow creed's exact seam" scope): a privileged Lenten/Passiontide feria carrying one commemoration reads Gloria=true in the LMS Ordo but Te-Deum=true/Gloria=false in FIUV -- the two oracles disagree with each other, not merely with colitur (data/ef/expected-divergences-lms.sexp L5, expected-divergences-fiuv.sexp F3); and a pre-existing, uncited Colour.Violet bug on Rogation Monday/Tuesday in Temporal_ef.temporal, surfaced by this comparison but root-caused as a separate defect (L6). day/readings verified byte-identical against a build from the branch tip before this task (v0.10.1's own tag predates an already-landed bissextile fix that legitimately changed both, so it is not the right baseline). 671 tests green (dune test); 678 with the exhaustive sweep (COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force, ~104s). --- test/test_rubrics_ef.ml | 324 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 323 insertions(+), 1 deletion(-) (limited to 'test/test_rubrics_ef.ml') diff --git a/test/test_rubrics_ef.ml b/test/test_rubrics_ef.ml index a778a5a..71f180d 100644 --- a/test/test_rubrics_ef.ml +++ b/test/test_rubrics_ef.ml @@ -46,6 +46,23 @@ let creed_on y m d = (Cal.day ctx layer (mk y m d)).LD.creed let check name y m d expected = Alcotest.(check bool) name expected (creed_on y m d) +(* ------------------------------------------------------------------------ *) +(* Breviary 237-238 (Te Deum) and RG 431-432 (Gloria) -- Phase 2 of the *) +(* celebrant-rubrics-phase1 design. See lib/rites/rite_ef/rubrics_ef.ml for *) +(* both rubrics quoted in full and every branch's own citation. One *) +(* end-to-end test per clause, resolved against REAL calendar dates through *) +(* the shipped data -- every expected value below was read off *) +(* `colitur rubrics `'s real output BEFORE being pinned here, the *) +(* same discipline the Creed tests above already establish. *) + +let day_on y m d = Cal.day ctx layer (mk y m d) +let gloria_on y m d = (day_on y m d).LD.gloria +let te_deum_on y m d = RE.te_deum ~temporal:(day_on y m d).LD.temporal ~observed:(day_on y m d).LD.observed + ~date:(mk y m d) + +let check_gloria name y m d expected = Alcotest.(check bool) name expected (gloria_on y m d) +let check_te_deum name y m d expected = Alcotest.(check bool) name expected (te_deum_on y m d) + (* ---- RG 475(a): "in qualibet dominica, etsi eius Officium alicui festo locum cedat" ---- *) @@ -440,6 +457,277 @@ let test_ferial_origin_never_carries_lord_bvm_or_apostle_slug () = Alcotest.(check bool) "span reached Lenten/Passiontide III-class ferias" true (!lenten_passiontide_class3 > 0) +(* ---- 237(a): the three named Paschaltide days ---- *) + +let test_237a_easter_sunday () = check_te_deum "237(a): Easter Sunday" 2026 4 5 true +let test_237a_low_sunday () = check_te_deum "237(a): Low Sunday" 2026 4 12 true +let test_237a_pentecost () = check_te_deum "237(a): Pentecost Sunday" 2026 5 24 true + +(* ---- 237(f): the vigils of Ascension and Pentecost, both otherwise + reachable by 238(b) (the Ascension vigil is Class2) or moot to it + (the Pentecost vigil is Class1) ---- *) + +let test_237f_ascension_vigil () = check_te_deum "237(f): Vigil of the Ascension" 2026 5 13 true +let test_237f_pentecost_vigil () = check_te_deum "237(f): Vigil of Pentecost" 2026 5 23 true + +(* ---- 237(d): the three privileged octaves, reusing [creed]'s own windows + -- a day within the Nativity octave whose OWN office is a real saint + (RG 67's "Com. octavae Nativitatis"), and an Easter-week feria outside + the narrower 237(a) list ---- *) + +let test_237d_nativity_octave_saint () = + check_te_deum "237(d): St John within the Nativity octave" 2026 12 27 true + +let test_237d_easter_week_feria () = check_te_deum "237(d): Easter Tuesday" 2026 4 7 true + +(* ---- 238(b): the omissible (Class2/Class3) vigils -- St Lawrence's own, + picked in a year where nothing displaces it, confirmed via + `colitur day 2026` first the same way the Creed tests were derived ---- *) + +let test_238b_omissible_vigil () = + (* 9 August 2026 is itself a Sunday (the vigil impeded, see 237(b)'s own + test below, which reuses that exact date) -- 2027 is picked instead, + confirmed via `colitur day 2027` first, the same discipline every + other date in this file follows. *) + check_te_deum "238(b): Vigil of St Lawrence" 2027 8 9 false + +(* ---- The Nativity Vigil (I class): NOT literally named by 238(b)'s own + "II et III classis" text -- excluded here on the structural RG 21/35 + taxonomy inference [te_deum]'s own comment states explicitly as an + inference, not a citation. Flagged the same way in the task report; + checked against the FIUV Ordo's own Te Deum marker for 24 December in + test_fiuv_ordo.ml, which is this inference's real corroboration. ---- *) + +let test_nativity_vigil_excluded_by_inference () = + check_te_deum "Nativity Vigil (I class): excluded, inference not literal 238(b)" 2026 12 24 false + +(* ---- 238(c)/RG23: Ash Wednesday and Good Friday (moot -- no Mass, but the + RG23 feria-I-classis exclusion still answers [false] regardless of + colour) ---- *) + +let test_238c_ash_wednesday () = check_te_deum "238(c)/RG23: Ash Wednesday" 2026 2 18 false +let test_238c_good_friday () = check_te_deum "238(c)/RG23: Good Friday" 2026 4 3 false + +(* ---- 237(g): the votive Office of the BVM on Saturday -- reused directly + from test/cli.t's own pinned example ---- *) + +let test_237g_bvm_saturday () = check_te_deum "237(g): BVM Saturday Office" 2026 7 11 true + +(* ---- 237(e): a plain, non-octave, non-Sunday feria of Christmastide (2-5 + January) ---- *) + +let test_237e_christmastide_feria () = + check_te_deum "237(e): 2 January, a Christmastide feria" 2026 1 2 true + +(* ---- 237(b): an ordinary II-class Sunday outside Septuagesima, and its + own explicit exception (a Septuagesima/Sexagesima Sunday, [Class2], + excepted by name) ---- *) + +let test_237b_ordinary_class2_sunday () = + check_te_deum "237(b): an ordinary Time-after-Pentecost Sunday" 2026 8 9 true + +(* RE-VERIFIED against the CORRECTED FIUV extraction ([te_deum]'s own + 237(a) comment has the full account of the extractor bug an earlier + pass of this task found and fixed): 237(b)'s own literal "exceptis + dominicis in Septuagesima, in Sexagesima et in Quinquagesima" holds + after all -- these two Sundays do NOT say the Te Deum, confirmed + against the corrected data, not merely the original transcription + alone. *) +let test_237b_septuagesima_exception () = + check_te_deum "237(b)'s own exception: Septuagesima Sunday" 2027 1 24 false + +let test_237b_sexagesima_exception () = + check_te_deum "237(b)'s own exception: Sexagesima Sunday" 2027 1 31 false + +(* ---- 237(c): a genuine sanctoral feast kept during a penitential season + (St Paul of the Cross, Class3, outside any privileged window) and one in + Ordinary Time (Lawrence, Class2) ---- *) + +let test_237c_sanctoral_feast () = check_te_deum "237(c): a sanctoral feast (Lawrence)" 2026 8 10 true + +(* Found alongside the FIUV extractor-bug fix ([te_deum]'s own 237(c) + comment has the full account): Passion Sunday and Palm Sunday are BOTH + entries in {!Temporal_ef.named} (named individually for RG 91 entry 6), + which without the [temporal.weekday <> Sun] guard on 237(c)'s own + [named<>None] disjunct would wrongly grant them [true] by ACCIDENT of + table membership -- neither is a genuine "festum" (RG 35's own "dies + dominica" is its own category), and both are [Class1] Sundays 237(b)'s + own [Class2] guard already excludes. Confirmed [false] directly against + the corrected FIUV extraction. *) +let test_passion_sunday_not_a_festum () = check_te_deum "Passion Sunday is not a festum for 237(c)" 2026 3 22 false +let test_palm_sunday_not_a_festum () = check_te_deum "Palm Sunday is not a festum for 237(c)" 2026 3 29 false + +(* ---- 238(d): the Requiem proxy, shared with [creed]'s own 476(f) and the + SAME two-member {!Colour.Black} population + {!test_colour_black_population_is_exactly_two} already asserts ---- *) + +let test_238d_all_souls () = check_te_deum "238(d): All Souls' Day (transferred)" 2025 11 3 false + +(* ---- RG 431(c): Holy Thursday and the Easter Vigil Mass say the Gloria + even though neither day's own Matins says the Te Deum (both are + feria-I-classis, [te_deum]'s own 238(c) branch) -- the one place [gloria] + and [te_deum] genuinely disagree on real data. Holy Saturday also proves + 431(c) outranks 432(b): its own colour is [Violet] + (Passiontide's [season_colour]), yet the Gloria is still said. ---- *) + +let test_431c_holy_thursday () = check_gloria "431(c): Holy Thursday" 2026 4 2 true +let test_431c_easter_vigil () = check_gloria "431(c): the Easter Vigil Mass (Holy Saturday)" 2026 4 4 true + +(* ---- RG 432(b): violet vestments -- independent of [te_deum], checked + against an Advent Sunday (I class, so [te_deum] would ALSO answer + [false] here via its own 237(b) rank guard: this is a same-answer + witness, not proof of independent teeth -- see the task report for why + no fully independent (te_deum=true, violet) witness exists anywhere in + the shipped 1583-9999 domain: every real [Colour.Violet] [Feast] entry + is one of the five sanctoral vigils, already excluded by [te_deum]'s + own [is_vigil] branch either way). ---- *) + +let test_432b_violet_sunday () = check_gloria "432(b): Advent I Sunday, violet" 2026 11 29 false + +(* ---- RG 432(d): the Requiem proxy, same population as 238(d) above ---- *) + +let test_432d_all_souls () = check_gloria "432(d): All Souls' Day (transferred)" 2025 11 3 false + +(* ---- RG 431(a)/432(a): mirrors [te_deum] for everything not already + decided above -- one true, one false, neither reachable via 431(c)/432(b)/ + 432(d) ---- *) + +let test_431a_mirrors_te_deum_true () = + check_gloria "431(a): mirrors [te_deum]=true (a sanctoral feast, Lawrence)" 2026 8 10 true + +let test_432a_mirrors_te_deum_false () = + check_gloria "432(a): mirrors [te_deum]=false (an ordinary Advent feria)" 2025 12 1 false + +(* ---- Domain-wide sanity, task requirement 5: "every violet day must be + false (RG 432 b), and every Requiem must be false (432 d)". Two + invariants, checked over every day {!Cal.year} resolves -- [Cal.year], + not [Cal.day] in a loop, for the same cost reason + {!test_every_sunday_in_2026_says_the_creed} above already gives. + + FAST (default suite): a fixed 200-year span, 1583-1782 -- large enough + to cross multiple Easter cycles and every season repeatedly, cheap + enough to stay in `dune test`'s own budget. EXHAUSTIVE (gated the same + way {!test_validate.test_exhaustive_domain_sweep} already is, via + COLITUR_EXHAUSTIVE_SWEEP): the full 1583-9999 domain, also tallying and + printing the true/false distribution so the measurement this task asks + for is not merely "did the invariant hold" but has a number attached -- + see the task report for the printed figures. *) +(* NOT a blanket "every violet day is [gloria]=false" (the task brief's own + phrasing, taken literally, is one exception too strong): RG 431(c) is a + NAMED, lex-specialis override for the Easter Vigil Mass, and colitur's + own per-day colour model gives Holy Saturday [Colour.Violet] + ([gloria]'s own header has the full argument for why -- the historical + violet-to-white vestment change happens AT the Gloria itself, a + per-action nuance this whole day/colour model already cannot express). + So the real invariant, checked here, is "every violet day is + [gloria]=false EXCEPT the Easter Vigil (Easter offset -1), which is + [gloria]=true BY DESIGN" -- and the exception is asserted to be EXACTLY + that one shape, every year, nothing else: [n = -1] is checked directly + rather than merely excluded, so a second, unexpected (violet, + gloria=true) day anywhere in the domain still fails loudly. Pushed back + on the task brief's own simplified phrasing rather than silently + special-cased -- see the task report. *) +let check_gloria_invariants_for_year y (counts : (int * int * int * int) ref) = + let days = Cal.year ctx layer y in + (* Per-date, NOT once per loop iteration off [y]: a liturgical year + "opening in civil year y" ({!Rite.t.year_start}'s own doc comment) + runs from that year's Advent into MOST of civil year y+1 -- so + [Cal.year ctx layer y]'s own array holds dates whose civil year is + y+1 for the whole Christmas-to-Pentecost span, governed by EASTER OF + y+1, not y. A single [Computus.gregorian_easter y] computed once here + wrongly used year y's own Easter for those dates -- found live: it + misidentified 1584-03-31 (Holy Saturday, governed by 1584's Easter, + surfaced while processing loop iteration y=1583) as an ordinary + violet day instead of the Vigil's own 431(c) exception, because + Easter 1583 (not 1584) was subtracted. Fixed by keying off + [Date.year d.LD.date] instead, which is always safe here: Holy + Saturday and Easter Sunday are never more than a few days apart and + never cross a civil-year boundary. *) + let violet, black, gloria_true, gloria_false = !counts in + let violet = ref violet + and black = ref black + and gloria_true = ref gloria_true + and gloria_false = ref gloria_false in + Array.iter + (fun (d : (V.season, V.rank) LD.t) -> + (if d.LD.gloria then incr gloria_true else incr gloria_false); + if d.LD.observed.Cel.colour = Colour.Violet then begin + incr violet; + let easter = Computus.gregorian_easter (Date.year d.LD.date) in + let n = Date.to_rata d.LD.date - Date.to_rata easter in + if n = -1 then + Alcotest.(check bool) + (Printf.sprintf "%s: the Easter Vigil's own 431(c) override, [gloria]=true despite violet" + (Date.to_iso8601 d.LD.date)) + true d.LD.gloria + else + Alcotest.(check bool) + (Printf.sprintf "%s: violet -> [gloria]=false (RG 432(b))" (Date.to_iso8601 d.LD.date)) + false d.LD.gloria + end; + if d.LD.observed.Cel.colour = Colour.Rose then + (* Gaudete/Laetare -- [gloria] reads [false] here via [te_deum]'s + own [Class2] guard on 237(b) (both Rose Sundays are [Class1] BY + CONSTRUCTION -- {!Rite_ef.Rubrics_ef.gloria}'s own 432(b) + citation has the full "checked, then found redundant, then + removed" account of why 432(b) itself does NOT need its own + Rose branch). Not folded into the [violet] counter above: + keeping the two colours separately tallied is what let this + invariant catch the Gaudete/Laetare gap live in the first place + (found via the LMS Ordo, 2023-12-17 and 2024-03-10) rather than + silently averaging it away inside one shared bucket. *) + Alcotest.(check bool) + (Printf.sprintf "%s: Rose (Gaudete/Laetare) -> [gloria]=false" (Date.to_iso8601 d.LD.date)) + false d.LD.gloria; + if d.LD.observed.Cel.colour = Colour.Black then begin + incr black; + Alcotest.(check bool) + (Printf.sprintf "%s: Requiem (black) -> [gloria]=false (RG 432(d))" (Date.to_iso8601 d.LD.date)) + false d.LD.gloria + end) + days; + counts := (!violet, !black, !gloria_true, !gloria_false) + +let test_domain_violet_implies_no_gloria_sample () = + let counts = ref (0, 0, 0, 0) in + for y = 1583 to 1782 do + check_gloria_invariants_for_year y counts + done; + let violet, _, _, _ = !counts in + Alcotest.(check bool) "the 200-year sample reached a real number of violet days" true (violet > 1000) + +let test_domain_requiem_implies_no_gloria_sample () = + (* Separate test name, same underlying sweep as the one immediately above + -- {!Cal.year} is only computed once per year regardless (module-level + [ctx]/[layer], no per-test reload), so this is not a second pass over + the domain, only a second, independently-named assertion on the same + tally, matching how {!test_colour_black_population_is_exactly_two} + above separates its own DATA and CODE checks into one function while + this pair keeps violet and black as two named outcomes. *) + let counts = ref (0, 0, 0, 0) in + for y = 1583 to 1782 do + check_gloria_invariants_for_year y counts + done; + let _, black, _, _ = !counts in + Alcotest.(check bool) "the 200-year sample reached at least one Requiem day" true (black > 0) + +let colitur_exhaustive_sweep_env = "COLITUR_EXHAUSTIVE_SWEEP" + +let test_exhaustive_gloria_domain_sweep () = + if Sys.getenv_opt colitur_exhaustive_sweep_env = None then Alcotest.skip () + else begin + let counts = ref (0, 0, 0, 0) in + for y = 1583 to 9999 do + check_gloria_invariants_for_year y counts + done; + let violet, black, gloria_true, gloria_false = !counts in + Printf.printf + "gloria domain sweep 1583..9999: violet=%d black=%d gloria_true=%d gloria_false=%d total=%d\n%!" + violet black gloria_true gloria_false (gloria_true + gloria_false); + Alcotest.(check bool) "the full domain reached a real number of violet days" true (violet > 100_000); + Alcotest.(check bool) "the full domain reached a real number of Requiem days" true (black > 1_000) + end + let suite = ( "Rubrics_ef", [ Alcotest.test_case "475(a): ordinary Sunday" `Quick test_475a_ordinary_sunday; @@ -477,4 +765,38 @@ let suite = Alcotest.test_case "every Sunday in 2026 says the Creed" `Quick test_every_sunday_in_2026_says_the_creed; Alcotest.test_case "RG 24/25: no ferial-origin office carries Lord/Bvm or an apostle slug" `Quick - test_ferial_origin_never_carries_lord_bvm_or_apostle_slug ] ) + test_ferial_origin_never_carries_lord_bvm_or_apostle_slug; + Alcotest.test_case "237(a): Easter Sunday" `Quick test_237a_easter_sunday; + Alcotest.test_case "237(a): Low Sunday" `Quick test_237a_low_sunday; + Alcotest.test_case "237(a): Pentecost Sunday" `Quick test_237a_pentecost; + Alcotest.test_case "237(f): Vigil of the Ascension" `Quick test_237f_ascension_vigil; + Alcotest.test_case "237(f): Vigil of Pentecost" `Quick test_237f_pentecost_vigil; + Alcotest.test_case "237(d): St John within the Nativity octave" `Quick + test_237d_nativity_octave_saint; + Alcotest.test_case "237(d): Easter Tuesday" `Quick test_237d_easter_week_feria; + Alcotest.test_case "238(b): Vigil of St Lawrence" `Quick test_238b_omissible_vigil; + Alcotest.test_case "Nativity Vigil excluded by structural inference, not literal 238(b)" `Quick + test_nativity_vigil_excluded_by_inference; + Alcotest.test_case "238(c)/RG23: Ash Wednesday" `Quick test_238c_ash_wednesday; + Alcotest.test_case "238(c)/RG23: Good Friday" `Quick test_238c_good_friday; + Alcotest.test_case "237(g): BVM Saturday Office" `Quick test_237g_bvm_saturday; + Alcotest.test_case "237(e): 2 January, a Christmastide feria" `Quick test_237e_christmastide_feria; + Alcotest.test_case "237(b): an ordinary Class2 Sunday" `Quick test_237b_ordinary_class2_sunday; + Alcotest.test_case "237(b)'s own exception: Septuagesima Sunday" `Quick test_237b_septuagesima_exception; + Alcotest.test_case "237(b)'s own exception: Sexagesima Sunday" `Quick test_237b_sexagesima_exception; + Alcotest.test_case "237(c): a sanctoral feast (Lawrence)" `Quick test_237c_sanctoral_feast; + Alcotest.test_case "Passion Sunday is not a festum for 237(c)" `Quick test_passion_sunday_not_a_festum; + Alcotest.test_case "Palm Sunday is not a festum for 237(c)" `Quick test_palm_sunday_not_a_festum; + Alcotest.test_case "238(d): All Souls' Day (transferred)" `Quick test_238d_all_souls; + Alcotest.test_case "431(c): Holy Thursday" `Quick test_431c_holy_thursday; + Alcotest.test_case "431(c): the Easter Vigil Mass" `Quick test_431c_easter_vigil; + Alcotest.test_case "432(b): Advent I Sunday, violet" `Quick test_432b_violet_sunday; + Alcotest.test_case "432(d): All Souls' Day (transferred)" `Quick test_432d_all_souls; + Alcotest.test_case "431(a): mirrors [te_deum]=true" `Quick test_431a_mirrors_te_deum_true; + Alcotest.test_case "432(a): mirrors [te_deum]=false" `Quick test_432a_mirrors_te_deum_false; + Alcotest.test_case "domain sanity: every violet day has [gloria]=false (sample)" `Quick + test_domain_violet_implies_no_gloria_sample; + Alcotest.test_case "domain sanity: every Requiem day has [gloria]=false (sample)" `Quick + test_domain_requiem_implies_no_gloria_sample; + Alcotest.test_case "domain sweep 1583..9999: violet/Requiem invariants, committed not sampled" + `Slow test_exhaustive_gloria_domain_sweep ] ) -- cgit v1.3