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.ml153
1 files changed, 151 insertions, 2 deletions
diff --git a/test/test_fiuv_ordo.ml b/test/test_fiuv_ordo.ml
index 5c009a9..3bfed68 100644
--- a/test/test_fiuv_ordo.ml
+++ b/test/test_fiuv_ordo.ml
@@ -40,6 +40,7 @@ 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"
@@ -138,7 +139,7 @@ let window_last = "2026-12-31"
(* late). *)
(* ---------------------------------------------------------------------- *)
-type colitur_row = { c_date : string; c_creed : bool; c_gloria : bool }
+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
@@ -156,7 +157,9 @@ let colitur_rows () =
(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 } :: !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;
@@ -346,6 +349,148 @@ let test_gloria_matches_or_is_explained () =
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 *)
@@ -440,6 +585,10 @@ let suite =
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