diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-22 13:38:53 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-22 13:38:53 +0200 |
| commit | ff7fa965d68631b5caac771d6db3a0f2110cb5dd (patch) | |
| tree | 5680164012528932aa5187cae77cc6d8fea8cc30 /test/test_lms_ordo.ml | |
| parent | 41263d60432fadd2aef3f04de66b60561f89cbec (diff) | |
| download | colitur-ff7fa965d68631b5caac771d6db3a0f2110cb5dd.tar.gz colitur-ff7fa965d68631b5caac771d6db3a0f2110cb5dd.zip | |
test(ef): compare the LMS Ordo's formulary override STRING, not just presence
test_lms_ordo.ml's formulary-override check only asked WHETHER a
Preceding_sunday day showed a "Mass of ..." override line, never WHICH
Sunday it named. colitur could have resumed the wrong Sunday on all
66 such days and this layer would have stayed green -- resuming the
right Sunday is precisely what step 3 of the reading chain exists to
do.
test_formulary_override_matches now derives the Ordo's own expected
prose from Mass_formulary.t.said's own resolved slug (parsed directly,
no Str/regex) and compares it against the Ordo's real text. Two
narrow, evidenced exceptions, both counted rather than silently
absorbed:
- Christ the King's own Sunday reaches this population under a
NAMED slug (ef-christ-the-king) that carries no embedded ordinal;
its position among the Sundays after Pentecost varies by year, so
Temporal_ef.week is read for that one slug specifically, not
called generically (an earlier version of this fix read [week]
for every day and was wrong -- caught by its own assertion on the
very first run, because Time_after_pentecost's own last-Sunday and
surplus-Sunday renumbering diverge from the raw week count. Reading
the number back out of the already-resolved slug sidesteps both
special cases and is not tautological with the wrong-Sunday
failure mode this fix exists to catch).
- Three real days between Ascension and the following Sunday
(2025-05-30, 2025-06-02, 2025-06-03) are a genuine, newly-found
defect: colitur resumes the nearest calendar Sunday, but the
Missal, and the Ordo, keep saying Ascension's own Mass through
this stretch instead. Structural (Ascension is a fixed Easter+39),
not a fixture artefact, and not fixed here -- it needs a
lectionary-data change out of this fix round's own scope.
Documented in expected-divergences-lms.sexp as a prose-only note
(no active sexp record: this file's id-lookup machinery is scoped
to the Creed axis, and a second axis reusing it would let one
test's "declared but never fired" check spuriously trip on the
other's entry) and enforced directly by this test's own count
assertion instead.
Mutation-proved: offsetting step 3's Sunday lookup by an extra 7 days
reddens this test (23 failures total across the suite: layers 3-6,
golden pins, and lectionary_ef's own unit tests), confirmed via the
compiled test binary directly; reverted clean, full suite green again.
Diffstat (limited to 'test/test_lms_ordo.ml')
| -rw-r--r-- | test/test_lms_ordo.ml | 220 |
1 files changed, 207 insertions, 13 deletions
diff --git a/test/test_lms_ordo.ml b/test/test_lms_ordo.ml index c5cae1a..cff4b3d 100644 --- a/test/test_lms_ordo.ml +++ b/test/test_lms_ordo.ml @@ -95,6 +95,7 @@ module LD = Colitur_kernel.Liturgical_day module Date = Colitur_kernel.Date module MF = Colitur_kernel.Mass_formulary module V = Rite_ef.Vocab_ef +module TE = Rite_ef.Temporal_ef let sanctoral_path = "../data/ef/sanctoral.sexp" let adjustments_path = "../data/ef/adjustments.sexp" @@ -314,7 +315,16 @@ let test_creed_matches_or_is_explained () = let colitur = colitur_rows () in let allow_list = load_allow_list () in let by_id = List.map (fun e -> (e.id, e)) allow_list in - let l1_dates = [ "2025-11-03" ] in + (* No known Creed divergence is adjudicated any more: L1 (RG 476(f), All + Souls' Day) was CLOSED by fixing {!Rite_ef.Rubrics_ef.creed} itself + (whole-branch review finding 1), not by re-adjudicating it -- see + expected-divergences-lms.sexp's own closure note for the citation and + the measured blast radius. [allow_list] is therefore expected to be + empty today; the [by_id]/[explained_counts] wiring below is kept + rather than deleted, because the "every declared entry must have + fired" check at the end is a real invariant worth keeping live even + while the list itself is empty -- exactly the shape a FUTURE entry of + this kind would need. *) let unexplained = ref [] in let explained_counts = Hashtbl.create 8 in List.iter2 @@ -323,10 +333,7 @@ let test_creed_matches_or_is_explained () = match o.creed with | None -> () (* Good Friday, see test_creed_coverage *) | Some ocreed -> - if Bool.equal ocreed c.c_creed then () - else if List.mem o.date l1_dates then - Hashtbl.replace explained_counts "L1" (1 + try Hashtbl.find explained_counts "L1" with Not_found -> 0) - else unexplained := describe_creed_mismatch o c :: !unexplained) + if Bool.equal ocreed c.c_creed then () else unexplained := describe_creed_mismatch o c :: !unexplained) ordo colitur; Alcotest.(check (list string)) "every Creed mismatch is named in the allow-list -- none unexplained" [] (List.rev !unexplained); @@ -403,15 +410,168 @@ let test_bvm_numeral_implies_votive () = (* Formulary override, restricted to Proper/Preceding_sunday/Common -- *) (* see this file's own header for why Own_slug is excluded, and Votive is *) (* covered by the BVM checks above instead. *) +(* *) +(* Whole-branch review finding 2: the ORIGINAL version of this test only *) +(* checked WHETHER a Preceding_sunday day showed an override line, never *) +(* WHICH Sunday it named -- so colitur could resume the WRONG Sunday on *) +(* every one of the 66 such days and this layer would stay green (step 3 *) +(* of the reading chain, {!Rite_ef.Lectionary_ef.readings}, is exactly the *) +(* code this was supposed to be testing). Fixed by deriving the EXPECTED *) +(* prose from {!MF.t.said}'s own resolved SLUG -- not by calling colitur's *) +(* own [sunday_slug] a second time (which would make the check tautological, *) +(* the same reasoning this file's own header already gives for *) +(* re-deriving the BVM numeral rather than calling {!Rite_ef. *) +(* Lectionary_ef.bvm_saturday_citations}). The FIRST version of this fix *) +(* instead recomputed the preceding Sunday's DATE and re-derived (season, *) +(* week) via {!Rite_ef.Temporal_ef.temporal} independently -- and was WRONG, caught by its *) +(* own assertion on the very first run: {!Rite_ef.Temporal_ef.temporal}'s raw [week] field *) +(* and the SLUG's own embedded number genuinely diverge for the last *) +(* Sunday before Advent (always slug-numbered "24", whatever the raw week *) +(* count that year -- temporal_ef.ml's own [sunday_slug], the *) +(* [same d last_sunday] branch) and for the "surplus Sundays" that resume *) +(* Epiphany's own numbering when Septuagesima cut it short (same function, *) +(* the [n > 23] branch) -- exactly the two hard cases that function's own *) +(* comment names. Parsing the number back OUT of the already-resolved slug *) +(* sidesteps both: it is not tautological with the wrong-Sunday failure *) +(* mode this whole fix exists to catch (a wrong slug still parses to a *) +(* wrong number, which still fails to match the Ordo's real text), and it *) +(* is simpler and more robust than re-deriving the two special cases a *) +(* second time by hand. *) (* ---------------------------------------------------------------------- *) -let test_formulary_override_by_source () = +(* English ordinal suffix: 1st, 2nd, 3rd, 4th, 11th, 12th, 13th, 21st... -- + the "teens" exception applies to every multiple of 100 plus 11-13, not + just 11-13 themselves, but nothing in this fixture's own domain (weeks + 1-24) ever reaches three digits, so the simpler [n mod 100] test below + is exercised fully by 11-13 alone and is not a latent bug for values + this function is actually ever called with. *) +let ordinal n = + let suffix = + if n mod 100 >= 11 && n mod 100 <= 13 then "th" + else + match n mod 10 with + | 1 -> "st" + | 2 -> "nd" + | 3 -> "rd" + | _ -> "th" + in + Printf.sprintf "%d%s" n suffix + +(* [MF.t.said]'s own slug shape for every Preceding_sunday day observed in + the real fixture is [ef-<season-word>-sunday-<n>] ({!Rite_ef.Temporal_ef.sunday_slug}'s + own generic branch -- the ONLY branch that ever reaches a weekday's + [Preceding_sunday] fallback; the specially-NAMED Sundays [ef-low-sunday], + [ef-palm-sunday] etc. are caught by {!Rite_ef.Temporal_ef.named} first and never fall + through to it). Manual parsing, not [Str]/regex (deps frozen): finds the + last "-sunday-" marker and splits on it, since a season word can itself + contain hyphens ("time-after-pentecost") but never contains "-sunday-". *) +let parse_sunday_slug slug = + let prefix = "ef-" in + let plen = String.length prefix in + if String.length slug <= plen || String.sub slug 0 plen <> prefix then None + else + let body = String.sub slug plen (String.length slug - plen) in + let marker = "-sunday-" in + let mlen = String.length marker and blen = String.length body in + let rec find_last i best = if i + mlen > blen then best + else find_last (i + 1) (if String.sub body i mlen = marker then Some i else best) + in + match find_last 0 None with + | None -> None + | Some i -> ( + let word = String.sub body 0 i in + let num_str = String.sub body (i + mlen) (blen - i - mlen) in + match int_of_string_opt num_str with Some n -> Some (word, n) | None -> None) + +(* The Ordo's own prose, derived from [MF.t.said]'s own (season-word, + number) -- verified against every one of the 66 real Preceding_sunday + days in the fixture (test_formulary_override_matches below), not + invented from the naming convention alone. [None] means "this pair is + EXCLUDED from the mapping", handled explicitly by the caller, never + silently -- see [ascension_week_override] just below for the one real + population that lands there. *) +let expected_preceding_sunday_override (word : string) (n : int) = + match (word, n) with + | "advent", 1 -> Some "Advent Sunday" + | "advent", 2 -> Some "2nd Sunday of Advent" + | "advent", 3 -> Some "3rd Sunday of Advent (Gaudete Sunday)" + | "advent", 4 -> Some "4th Sunday of Advent" + | "septuagesima", 1 -> Some "Septuagesima Sunday" + | "septuagesima", 2 -> Some "Sexagesima Sunday" + | "septuagesima", 3 -> Some "Quinquagesima Sunday" + | "time-after-epiphany", n -> Some (Printf.sprintf "%s Sunday after the Epiphany" (ordinal n)) + | "time-after-pentecost", 24 -> Some "24th & Last Sunday After Pentecost" + | "time-after-pentecost", n -> Some (Printf.sprintf "%s Sunday after Pentecost" (ordinal n)) + (* [ef-easter-sunday-<n>]: Paschaltide's own numbering counts Easter + Sunday itself as 1 (Low Sunday, named separately via {!Rite_ef.Temporal_ef.named} and + so never reaching this generic branch at all, is 2), so the ordinal + PRINTED is [n - 1] -- confirmed against every one of n=3..5 in the + fixture ("2nd" through "4th" Sunday after Easter). n=6,7 are + deliberately NOT given here: see [ascension_week_override]. *) + | "easter", n when n >= 3 && n <= 5 -> Some (Printf.sprintf "%s Sunday after Easter" (ordinal (n - 1))) + | _ -> None + +(* One NAMED Sunday (not a generic "ef-<season>-sunday-<n>" slug at all) + reaches [Preceding_sunday] in the real 66-day population: Christ the + King ({!Rite_ef.Temporal_ef.christ_the_king}, "the last Sunday of + October"), whose own ferias resume it the same way any other Sunday's + would. Unlike the Septuagesima trio's fixed proper names, its ORDINAL + position among the Sundays after Pentecost is not fixed -- it varies + year to year with Easter's own date -- so there is no string this + function could hard-code the way [expected_preceding_sunday_override] + does for the others. [word]/[n] cannot express it EITHER, because + {!Rite_ef.Temporal_ef.named} overrides the SLUG for that Sunday but, + confirmed directly in temporal_ef.ml ([build ~season ~slug ~colour + ~rank ~week:(week d) ()], called identically whether or not [named] + matched), never the raw [week] FIELD -- so re-deriving that one field + for this one named exception is not the same mistake the first version + of this fix made re-deriving [week] WHOLESALE (that failed on the + season's own two special-numbering cases, both of which this function + sidesteps by reading the slug directly); here there is no slug number + to read in the first place, so [week] is the only source of truth, + narrowly applied to the single slug that needs it. *) +let expected_named_sunday_override slug ~year = + if String.equal slug "ef-christ-the-king" then + match TE.week (TE.christ_the_king year) with + | Some n -> Some (Printf.sprintf "%s Sunday after Pentecost" (ordinal n)) + | None -> None + else None + +(* [n] = 6 or 7 (Paschaltide's own numbering, see the comment above -- the + calendar Sundays traditionally called the 5th and 6th Sunday after + Easter) are STRUCTURALLY, not coincidentally, always Ascension-adjacent: + the Ascension is a fixed Easter+39 (a Thursday inside week 6), so EVERY + feria whose own [Preceding_sunday] fallback would otherwise resolve to + one of these two weeks falls between Ascension Thursday and the + following Saturday, every year, not merely in this fixture's own + 2024-2025 window. A REAL finding, not a fixture artefact: the Ordo + shows "Mass of the Ascension" on all three such days this window + contains (2025-05-30, 2025-06-02, 2025-06-03 -- Friday and the + following Monday/Tuesday, the days between Ascension and the Sunday + after it that have no proper of their own), never "5th"/"6th Sunday + after Easter" -- the Missal keeps saying ASCENSION's own Mass through + this stretch, not the last numbered Sunday's. colitur's step 3 has no + notion of "inside Ascension's own after-feast period" -- it always + resumes the nearest PRECEDING SUNDAY, numbered or not -- so on these + three real days {!MF.t.said} is a plain "ef-easter-sunday-6"/"-7" slug + the Missal would never actually print as such. A genuine content gap in + colitur's reading chain, found by this fix, NOT fixed here (fixing it + means teaching step 3 or the lectionary data about Ascension's own + after-feast period, a lectionary-data change out of this fix round's + own scope) -- recorded honestly, the same way L1 in + expected-divergences-lms.sexp already records the 476(f) gap this same + branch closed, and counted below so a change to this population would + be caught rather than silently absorbed. *) +let ascension_week_override = "Mass of the Ascension" + +let test_formulary_override_matches () = let ordo = ordo_rows () in let colitur = colitur_rows () in let pairs = List.combine ordo colitur in let counts = Hashtbl.create 8 in let bump k = Hashtbl.replace counts k (1 + try Hashtbl.find counts k with Not_found -> 0) in let bad = ref [] in + let ascension_week_count = ref 0 in List.iter (fun (o, c) -> match c.c_formulary with @@ -425,17 +585,51 @@ let test_formulary_override_by_source () = if o.formulary_override <> None then bad := Printf.sprintf "%s: Common day but Ordo shows override %s" o.date (Option.get o.formulary_override) :: !bad - | Some { MF.via = MF.Preceding_sunday; _ } -> + | Some { MF.via = MF.Preceding_sunday; said } -> ( bump "preceding_sunday"; - if o.formulary_override = None then bad := Printf.sprintf "%s: Preceding_sunday day but Ordo shows no override" o.date :: !bad + let slug = Colitur_kernel.Slug.to_string said in + let ordo_says = Printf.sprintf "Mass of %s" in + (* Try the generic [ef-<season>-sunday-<n>] shape first; fall back + to the one NAMED Sunday that also reaches this population + ([expected_named_sunday_override], see its own citation) when + the slug doesn't parse that way at all. *) + let year = int_of_string (String.sub o.date 0 4) in + let expected = + match parse_sunday_slug slug with + | Some (word, n) -> expected_preceding_sunday_override word n + | None -> expected_named_sunday_override slug ~year + in + match expected with + | Some expected -> ( + let expected_line = ordo_says expected in + match o.formulary_override with + | Some got when String.equal got expected_line -> () + | Some got -> bad := Printf.sprintf "%s: colitur resumed %S, Ordo says %S" o.date expected_line got :: !bad + | None -> bad := Printf.sprintf "%s: colitur resumed %S, Ordo shows no override" o.date expected_line :: !bad) + | None -> ( + (* The Ascension-week exclusion above -- checked, not + assumed: fails loudly if the Ordo's own text ever stops + matching the one string this population is adjudicated to + carry, or if a slug outside every adjudicated case above + ever reaches here. *) + match o.formulary_override with + | Some got when String.equal got ascension_week_override -> incr ascension_week_count + | Some got -> + bad := + Printf.sprintf "%s: unmapped (colitur resumed %S) and Ordo shows %S, not the adjudicated %S" o.date + slug got ascension_week_override + :: !bad + | None -> + bad := Printf.sprintf "%s: unmapped (colitur resumed %S) and Ordo shows no override" o.date slug :: !bad)) | Some { MF.via = MF.Votive; _ } | Some { MF.via = MF.Own_slug; _ } | None -> ()) pairs; - Alcotest.(check (list string)) "no unexplained formulary-override mismatch on Proper/Common/Preceding_sunday days" - [] (List.rev !bad); + Alcotest.(check (list string)) "every formulary-override mismatch is explained -- none unexplained" [] (List.rev !bad); Alcotest.(check int) "179 Proper days in the window" 179 (try Hashtbl.find counts "proper" with Not_found -> 0); Alcotest.(check int) "2 Common days in the window" 2 (try Hashtbl.find counts "common" with Not_found -> 0); Alcotest.(check int) "66 Preceding_sunday days in the window" 66 - (try Hashtbl.find counts "preceding_sunday" with Not_found -> 0) + (try Hashtbl.find counts "preceding_sunday" with Not_found -> 0); + Alcotest.(check int) "3 Preceding_sunday days fall in the Ascension-week exclusion, no more, no fewer" 3 + !ascension_week_count let test_via_distribution_totals_400 () = let colitur = colitur_rows () in @@ -461,7 +655,7 @@ let suite = 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; - Alcotest.test_case "formulary override matches on Proper/Common/Preceding_sunday days" `Quick - test_formulary_override_by_source; + Alcotest.test_case "formulary override STRING matches on Proper/Common/Preceding_sunday days" `Quick + test_formulary_override_matches; Alcotest.test_case "the via distribution sums to the full window" `Quick test_via_distribution_totals_400 ] ) |
