diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-23 23:01:42 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-23 23:01:42 +0200 |
| commit | 8faf89f89bd6783a518b6fc0b48c6ee452ba2e94 (patch) | |
| tree | 6aa160722b0c8cf0cc29df7e7ee5cce6082e4429 /test/test_lms_ordo.ml | |
| parent | 0806fe65e388a9502035bc9d4f551528ac74b26d (diff) | |
| download | colitur-8faf89f89bd6783a518b6fc0b48c6ee452ba2e94.tar.gz colitur-8faf89f89bd6783a518b6fc0b48c6ee452ba2e94.zip | |
feat(ordo): wire the LMS preface column into the witness suite
Extends tools/extract_lms_ordo.ml's row type with a praef field (the
first "Pr of ..." / "Common Pr" match in each day's universal block,
same first-in-reading-order discipline already used for Gl/Cr) and
regenerates all three LMS fixtures. Also brings lms-ordo-2024-2025.sexp
under the generalised, probe-verified provenance template the Witnesses
task previously added only for the two newer editions -- it had kept
Task 6's original hand-written header.
Characterisation finding: this Ordo prints OPTION LISTS ("Pr of X or Pr
of Y or Common Pr"), not a single value per day, and several named
options (Martyrs, All Saints and Patron Saints, the Dedication of a
Church, the Most Holy Sacrament, St John the Baptist, the Angels) are
genuinely absent from RG 484-497's own fourteen named prefaces. Every
instance found in the universal block pairs such an extra with a
genuine RG-482 answer, so test/test_lms_ordo.ml compares by membership
(classify_praef_options, a 15-entry prefix table split on the
standalone token "or"), not string equality.
Opposite-prediction probes (Nativity/Apostles/Requiem) are hard-
verified inside the extractor itself before any fixture is trusted, the
same discipline already used for the Creed column.
Result: 396/396, 399/399, 398/398 comparable rows agree across the
three editions (Good Friday excluded per edition, no Mass that day),
zero allow-list entries needed.
extraordinaryform.org's own three editions were characterised but
deliberately not wired in this pass -- see
.superpowers/sdd/2026-08-21-colitur-celebrant-rubrics-phase1/preface-witnesses-report.md
for the full account (the field is genuinely per-day, contrary to a
naive header-count read, but carries its own classification anomaly
during Advent and reconfirms the corpus's known St Lawrence vigil
defect, and building a full extractor for it is a separate, comparably
large task not needed to decide the RG 497 question this same report
settles).
Claude-Session: https://claude.ai/code/session_017ZBxCCRM2ojnBupp3SBxV9
Diffstat (limited to 'test/test_lms_ordo.ml')
| -rw-r--r-- | test/test_lms_ordo.ml | 138 |
1 files changed, 134 insertions, 4 deletions
diff --git a/test/test_lms_ordo.ml b/test/test_lms_ordo.ml index 32ecfbc..0ac314e 100644 --- a/test/test_lms_ordo.ml +++ b/test/test_lms_ordo.ml @@ -80,6 +80,7 @@ module Computus = Colitur_kernel.Computus module MF = Colitur_kernel.Mass_formulary module V = Rite_ef.Vocab_ef module TE = Rite_ef.Temporal_ef +module Preface = Colitur_kernel.Preface let sanctoral_path = "../data/ef/sanctoral.sexp" let adjustments_path = "../data/ef/adjustments.sexp" @@ -148,6 +149,7 @@ type ordo_row = { bvm_numeral : string option; gloria : bool option; creed : bool option; + praef : string option; has_diocesan_variant : bool; } [@@deriving sexp] @@ -174,6 +176,7 @@ type colitur_row = { c_formulary : MF.t option; c_creed : bool; c_gloria : bool; + c_preface : Preface.t option; c_rank : V.rank; c_slug : string; c_has_commemoration : bool; @@ -202,6 +205,7 @@ let colitur_rows ~year_lo ~year_hi ~window_first ~window_last = 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_gloria = day.LD.gloria; + c_preface = day.LD.preface; 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 <> [] } @@ -493,6 +497,85 @@ let check_formulary_overrides ordo colitur = fc_ascension_week = !ascension_week } (* ---------------------------------------------------------------------- *) +(* Preface (RG 482-499, {!Rite_ef.Rubrics_ef.preface}) -- Preface-witnesses *) +(* task (2026-08-23), extending tools/extract_lms_ordo.ml's new [praef] *) +(* column into a real comparison, the same shape as test_fiuv_ordo.ml's *) +(* own preface axis but with a genuinely DIFFERENT classification rule -- *) +(* see the fixture's own provenance header, finding 5, for the full *) +(* CHARACTERISATION FIRST account this discipline requires. *) +(* *) +(* THE KEY DIFFERENCE FROM FIUV: this Ordo prints OPTION LISTS ("Pr of X *) +(* or Pr of Y or Common Pr"), not a single value per day, and several of *) +(* the named options are NOT among the fourteen RG 484-497 enumerate *) +(* ("Martyrs", "All Saints and Patron Saints", "the Dedication of a *) +(* Church", "the Most Holy Sacrament", "St John the Baptist", "the *) +(* Angels" -- checked directly against docs/research/LT.txt lines *) +(* 3936-4020: no such clause exists there). Every raw value in this *) +(* corpus's own universal block that carries one of these extras also *) +(* carries a genuine RG-482 alternative alongside it (never as the row's *) +(* own and only option) -- so [classify_praef_options] parses the FULL *) +(* option set (splitting on the standalone token "or", never a substring *) +(* search: checked directly that no trailing descriptive prose in this *) +(* corpus's universal block ever uses "or" as an ordinary English *) +(* conjunction), drops any segment that does not name one of *) +(* {!Preface.t}'s fifteen values, and the comparison below checks colitur's *) +(* single computed answer is a MEMBER of what survives, not string *) +(* equality against whichever option prints first. *) +let has_prefix ~prefix s = + String.length s >= String.length prefix && String.sub s 0 (String.length prefix) = prefix + +(* Every prefix here was read directly off the three fixtures' own 47 + distinct universal-block [praef] values (measured, not guessed) -- + mirrors test_fiuv_ordo.ml's own [praef_prefixes] table in spirit, but + this publisher's own English vocabulary, not the FIUV Ordo's Latin. + None of these prefixes is a prefix of another (checked): "Pr of St + Joseph" and "Pr of St John the Baptist" are the one pair that shares an + opening ("Pr of St "), disambiguated by their very next word. *) +let praef_prefixes = + [ ("Common Pr", Preface.Common); + ("Pr of the Nativity", Preface.Nativity); + ("Pr of the Epiphany", Preface.Epiphany); + ("Pr of Lent", Preface.Lent); + ("Pr of the Holy Cross", Preface.Holy_cross); + ("Pr of Easter", Preface.Easter); + ("Pr of the Ascension", Preface.Ascension); + ("Pr of the Sacred Heart", Preface.Sacred_heart); + ("Pr of Our Lord Jesus Christ the King", Preface.Christ_the_king); + ("Pr of the Holy Spirit", Preface.Holy_spirit); + ("Pr of the Trinity", Preface.Trinity); + ("Pr of the BVM", Preface.Bvm); + ("Pr of St Joseph", Preface.St_joseph); + ("Pr of the Apostles", Preface.Apostles); + ("Pr of the Dead", Preface.Requiem) + (* Deliberately UNMAPPED, found live in this corpus's own universal + block (not merely in the excluded diocesan variants): "Pr of + Martyrs", "Pr of All Saints and Patron Saints", "Pr of the + Dedication of a Church", "Pr of the Most Holy Sacrament", "Pr of + St John the Baptist", "Pr of the Angels" -- see this section's own + header. A segment matching none of the fifteen prefixes above is + simply dropped from the parsed option set, never forced into a + guess. *) + ] + +let classify_praef_segment (s : string) : Preface.t option = + let rec go = function [] -> None | (prefix, p) :: rest -> if has_prefix ~prefix s then Some p else go rest in + go praef_prefixes + +(* Splits on the standalone token "or" (never a substring search -- see + this section's own header on why that is safe in this corpus), then + classifies each segment independently and keeps only the recognised + ones. *) +let classify_praef_options (raw : string) : Preface.t list = + let toks = String.split_on_char ' ' raw |> List.filter (fun t -> t <> "") in + let rec split_on_or acc cur = function + | [] -> List.rev (List.rev cur :: acc) + | "or" :: rest -> split_on_or (List.rev cur :: acc) [] rest + | t :: rest -> split_on_or acc (t :: cur) rest + in + let segments = split_on_or [] [] toks in + List.filter_map (fun seg -> classify_praef_segment (String.concat " " seg)) segments + +(* ---------------------------------------------------------------------- *) (* [make_suite]: one edition's worth of tests, parameterised. Every count *) (* below is a genuine per-edition PIN (measured once against real data, *) (* not guessed), the same discipline every other golden/oracle fixture in *) @@ -793,6 +876,49 @@ let make_suite ~label ~fixture_path ~fixture_sha256 ~window_first ~window_last ~ expected_ascension_week) expected_ascension_week r.fc_ascension_week in + let test_praef_coverage () = + let ordo = ordo_rows fixture_path in + let no_praef = List.filter (fun o -> o.praef = None) ordo in + let expected = window_good_fridays () in + Alcotest.(check (list string)) "only this window's own Good Friday has no Ordo Pr-of/Common-Pr marker" expected + (List.map (fun o -> o.date) no_praef) + in + let describe_praef_mismatch (o : ordo_row) (c : colitur_row) (options : Preface.t list) = + Printf.sprintf "%s %S: colitur preface=%s, Ordo praef=%S (parsed options: %s)" o.date o.title + (match c.c_preface with Some p -> Preface.to_string p | None -> "NONE") + (Option.get o.praef) + (String.concat ", " (List.map Preface.to_string options)) + in + let test_praef_matches_or_is_explained () = + 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 unclassifiable = ref [] 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.praef with + | None -> () + | Some raw -> ( + let options = classify_praef_options raw in + match options with + | [] -> unclassifiable := o.date :: !unclassifiable + | opts -> ( + match c.c_preface with + | None -> + unexplained := + Printf.sprintf "%s %S: colitur preface=NONE (unexpected outside Good Friday), Ordo praef=%S" + o.date o.title raw + :: !unexplained + | Some p -> if List.mem p opts then () else unexplained := describe_praef_mismatch o c opts :: !unexplained))) + ordo colitur; + Alcotest.(check (list string)) + (Printf.sprintf "[%s] every Pr-of/Common-Pr mismatch is named in the allow-list -- none unexplained" label) + [] (List.rev !unexplained); + Alcotest.(check (list string)) + (Printf.sprintf "[%s] every option list has at least one recognised alternative -- none dropped entirely" label) + [] (List.rev !unclassifiable) + in let test_via_distribution_totals () = let colitur = colitur_rows ~year_lo ~year_hi ~window_first ~window_last in let via_name c = match c.c_formulary with None -> "none" | Some f -> MF.source_to_string f.MF.via in @@ -818,7 +944,11 @@ let make_suite ~label ~fixture_path ~fixture_sha256 ~window_first ~window_last ~ test_bvm_numeral_implies_votive; 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 + Alcotest.test_case "the via distribution sums to the full window" `Quick test_via_distribution_totals; + Alcotest.test_case "only this window's own Good Friday has no Ordo Pr-of/Common-Pr marker" `Quick + test_praef_coverage; + Alcotest.test_case "every Pr-of/Common-Pr mismatch is named in the allow-list -- none unexplained" `Quick + test_praef_matches_or_is_explained ] ) let allow_list_path = "../data/ef/expected-divergences-lms.sexp" @@ -843,7 +973,7 @@ let suite_allow_list = ("lms-ordo-allow-list", [ Alcotest.test_case "declares ex let suite_2023_2024 = make_suite ~label:"lms-ordo-2023-2024" ~fixture_path:"fixtures/lms-ordo-2023-2024.sexp" - ~fixture_sha256:"c8d9d4b790f6b4438e932f70dad8ebe9a0316662f1eb02c26248e04c7be1a62b" ~window_first:"2023-12-01" + ~fixture_sha256:"631aedad5a749c814efcffbf1dfd0970181552a7b8985f6f74c5cfa4bf83baf5" ~window_first:"2023-12-01" ~window_last:"2024-12-31" ~year_lo:2022 ~year_hi:2025 ~allow_list_path ~expected_rows:397 (* expected_proper 182 -> 181 (bissextile-shift task, 2026-08-22): 24 February 2024, previously Matthias's own Proper Mass, is now the @@ -869,7 +999,7 @@ let suite_2023_2024 = 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" + ~fixture_sha256:"a2b549f10dfbebfa5f7df7dea74b0e5352f5abbf5e2ce33ef681a9eb8eb116e6" ~window_first:"2024-11-27" ~window_last:"2025-12-31" ~year_lo:2023 ~year_hi:2026 ~allow_list_path ~expected_rows:400 (* expected_preceding_sunday 66 -> 63 (celebrant-rubrics-phase1, Bug 2 fix): this window's own three Ascension-week days move from @@ -880,7 +1010,7 @@ let suite_2024_2025 = 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" + ~fixture_sha256:"8595ffebfc50b7a165643c172f9ba542871dd1d1e45f8c85a21b104da689535f" ~window_first:"2025-11-28" ~window_last:"2026-12-31" ~year_lo:2024 ~year_hi:2027 ~allow_list_path ~expected_rows:399 (* expected_preceding_sunday 68 -> 66 (celebrant-rubrics-phase1, Bug 2 fix): this window's own two Ascension-week days move from |
