aboutsummaryrefslogtreecommitdiff
path: root/test/test_fiuv_ordo.ml
diff options
context:
space:
mode:
Diffstat (limited to 'test/test_fiuv_ordo.ml')
-rw-r--r--test/test_fiuv_ordo.ml595
1 files changed, 595 insertions, 0 deletions
diff --git a/test/test_fiuv_ordo.ml b/test/test_fiuv_ordo.ml
new file mode 100644
index 0000000..3bfed68
--- /dev/null
+++ b/test/test_fiuv_ordo.ml
@@ -0,0 +1,595 @@
+(* Witnesses task (2026-08-22-colitur-celebrant-rubrics-phase1): the
+ SEVENTH validation layer, and the first UNIVERSAL, SECOND-COMPILER
+ witness -- the FIUV (Foederatio Internationalis Una Voce) Ordo for
+ 2025-2026, compiled by Joseph Shaw, independent of the Latin Mass
+ Society's own three editions (test_lms_ordo.ml) except for sharing a
+ publisher/host (see test/fixtures/fiuv-ordo-2025-2026.sexp's own
+ provenance header, and docs/research/ordo/PROVENANCE-ordo-corpus.md,
+ for the full account of what independence this source does and does
+ not buy).
+
+ WHY THIS LAYER IS MORE VALUABLE THAN THE LMS ONE, per the task brief's
+ own four reasons: (1) UNIVERSAL -- no diocesan variants to exclude at
+ all, so this fixture's own [row] carries no [has_diocesan_variant]
+ field, unlike the LMS one; (2) IN LATIN, the rubrics' own vocabulary;
+ (3) BOTH DIRECTIONS EXPLICIT ("Gloria"/"sine Gloria", "Credo"/"sine
+ Credo") -- used here exactly as with the LMS source; (4) RECORDS THE
+ TE DEUM, the hinge of RG 431(a)'s own Mass-Gloria rule (deferred to
+ Phase 2, Breviary nn. 237-238) -- captured into the fixture, NOT
+ compared here (no colitur-side Te Deum predicate exists yet).
+
+ SCOPE: the CREED (RG 475-476, {!Rite_ef.Rubrics_ef.creed}) is the ONE
+ axis compared against colitur, per the task brief. [praef] and
+ [te_deum] are captured into the fixture (a genuinely useful data set
+ for Phase 2/3's still-unbuilt Gloria and preface rules) but
+ deliberately NOT validated here -- there is nothing on colitur's own
+ side yet to compare either against. [class_] (raw, including the FIUV
+ ordo's own explicit "III cl." vs "III cl. (Priv.)" Cum Sanctissima
+ convention, stated on its own title page) is likewise captured but not
+ compared: colitur's core is strictly the 1962 Missal with Cum
+ Sanctissima/Quo Magis modelled only as an OVERLAY, never core
+ (CLAUDE.md's own binding decision #2), so colitur's UNOVERLAID output
+ has no Cum-Sanctissima-aware notion of "III cl." vs "(Priv.)" to
+ compare this field against in the first place -- comparing it would be
+ comparing the Ordo against a question colitur's default configuration
+ was never asked. *)
+
+module Cal = Colitur_kernel.Calendar
+module Layer = Colitur_kernel.Layer
+module Overlay = Colitur_kernel.Overlay
+module LD = Colitur_kernel.Liturgical_day
+module Date = Colitur_kernel.Date
+module V = Rite_ef.Vocab_ef
+module Preface = Colitur_kernel.Preface
+
+let sanctoral_path = "../data/ef/sanctoral.sexp"
+let adjustments_path = "../data/ef/adjustments.sexp"
+let fixture_path = "fixtures/fiuv-ordo-2025-2026.sexp"
+let allow_list_path = "../data/ef/expected-divergences-fiuv.sexp"
+
+(* Duplicated, not shared -- test_lms_ordo.ml's own identical function
+ carries the same "no shared .mli" reasoning. *)
+let sha256_of_file path =
+ let tmp = Filename.temp_file "colitur_fiuv_ordo_sha256" ".txt" in
+ Fun.protect
+ ~finally:(fun () -> try Sys.remove tmp with Sys_error _ -> ())
+ (fun () ->
+ let cmd = Printf.sprintf "sha256sum %s > %s" (Filename.quote path) (Filename.quote tmp) in
+ let rc = Sys.command cmd in
+ if rc <> 0 then Alcotest.failf "sha256sum exited %d for %s (is it on PATH?)" rc path;
+ let ic = open_in tmp in
+ let line =
+ try input_line ic
+ with End_of_file ->
+ close_in ic;
+ Alcotest.failf "sha256sum produced no output for %s" path
+ in
+ close_in ic;
+ match String.index_opt line ' ' with
+ | Some i -> String.sub line 0 i
+ | None -> Alcotest.failf "unexpected sha256sum output for %s: %S" path line)
+
+let fixture_sha256 = "a744a2dbc7c8cf09590005e956c973f316209f3cf102b8035edadebf764110de"
+
+let real_layer () =
+ let layer =
+ match Layer.load V.rank_of_sexp sanctoral_path with
+ | Ok l -> l
+ | Error e -> Alcotest.failf "%s: failed to load: %s" sanctoral_path e
+ in
+ let overlay =
+ match Overlay.load V.rank_of_sexp adjustments_path with
+ | Ok o -> o
+ | Error e -> Alcotest.failf "%s: failed to load: %s" adjustments_path e
+ in
+ let layer, diagnostics = Overlay.apply layer overlay in
+ Alcotest.(check (list string)) "the committed overlay applies cleanly, no diagnostics" []
+ (List.map Overlay.diagnostic_to_string diagnostics);
+ layer
+
+let real_lectionary () =
+ match Colitur_kernel.Lectionary.load "../data/ef/lectionary.sexp" with
+ | Ok l -> l
+ | Error e -> Alcotest.failf "../data/ef/lectionary.sexp: failed to load: %s" e
+
+let real_commons () =
+ match Rite_ef.Lectionary_ef.Commons.load "../data/ef/commons.sexp" with
+ | Ok c -> c
+ | Error e -> Alcotest.failf "../data/ef/commons.sexp: failed to load: %s" e
+
+(* ---------------------------------------------------------------------- *)
+(* The Ordo side: the sexp row, mirrored from tools/extract_fiuv_ordo.ml's *)
+(* own [row] -- see this file's own header on why there is no *)
+(* [has_diocesan_variant] field here, unlike the LMS mirror. *)
+(* ---------------------------------------------------------------------- *)
+
+open Sexplib0.Sexp_conv
+
+type ordo_row = {
+ date : string;
+ class_ : string option;
+ title : string;
+ te_deum : bool option;
+ gloria : bool option;
+ credo : bool option;
+ praef : string option;
+ alt_te_deum : bool option;
+ (** the "-VEL-" Cum-Sanctissima-alternative office's own Te Deum,
+ when this day prints one -- see tools/extract_fiuv_ordo.ml's own
+ [row.alt_te_deum] citation. Captured, not compared here: closing
+ F3 needed [te_deum] itself to stop reading it, not a new
+ colitur-side axis to check it against. *)
+}
+[@@deriving sexp]
+
+let ordo_rows () =
+ let sexp =
+ try Sexplib.Sexp.load_sexp fixture_path
+ with e -> Alcotest.failf "%s: failed to load: %s" fixture_path (Printexc.to_string e)
+ in
+ list_of_sexp ordo_row_of_sexp sexp
+
+let window_first = "2025-11-27"
+let window_last = "2026-12-31"
+
+(* ---------------------------------------------------------------------- *)
+(* The colitur side, over the same window -- mirrors test_lms_ordo.ml's *)
+(* own [colitur_rows] exactly (a liturgical year straddles two civil *)
+(* years, so the resolution walk starts a civil year early and ends one *)
+(* late). *)
+(* ---------------------------------------------------------------------- *)
+
+type colitur_row = { c_date : string; c_creed : bool; c_gloria : bool; c_preface : Preface.t option }
+
+let colitur_rows () =
+ let layer = real_layer () in
+ let rite = Rite_ef.context ~lectionary:(real_lectionary ()) ~commons:(real_commons ()) in
+ let by_rata : (int, (V.season, V.rank) LD.t) Hashtbl.t = Hashtbl.create 800 in
+ for y = 2024 to 2027 do
+ let days = Cal.year rite layer y in
+ Array.iter (fun (d : (V.season, V.rank) LD.t) -> Hashtbl.replace by_rata (Date.to_rata d.LD.date) d) days
+ done;
+ let mk s = match Date.of_iso8601 s with Ok d -> d | Error e -> Alcotest.failf "%s: %s" s e in
+ let rows = ref [] in
+ let d = ref (mk window_first) in
+ let stop = mk window_last in
+ while Date.compare !d stop <= 0 do
+ (match Hashtbl.find_opt by_rata (Date.to_rata !d) with
+ | Some day ->
+ rows :=
+ { c_date = Date.to_iso8601 day.LD.date; c_creed = day.LD.creed; c_gloria = day.LD.gloria;
+ c_preface = day.LD.preface }
+ :: !rows
+ | None -> Alcotest.failf "no colitur day resolved for %s" (Date.to_iso8601 !d));
+ d := Date.add_days !d 1
+ done;
+ List.rev !rows
+
+(* Te Deum has no colitur-side [Liturgical_day.t] field of its own -- it is
+ a Breviary fact {!Rite_ef.Rubrics_ef.gloria} reads internally, not a Mass
+ part {!Colitur_kernel.Rite.t} exposes. Resolved separately, straight from
+ {!Rite_ef.Rubrics_ef.te_deum}, over the identical window/day set. *)
+type colitur_te_deum_row = {
+ t_date : string;
+ t_te_deum : bool;
+ t_rank : V.rank;
+ t_slug : string;
+ t_has_commemoration : bool;
+}
+
+let colitur_te_deum_rows () =
+ let layer = real_layer () in
+ let rite = Rite_ef.context ~lectionary:(real_lectionary ()) ~commons:(real_commons ()) in
+ let by_rata : (int, (V.season, V.rank) LD.t) Hashtbl.t = Hashtbl.create 800 in
+ for y = 2024 to 2027 do
+ let days = Cal.year rite layer y in
+ Array.iter (fun (d : (V.season, V.rank) LD.t) -> Hashtbl.replace by_rata (Date.to_rata d.LD.date) d) days
+ done;
+ let mk s = match Date.of_iso8601 s with Ok d -> d | Error e -> Alcotest.failf "%s: %s" s e in
+ let rows = ref [] in
+ let d = ref (mk window_first) in
+ let stop = mk window_last in
+ while Date.compare !d stop <= 0 do
+ (match Hashtbl.find_opt by_rata (Date.to_rata !d) with
+ | Some day ->
+ let te_deum =
+ Rite_ef.Rubrics_ef.te_deum ~temporal:day.LD.temporal ~observed:day.LD.observed ~date:day.LD.date
+ in
+ rows :=
+ { t_date = Date.to_iso8601 day.LD.date; t_te_deum = te_deum;
+ t_rank = day.LD.observed.Colitur_kernel.Celebration.rank;
+ t_slug = Colitur_kernel.Slug.to_string day.LD.observed.Colitur_kernel.Celebration.slug;
+ t_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
+ done;
+ List.rev !rows
+
+(* ---------------------------------------------------------------------- *)
+(* data/ef/expected-divergences-fiuv.sexp -- same shape as the LMS/ *)
+(* missalemeum allow-lists' own [allow_entry]. A SEPARATE file: this Ordo *)
+(* is a fourth, independent lineage from all three others already in this *)
+(* project. *)
+(* ---------------------------------------------------------------------- *)
+
+type allow_entry = { id : string; citation : string; verdict : string; note : string; expected_rows : int }
+[@@deriving sexp]
+
+let load_allow_list () =
+ let sexps =
+ try Sexplib.Sexp.load_sexps allow_list_path
+ with e -> Alcotest.failf "%s: failed to load: %s" allow_list_path (Printexc.to_string e)
+ in
+ List.map allow_entry_of_sexp sexps
+
+(* ---------------------------------------------------------------------- *)
+(* Tests *)
+(* ---------------------------------------------------------------------- *)
+
+let test_fixture_checksum () =
+ Alcotest.(check string) "fixture SHA-256 matches its provenance note" fixture_sha256 (sha256_of_file fixture_path)
+
+let test_dates_align () =
+ let ordo = ordo_rows () in
+ let colitur = colitur_rows () in
+ Alcotest.(check int) "the Ordo fixture has 400 rows (2025-11-27..2026-12-31)" 400 (List.length ordo);
+ Alcotest.(check int) "colitur resolved the same number of days" (List.length ordo) (List.length colitur);
+ (match ordo with o :: _ -> Alcotest.(check string) "first date" window_first o.date | [] -> Alcotest.fail "empty");
+ (match List.rev ordo with
+ | o :: _ -> Alcotest.(check string) "last date" window_last o.date
+ | [] -> Alcotest.fail "empty");
+ let mismatched =
+ List.filter_map
+ (fun (o, c) -> if String.equal o.date c.c_date then None else Some (o.date, c.c_date))
+ (List.combine ordo colitur)
+ in
+ Alcotest.(check (list (pair string string))) "no misaligned dates" [] mismatched
+
+(* Holy Saturday (2026-04-04) is the ONLY day this fixture shows no Credo
+ marker for -- confirmed directly against the raw source: unlike the
+ LMS Ordo (whose "no Mass" day is Good Friday, the 1955-restored Holy
+ Week having no Mass that day at all), THIS Ordo's Good Friday DOES
+ carry a Mass rubric ("Missa pr., ... Gloria, sine Credo, praef.
+ comm."), leaving Holy Saturday as the sole day with no Mass mentioned
+ at all (its own Vigil Mass is conventionally counted under Easter
+ Sunday's own date in this source, matching a real, if narrower,
+ instance of the same "a vigil Mass is dated to the day it anticipates"
+ convention the LMS characterisation already established). Asserted as
+ the ONLY such day, not merely observed once: a parsing regression that
+ swallowed a real row would otherwise silently join this bucket instead
+ of failing loudly as an unexplained mismatch. *)
+let test_creed_coverage () =
+ let ordo = ordo_rows () in
+ let no_creed = List.filter (fun o -> o.credo = None) ordo in
+ Alcotest.(check (list string)) "only Holy Saturday has no Ordo Creed marker" [ "2026-04-04" ]
+ (List.map (fun o -> o.date) no_creed)
+
+let describe_creed_mismatch (o : ordo_row) (c : colitur_row) =
+ Printf.sprintf "%s %S: colitur creed=%b, Ordo creed=%b" o.date o.title c.c_creed (Option.get o.credo)
+
+(* Which allow-list id, if any, explains a Creed mismatch on this date --
+ date-keyed, not "any declared entry excuses any mismatch", the same
+ discipline test_lms_ordo.ml's own [allow_list_id_for_date] uses and
+ for the identical reason: an entry that starts firing on an
+ UNEXPECTED new date must be visible as a real change, not silently
+ absorbed. Empty until characterisation found a real divergence to
+ name. *)
+let fiuv_allow_list_id_for_date = ([] : (string * string) list)
+
+let test_creed_matches_or_is_explained () =
+ let ordo = ordo_rows () in
+ 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 unexplained = ref [] in
+ let explained_counts = Hashtbl.create 8 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.credo with
+ | None -> ()
+ | Some ocreed ->
+ if Bool.equal ocreed c.c_creed then ()
+ else
+ match List.assoc_opt o.date fiuv_allow_list_id_for_date with
+ | Some id -> Hashtbl.replace explained_counts id (1 + try Hashtbl.find explained_counts id with Not_found -> 0)
+ | None -> 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);
+ Hashtbl.iter
+ (fun id n ->
+ match List.assoc_opt id by_id with
+ | None -> Alcotest.failf "allow-list entry %s used by the comparator but not declared in %s" id allow_list_path
+ | Some e -> Alcotest.(check int) (Printf.sprintf "%s: expected_rows matches the actual count" id) e.expected_rows n)
+ explained_counts;
+ List.iter
+ (fun e ->
+ if not (Hashtbl.mem explained_counts e.id) then
+ Alcotest.failf "allow-list entry %s is declared but never matched a real divergence" e.id)
+ allow_list
+
+(* ---------------------------------------------------------------------- *)
+(* Gloria (RG 431-432, {!Rite_ef.Rubrics_ef.gloria}) -- Phase 2. Same shape *)
+(* as the Creed comparison above, over the identical 400-row window. *)
+(* ---------------------------------------------------------------------- *)
+
+let test_gloria_coverage () =
+ let ordo = ordo_rows () in
+ let no_gloria = List.filter (fun o -> o.gloria = None) ordo in
+ Alcotest.(check int) "exactly one day has no Ordo Gloria marker" 1 (List.length no_gloria)
+
+let describe_gloria_mismatch (o : ordo_row) (c : colitur_row) =
+ Printf.sprintf "%s %S: colitur gloria=%b, Ordo gloria=%b" o.date o.title c.c_gloria (Option.get o.gloria)
+
+(* F1 -- OPEN, adjudicated FOR colitur. The single Gloria mismatch found:
+ Good Friday (2026-04-03). See data/ef/expected-divergences-fiuv.sexp's
+ own F1 for the full citation and the raw source text this was checked
+ against directly (docs/research/ordo/fiuv-ordo-2025-2026.pdf, page 46). *)
+let is_f1_good_friday (o : ordo_row) = String.equal o.date "2026-04-03"
+
+let test_gloria_matches_or_is_explained () =
+ let ordo = ordo_rows () in
+ let colitur = colitur_rows () in
+ let unexplained = ref [] in
+ let f1_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_f1_good_friday o then incr f1_count
+ else unexplained := describe_gloria_mismatch o c :: !unexplained)
+ ordo colitur;
+ Alcotest.(check (list string)) "every Gloria mismatch is named in the allow-list -- none unexplained" []
+ (List.rev !unexplained);
+ Alcotest.(check int) "F1 (Good Friday) count" 1 !f1_count
+
+(* ---------------------------------------------------------------------- *)
+(* Preface (RG 482-499, {!Rite_ef.Rubrics_ef.preface}) -- Phase 3. Same *)
+(* shape as Creed/Gloria above, over the identical 400-row window, using *)
+(* the [praef] column already captured into the fixture (CAPTURED, NOT *)
+(* VALIDATED at the time -- tools/extract_fiuv_ordo.ml's own [row.praef] *)
+(* citation) but never compared until now. *)
+(* *)
+(* CHARACTERISATION FIRST, per the task's own standing instruction (this
+ project has been misled by trusting an Ordo field's surface shape
+ before, four times this session alone): [praef] is RAW TRAILING TEXT
+ after the literal token "praef." to the end of the primary Mass
+ clause -- it is not a clean enum, it can carry an arbitrary tail of
+ unrelated rubrical prose glued on by the same token-flattening that
+ produces it (Good Friday's own [gloria]/F1 finding above is the
+ worked example of exactly this shape). CLASSIFIED here by PREFIX
+ match against the FIRST WORD(S) only -- every prefix below was found
+ by direct inspection of the fixture's own distinct [praef] values
+ (test/fixtures/fiuv-ordo-2025-2026.sexp, 400 rows, all read by hand
+ while building this comparison) and cross-checked against the primary
+ rubric text, not guessed. Two genuine EXCEPTION SHAPES, THREE rows,
+ found the same way:
+
+ - Christmas Day (both of them -- the fixture's own window, 27
+ November 2025-31 December 2026, spans thirteen months and TWO 25
+ Decembers, confirmed by running the coverage test below before
+ pinning it at "two", not three, the first time) and Epiphany Day
+ (2026-01-06, the window's only one) print "praef. et Communic..."
+ -- an ELLIPSIS: the compiler treats "this feast's own [i.e.
+ obviously implied] preface" as not needing its own name restated
+ on the ONE day that IS that feast, unlike every octave/de-Tempore
+ day governed by the SAME feast (which always restates the name
+ explicitly, e.g. 26 December's own "de Nativ."). Confirmed by
+ direct inspection: no OTHER day in the fixture omits the name this
+ way. [classify_praef] returns [None] (unrecognised) for all three
+ rather than special-casing them by date -- both feasts are
+ independently, directly citable from RG 484(a)/485(a)'s own text
+ (the Nativity/Epiphany feast itself is the paradigm case each
+ clause names first), so the oracle's corroboration is not needed
+ to trust colitur's own answer on these dates, and forcing a match
+ here would mean trusting an inference about the source's own
+ ellipsis rather than reading it.
+ - Good Friday (2026-04-03) prints "comm. Feria VI prima in mense." --
+ classifies cleanly as [Common] by the same prefix rule as any
+ other day, but this file's own F1 (Gloria, above) already
+ adjudicated this EXACT date's raw text as unreliable (a copied,
+ not a considered, line -- confirmed against the PDF's own page
+ image, not merely the extracted fixture). The same defect
+ extends to whatever trails "praef." on the identical corrupted
+ line: excluded from this comparison by reusing [is_f1_good_friday]
+ directly, not re-argued. *)
+
+let has_prefix ~prefix s =
+ String.length s >= String.length prefix && String.sub s 0 (String.length prefix) = prefix
+
+(* Every prefix below maps 1:1 onto exactly one branch of
+ {!Rite_ef.Rubrics_ef.preface}'s own priority order -- see that
+ function's own header for the full citation of each. Order does not
+ matter here (unlike in [preface] itself): the Ordo's own printed text
+ never carries two of these prefixes on the same row, so this is a
+ partition, not a priority list. *)
+let praef_prefixes =
+ [ ("comm.", Preface.Common);
+ ("Trinit.", Preface.Trinity);
+ ("Quadr.", Preface.Lent);
+ ("de Sancta Cruce", Preface.Holy_cross);
+ ("Pasch.", Preface.Easter);
+ ("etc. ut in festo", Preface.Easter);
+ (* Easter-octave ferias, "as on the feast [of Easter]" -- e.g. 6
+ April 2026, "etc. ut in festo., ad Ite, missa est additur duplex
+ Alleluia." *)
+ ("Ascensionis", Preface.Ascension);
+ ("de Spirito Sancto", Preface.Holy_spirit);
+ ("BMV", Preface.Bvm);
+ ("App.", Preface.Apostles);
+ ("de Nativ.", Preface.Nativity);
+ ("de Epiphania", Preface.Epiphany);
+ ("de Ss.mi Corde", Preface.Sacred_heart);
+ ("de Ss.mo Corde", Preface.Sacred_heart);
+ ("de Domino Nostro Jesu Rege", Preface.Christ_the_king);
+ ("de S Iosepho", Preface.St_joseph);
+ ("de S Ioseph", Preface.St_joseph);
+ ("defunctorum", Preface.Requiem) ]
+
+let classify_praef (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
+
+(* Measured directly, not assumed: every one of the 400 rows' own [praef]
+ text, classified, tallied by outcome. Two rows are genuinely
+ unclassifiable by this prefix table (the ellipsis dates above); every
+ other non-[None] [praef] value classifies. Pinned so a change to
+ either the fixture or [classify_praef] that silently drops coverage
+ fails loudly here rather than merely narrowing the comparison below. *)
+let test_praef_classification_coverage () =
+ let ordo = ordo_rows () in
+ let with_praef = List.filter_map (fun o -> o.praef) ordo in
+ Alcotest.(check int) "399 of 400 rows carry a [praef] value (Holy Saturday is the one exception)" 399
+ (List.length with_praef);
+ let unclassified = List.filter (fun s -> classify_praef s = None) with_praef in
+ (* THREE rows, not two: the fixture's own window (27 November 2025-31
+ December 2026, 13 months) spans TWO Christmas Days, both printing the
+ identical ellipsis text -- found running this exact assertion, not
+ assumed from the date count alone. *)
+ Alcotest.(check int) "the three ellipsis rows (two Christmas Days, one Epiphany) are unclassified" 3
+ (List.length unclassified);
+ Alcotest.(check bool) "every unclassified row starts with the ellipsis's own \"et Communic\"" true
+ (List.for_all (has_prefix ~prefix:"et Communic") unclassified)
+
+let describe_preface_mismatch (o : ordo_row) (c : colitur_row) (expected : Preface.t) =
+ Printf.sprintf "%s %S: colitur preface=%s, Ordo praef=%S (classified %s)" o.date o.title
+ (match c.c_preface with Some p -> Preface.to_string p | None -> "-")
+ (Option.get o.praef) (Preface.to_string expected)
+
+let test_preface_matches_or_is_explained () =
+ let ordo = ordo_rows () in
+ let colitur = colitur_rows () in
+ let unexplained = ref [] in
+ let unclassified_count = ref 0 in
+ let f1_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;
+ if is_f1_good_friday o then incr f1_count
+ else
+ match o.praef with
+ | None -> ()
+ | Some raw -> (
+ match classify_praef raw with
+ | None -> incr unclassified_count
+ | Some expected ->
+ if c.c_preface = Some expected then ()
+ else unexplained := describe_preface_mismatch o c expected :: !unexplained))
+ ordo colitur;
+ Alcotest.(check (list string)) "every classified preface difference is explained -- none unexplained" []
+ (List.rev !unexplained);
+ Alcotest.(check int) "the three ellipsis rows are skipped, not silently counted as agreement" 3
+ !unclassified_count;
+ Alcotest.(check int) "F1 (Good Friday) is skipped here too, the same root cause as the Gloria axis" 1 !f1_count
+
+(* ---------------------------------------------------------------------- *)
+(* Te Deum (Breviary 237-238, {!Rite_ef.Rubrics_ef.te_deum}) -- Phase 2, *)
+(* the mitigation the task brief names for this source's own stated *)
+(* weakness (a single, not yet scan-verified, web transcription). A *)
+(* SEPARATE colitur-side resolution ({!colitur_te_deum_rows}), since Te *)
+(* Deum has no {!Colitur_kernel.Liturgical_day.t} field of its own. *)
+(* *)
+(* The SUNDAY shape this same comparison originally found (237(b)'s own *)
+(* literal Septuagesima/Sexagesima/Quinquagesima exception, contradicted *)
+(* 15/15) is FIXED at the source ({!Rite_ef.Rubrics_ef.te_deum}'s own *)
+(* header has the correction and its full citation) -- not allow-listed, *)
+(* because it no longer diverges. Three OTHER shapes remain OPEN, *)
+(* single-witnessed (never captured by the LMS fixtures, which do not *)
+(* record Te Deum at all) -- see data/ef/expected-divergences-fiuv.sexp's *)
+(* own F2/F3/F4 for the full citations. Matched by PREDICATE, the same *)
+(* "varies by shape, not by a fixed date list" reasoning L5/L6 already *)
+(* establish for the LMS suite. *)
+(* ---------------------------------------------------------------------- *)
+
+let test_te_deum_coverage () =
+ let ordo = ordo_rows () in
+ let no_te_deum = List.filter (fun o -> o.te_deum = None) ordo in
+ Alcotest.(check int) "the unresolved-Te-Deum population matches this fixture's own measured figure" 133
+ (List.length no_te_deum)
+
+let describe_te_deum_mismatch (o : ordo_row) (t : colitur_te_deum_row) =
+ Printf.sprintf "%s %S: colitur te_deum=%b, Ordo te_deum=%b (rank=%s commemoration=%b)" o.date o.title t.t_te_deum
+ (Option.get o.te_deum) (V.rank_to_string t.t_rank) t.t_has_commemoration
+
+(* F2 (every I-class and omissible vigil), F4 (the three September Ember
+ days) and NOW F3 (the privileged-Lenten/Passiontide-feria-plus-
+ commemoration shape) were ALL THREE found against a buggy extraction
+ (see the fixture's own provenance header, "FIX HISTORY" section, and
+ {!Rite_ef.Rubrics_ef.te_deum}'s own 237(a) comment for F2/F4's own
+ account) -- re-run against the corrected data, NONE of the three fires
+ any more.
+
+ F3 specifically was NEVER a Te Deum defect on either side: the six
+ dates print a complete SECOND office after a literal "-VEL-" ("or")
+ separator -- the same saint kept outright under the 2020 decree Cum
+ Sanctissima, with its OWN "Ad Mat. ... Te Deum" lessons -- and
+ tools/extract_fiuv_ordo.ml's own [extract_te_deum] used to scan the
+ WHOLE block unbounded, so on these six days (whose PRIMARY, strict-
+ 1962 office states no "Ad Mat." of its own at all) it silently read
+ the SECOND office's Te Deum instead. Fixed at the source
+ (tools/extract_fiuv_ordo.ml's own [split_at_alt_office]); the fixture
+ re-extracted from the SAME pdftotext dump. colitur's own answer
+ ([false], the primary office's own ferial Matins) was never wrong --
+ the OTHER side of the comparison was reading the wrong printed office.
+ [te_deum] now reads [None] (unresolved, not [false]) on these six
+ dates, because the PRIMARY office's own text states no "Ad Mat." at
+ all, so there is nothing to positively confirm either direction from
+ -- consistent with [te_deum]'s own documented meaning elsewhere in
+ this fixture.
+
+ NOTE, so a reader arriving here from data/ef/expected-divergences-lms.sexp's
+ own L5 does not draw the wrong conclusion: F3 never "contradicted" L5
+ in the first place, and its removal does not corroborate L5 either.
+ Both were the SAME underlying fact read two different ways -- L5's own
+ 16 LMS dates and these 6 FIUV dates are exactly the shape the 2020
+ decree Cum Sanctissima changes, and colitur, by CLAUDE.md's own binding
+ decision 2, models the 1962 core without it. See L5's own rewritten
+ entry for the full account, including the FIUV "-VEL-" dual printing
+ as the decisive evidence for BOTH files.
+
+ Removed rather than kept as dead code with an [expected 0] pin, the
+ same "an entry that stops firing is a real change, not silently
+ absorbed" discipline data/ef/expected-divergences-lms.sexp's own L1/L3
+ closures already establish -- see that file's own history for the
+ precedent this follows. *)
+
+let test_te_deum_matches_or_is_explained () =
+ let ordo = ordo_rows () in
+ let colitur = colitur_te_deum_rows () in
+ let unexplained = ref [] in
+ List.iter2
+ (fun (o : ordo_row) (t : colitur_te_deum_row) ->
+ if not (String.equal o.date t.t_date) then Alcotest.failf "misaligned: ordo %s vs colitur %s" o.date t.t_date;
+ match o.te_deum with
+ | None -> ()
+ | Some otd -> if Bool.equal otd t.t_te_deum then () else unexplained := describe_te_deum_mismatch o t :: !unexplained)
+ ordo colitur;
+ Alcotest.(check (list string)) "every Te Deum mismatch is named in the allow-list -- none unexplained" []
+ (List.rev !unexplained)
+
+let suite =
+ ( "fiuv-ordo",
+ [ Alcotest.test_case "fixture SHA-256 matches its provenance note" `Quick test_fixture_checksum;
+ Alcotest.test_case "streams are 400 rows each, dates aligned 1:1" `Quick test_dates_align;
+ Alcotest.test_case "only Holy Saturday 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 "Ordo Gloria coverage matches the measured figure" `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;
+ Alcotest.test_case "praef classification coverage matches the measured figure" `Quick
+ test_praef_classification_coverage;
+ Alcotest.test_case "every classified preface difference is explained -- none unexplained" `Quick
+ test_preface_matches_or_is_explained;
+ Alcotest.test_case "Ordo Te Deum coverage matches the measured figure" `Quick test_te_deum_coverage;
+ Alcotest.test_case "every Te Deum difference is named in the cited allow-list -- none unexplained" `Quick
+ test_te_deum_matches_or_is_explained
+ ] )