(* 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 ] )