diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-22 17:56:42 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-22 17:56:42 +0200 |
| commit | f4cc032d7e812d716ff6b5df8192f79a2560e8f0 (patch) | |
| tree | 65c8c20456ebe66fd1ff2253428357b5b027ec0a /test/test_lms_ordo.ml | |
| parent | 60e87914718dd2c8e69bb89d8cc48d6bef9bfc74 (diff) | |
| download | colitur-f4cc032d7e812d716ff6b5df8192f79a2560e8f0.tar.gz colitur-f4cc032d7e812d716ff6b5df8192f79a2560e8f0.zip | |
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).
Diffstat (limited to 'test/test_lms_ordo.ml')
| -rw-r--r-- | test/test_lms_ordo.ml | 112 |
1 files changed, 107 insertions, 5 deletions
diff --git a/test/test_lms_ordo.ml b/test/test_lms_ordo.ml index 219e51f..42dc31d 100644 --- a/test/test_lms_ordo.ml +++ b/test/test_lms_ordo.ml @@ -173,6 +173,13 @@ type colitur_row = { c_season : V.season; c_formulary : MF.t option; c_creed : bool; + c_gloria : bool; + c_rank : V.rank; + c_slug : string; + c_has_commemoration : bool; + (** whether {!Colitur_kernel.Liturgical_day.t.commemorations} is + non-empty -- needed by the Gloria comparison's own L5 shape + (RG 431(b)/n.302(b)), not by anything Task 6 built. *) } let colitur_rows ~year_lo ~year_hi ~window_first ~window_last = @@ -194,7 +201,10 @@ let colitur_rows ~year_lo ~year_hi ~window_first ~window_last = | Some day -> rows := { c_date = Date.to_iso8601 day.LD.date; c_season = day.LD.temporal.Colitur_kernel.Temporal.season; - c_formulary = day.LD.formulary; c_creed = day.LD.creed } + c_formulary = day.LD.formulary; c_creed = day.LD.creed; c_gloria = day.LD.gloria; + c_rank = day.LD.observed.Colitur_kernel.Celebration.rank; + c_slug = Colitur_kernel.Slug.to_string day.LD.observed.Colitur_kernel.Celebration.slug; + c_has_commemoration = day.LD.commemorations <> [] } :: !rows | None -> Alcotest.failf "no colitur day resolved for %s" (Date.to_iso8601 !d)); d := Date.add_days !d 1 @@ -434,9 +444,19 @@ let check_formulary_overrides ordo colitur = (* not noise to silence. *) (* ---------------------------------------------------------------------- *) +(* No Str/regex (frozen deps) -- the same hand-rolled substring test every + other file in this codebase reaches for (rubrics_ef.ml's own + [contains_substring], precedence_ef.ml's [contains_substring]). Local to + this file, not shared, on the same "no common .mli to hang it from" + footing those other copies already document. *) +let contains_substring s ~needle = + let ls = String.length s and ln = String.length needle in + let rec at i = i + ln <= ls && (String.sub s i ln = needle || at (i + 1)) in + ln = 0 || at 0 + let make_suite ~label ~fixture_path ~fixture_sha256 ~window_first ~window_last ~year_lo ~year_hi ~allow_list_path ~expected_rows ~expected_bvm_votive ~expected_proper ~expected_common ~expected_preceding_sunday - ~expected_ascension_week = + ~expected_ascension_week ~expected_gloria_l5 ~expected_gloria_l6 = let test_fixture_checksum () = Alcotest.(check string) "fixture SHA-256 matches its provenance note" fixture_sha256 (sha256_of_file fixture_path) @@ -540,6 +560,84 @@ let make_suite ~label ~fixture_path ~fixture_sha256 ~window_first ~window_last ~ Alcotest.(check int) (Printf.sprintf "%s: expected_rows matches the actual count" id) e.expected_rows n) explained_counts in + (* ---- Gloria (RG 431-432, {!Rite_ef.Rubrics_ef.gloria}) -- Phase 2 of + this task. Same shape as the Creed comparison immediately above: + coverage (which dates carry no Ordo "Gl"/"No Gl" marker at all) is + checked separately from the value comparison, and both are + date-keyed against their own allow-list ids, never a loose "some + divergence is fine" check. *) + let test_gloria_coverage () = + let ordo = ordo_rows fixture_path in + let no_gloria = List.filter (fun o -> o.gloria = None) ordo in + let expected = window_good_fridays () in + Alcotest.(check (list string)) "only this window's own Good Friday has no Ordo Gloria marker" expected + (List.map (fun o -> o.date) no_gloria) + in + let describe_gloria_mismatch (o : ordo_row) (c : colitur_row) = + Printf.sprintf "%s %S: colitur gloria=%b, Ordo gloria=%b (rank=%s commemoration=%b formulary=%s)" o.date o.title + c.c_gloria (Option.get o.gloria) (V.rank_to_string c.c_rank) c.c_has_commemoration + (match c.c_formulary with + | Some { MF.said = Some s; _ } -> Colitur_kernel.Slug.to_string s + | Some { MF.said = None; via = MF.Votive } -> "votive (said unnamed in the data)" + | Some { MF.said = None; _ } -> "NONE (said, unexpectedly outside Votive)" + | None -> "NONE") + in + (* Two REAL, STRUCTURAL shapes found running this comparison (see + data/ef/expected-divergences-lms.sexp's own L5/L6 for the full + citations) -- neither is a fixed date list, both a property of the + day itself, because both recur every year the underlying condition + holds, not on a fixed calendar date the way L4 above does. Matched + by PREDICATE, not by date, for the same reason L2's own note gives + for why it is prose-only rather than wired through the shared + [expected_rows] mechanism: the count varies window to window (L5: + 6/5/6; L6: 1/0/0), so a single static count cannot check it, and + [make_suite] threads the per-window expected totals directly + (~expected_gloria_l5/~expected_gloria_l6 below) instead. *) + let is_l5_lenten_commemoration (o : ordo_row) (c : colitur_row) = + (* RG 431(b)/n.302(b): "Missa de commemoratione in Officio diei + occurrente" says the Gloria. Every instance found is a privileged + Lenten/Passiontide feria (Class3, violet -- {!TE}'s own + [ferial_rank], RG25) carrying exactly one commemoration of an + impeded Class3 saint, colitur reading [gloria]=false (the ferial + Mass on its own) where the Ordo reads [true]. *) + (not c.c_gloria) && Option.value o.gloria ~default:false && c.c_has_commemoration && c.c_rank = V.Class3 + in + let is_l6_rogation_colour (o : ordo_row) (c : colitur_row) = + (* Root-caused to a DIFFERENT, pre-existing bug this comparison merely + surfaced -- {!Rite_ef.Temporal_ef.temporal}'s own Rogation Monday/ + Tuesday branch hardcodes [Colour.Violet] with no RG citation at + all, but the Ordo shows this exact date "FERIA IV Cl W" (white, + matching Paschaltide's own [season_colour] and RG88's "nihil fit in + Officio" -- the Office, hence its colour, is unchanged by the + Rogation, only the Mass TEXT is proper) -- so [gloria]'s own 432(b) + violet guard wrongly fires. NOT a Gloria defect and NOT fixed here + (out of this task's own scope; see the task report). *) + (not c.c_gloria) && Option.value o.gloria ~default:false + && (contains_substring c.c_slug ~needle:"rogation-monday" || contains_substring c.c_slug ~needle:"rogation-tuesday") + in + let test_gloria_matches_or_is_explained ~expected_gloria_l5 ~expected_gloria_l6 () = + let ordo = ordo_rows fixture_path in + let colitur = colitur_rows ~year_lo ~year_hi ~window_first ~window_last in + let unexplained = ref [] in + let l5_count = ref 0 and l6_count = ref 0 in + List.iter2 + (fun (o : ordo_row) (c : colitur_row) -> + if not (String.equal o.date c.c_date) then Alcotest.failf "misaligned: ordo %s vs colitur %s" o.date c.c_date; + match o.gloria with + | None -> () + | Some ogloria -> + if Bool.equal ogloria c.c_gloria then () + else if is_l5_lenten_commemoration o c then incr l5_count + else if is_l6_rogation_colour o c then incr l6_count + else unexplained := describe_gloria_mismatch o c :: !unexplained) + ordo colitur; + Alcotest.(check (list string)) (Printf.sprintf "[%s] every Gloria mismatch is named in the allow-list -- none unexplained" label) + [] (List.rev !unexplained); + Alcotest.(check int) (Printf.sprintf "[%s] L5 (Lenten privileged feria + commemoration) count" label) + expected_gloria_l5 !l5_count; + Alcotest.(check int) (Printf.sprintf "[%s] L6 (Rogation Monday/Tuesday colour bug) count" label) expected_gloria_l6 + !l6_count + in let test_bvm_seasonal_selection () = let ordo = ordo_rows fixture_path in let colitur = colitur_rows ~year_lo ~year_hi ~window_first ~window_last in @@ -635,6 +733,10 @@ let make_suite ~label ~fixture_path ~fixture_sha256 ~window_first ~window_last ~ Alcotest.test_case "only this window's own Good Friday has no Ordo Creed marker" `Quick test_creed_coverage; Alcotest.test_case "every Creed difference is named in the cited allow-list -- none unexplained" `Quick test_creed_matches_or_is_explained; + Alcotest.test_case "only this window's own Good Friday has no Ordo Gloria marker" `Quick + test_gloria_coverage; + Alcotest.test_case "every Gloria difference is named in the cited allow-list -- none unexplained" `Quick + (test_gloria_matches_or_is_explained ~expected_gloria_l5 ~expected_gloria_l6); Alcotest.test_case "every BVM-Saturday numeral matches its season" `Quick test_bvm_seasonal_selection; Alcotest.test_case "every Ordo BVM numeral day is a colitur Votive day" `Quick test_bvm_numeral_implies_votive; @@ -676,18 +778,18 @@ let suite_2023_2024 = I-class Sunday that admits him not even as a commemoration (RG16(a)), so his own Mass is not said anywhere in this window any more. *) ~expected_bvm_votive:12 ~expected_proper:181 ~expected_common:2 ~expected_preceding_sunday:61 - ~expected_ascension_week:1 + ~expected_ascension_week:1 ~expected_gloria_l5:5 ~expected_gloria_l6:1 let suite_2024_2025 = make_suite ~label:"lms-ordo-2024-2025" ~fixture_path:"fixtures/lms-ordo-2024-2025.sexp" ~fixture_sha256:"da817b75c5bf40ed3be1d5f6890b199705e02ce4d42111253ab8547bccabc3f7" ~window_first:"2024-11-27" ~window_last:"2025-12-31" ~year_lo:2023 ~year_hi:2026 ~allow_list_path ~expected_rows:400 ~expected_bvm_votive:14 ~expected_proper:179 ~expected_common:2 ~expected_preceding_sunday:66 - ~expected_ascension_week:3 + ~expected_ascension_week:3 ~expected_gloria_l5:5 ~expected_gloria_l6:0 let suite_2025_2026 = make_suite ~label:"lms-ordo-2025-2026" ~fixture_path:"fixtures/lms-ordo-2025-2026.sexp" ~fixture_sha256:"8839a61e0c7d1e6c8326114f4f45a5c183551287a88f4ed01127154add6ae5a5" ~window_first:"2025-11-28" ~window_last:"2026-12-31" ~year_lo:2024 ~year_hi:2027 ~allow_list_path ~expected_rows:399 ~expected_bvm_votive:13 ~expected_proper:174 ~expected_common:2 ~expected_preceding_sunday:68 - ~expected_ascension_week:2 + ~expected_ascension_week:2 ~expected_gloria_l5:6 ~expected_gloria_l6:0 |
