aboutsummaryrefslogtreecommitdiff
path: root/test/test_lms_ordo.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-22 17:56:42 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-22 17:56:42 +0200
commitf4cc032d7e812d716ff6b5df8192f79a2560e8f0 (patch)
tree65c8c20456ebe66fd1ff2253428357b5b027ec0a /test/test_lms_ordo.ml
parent60e87914718dd2c8e69bb89d8cc48d6bef9bfc74 (diff)
downloadcolitur-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.ml112
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