summaryrefslogtreecommitdiff
path: root/test/test_fiuv_ordo.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-22 23:24:12 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-22 23:24:12 +0200
commit0806fe65e388a9502035bc9d4f551528ac74b26d (patch)
tree094d96e0d685c90fbfc8c91e659d185b1141a1c0 /test/test_fiuv_ordo.ml
parent761ae859d73660bcfaa59581312989cb942e704d (diff)
downloadcolitur-0806fe65e388a9502035bc9d4f551528ac74b26d.tar.gz
colitur-0806fe65e388a9502035bc9d4f551528ac74b26d.zip
feat(ef): the Mass preface, RG 482-499
Item 2 of Phase 3 (celebrant-rubrics-phase1), the bulk of this phase and the last EF Mass rubric this project scoped: which preface is said, deferring to seasonal/proper-title rules for the fourteen named prefaces, then RG 498's Common residual. RG 482 gives the resolution chain: the Mass's own proper preface, failing that the seasonal one, failing that Common. Read literally, RG 484-497 look like fourteen separate rules, but each numbered rubric's own propria/de-Tempore pair produces the SAME preface identity either way, so the whole chain collapses into one priority- ordered decision: title/mystery triggers (Holy Cross, Sacred Heart, Christ the King, Trinity, St Joseph, BVM, the Nativity octave, the Apostles, Epiphany), each independent of season, then six seasonal windows (Nativity, Epiphany, Lent, Holy Cross/Passiontide, Easter, Ascension, Holy Spirit, Trinity-for-ordinary-Sundays), then Common. The "one genuinely unproven piece" the design spec worried about -- per-feast proper prefaces extracted from the Missal's propers at scale -- turned out not to be a large-scale extraction problem at all: every one of the fourteen propers is a closed, small, subject/slug-keyed trigger (mirroring Precedence_ef.band's own RG 91 table), not thousands of individual saints' pages. The one genuine open question (RG 488, the Chrism Mass) is N/A: this engine resolves one Mass per civil day and has no separate Chrism-Mass dimension. Preface is a new kernel type (lib/kernel/preface.ml[i]), not an EF-specific one, the same placement as Colour/Subject/Mass_formulary: Liturgical_day.t is parameterised only over season/rank, so any field it carries generically must live in the kernel even though only EF constructs a value of it today. Wired exactly as creed/gloria were (Rite.t.preface, Calendar.ml, Rite_ef.context), but Preface.t option, not a bare bool: unlike creed/gloria, a preface is said only at a Mass, and Good Friday (1955-restored Holy Week) resolves an observed celebration but has no Mass at all -- None is the honest answer there, and also the neutral value an unimplemented rite returns. Two priority-order findings only the oracle settled, not derivable from the Latin text in isolation, both cross-checked against 358 individually classified entries in the FIUV Ordo's own praef column (test/fixtures/fiuv-ordo-2025-2026.sexp, already captured, wired up here as a new comparison axis): RG 484(b)'s own "except Masses with a proper of the divine mysteries or Persons" is narrower than every other window's implicit exception (an Apostle inside the Nativity octave is overridden to Nativity; outside it, keeps his own preface even inside another window); and RG 495's "et votivis" half is live for the one office this engine models without a votive-Mass dimension (the Saturday Office of the BVM), which also proved a vigil is not a "festum" for this purpose (the Assumption's own vigil takes Common, not BVM, correcting an initial reuse of Precedence_ef.marian_slugs that had no reason to make that distinction for its own, different rubric). A third, RG16(a)-shaped fix landed the same way: RG 494(b)'s own Trinity grant must read the day's TEMPORAL season, not the celebration that actually won it, or a Class1 feast with no preference of its own (All Saints) wrongly falls to Common on a Sunday it merely commemorates. colitur rubrics gains a sixth TAB-separated column. Domain-wide 1583-9999 exhaustive sweep confirms every Christmastide day resolves Nativity/Epiphany/Bvm, every Paschaltide day one of its own three windows or a season-independent title, every Lent day Lent or a title, and Passiontide legitimately produces Easter exactly once a year (the Vigil Mass) -- 8416 of the domain's 8417 years, the one short year being the domain's own upper boundary (the liturgical year opening in 9999 cannot construct dates in year 10000, a pre-existing edge this sweep re-confirms rather than a new one). day/readings verified byte-identical to the branch's own state before this phase (commit 9c96e0a) across a 455-year sample spanning the whole domain -- not literally to the v0.10.1 tag, which 22 earlier commits on this same branch (Phase 1/2, the bissextile shift, the Rogation colour fix) had already moved past before this phase began.
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