aboutsummaryrefslogtreecommitdiff
path: root/test/test_lms_ordo.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-23 23:01:42 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-23 23:01:42 +0200
commit8faf89f89bd6783a518b6fc0b48c6ee452ba2e94 (patch)
tree6aa160722b0c8cf0cc29df7e7ee5cce6082e4429 /test/test_lms_ordo.ml
parent0806fe65e388a9502035bc9d4f551528ac74b26d (diff)
downloadcolitur-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.ml138
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