aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/cli.t33
-rw-r--r--test/test_calendar.ml10
-rw-r--r--test/test_fiuv_ordo.ml153
-rw-r--r--test/test_rubrics_ef.ml387
-rw-r--r--test/test_validate.ml16
5 files changed, 573 insertions, 26 deletions
diff --git a/test/cli.t b/test/cli.t
index 8bd5b9f..4ae116f 100644
--- a/test/cli.t
+++ b/test/cli.t
@@ -210,9 +210,9 @@ separate command for the same mechanical reason `readings` is: `day`'s row
is fixed-width space-separated with a variable-length "+slug" tail.
$ colitur rubrics 2026 | head -3
- 2026-01-01 ef-circumcision own true true
- 2026-01-02 ef-christmas-1-friday own false true
- 2026-01-03 ef-christmas-1-saturday votive false true
+ 2026-01-01 ef-circumcision own true true nativity
+ 2026-01-02 ef-christmas-1-friday own false true nativity
+ 2026-01-03 ef-christmas-1-saturday votive false true bvm
$ colitur rubrics 2026 | wc -l
365
@@ -224,19 +224,19 @@ apply to it -- step 2 does (the day's own temporal slug in the lectionary),
tagged `own`. Contrast a real sanctoral saint with his own proper:
$ colitur rubrics 2038 | grep '^2038-03-08'
- 2038-03-08 john-of-god proper false true
+ 2038-03-08 john-of-god proper false true common
A saint with no proper of his own says his assigned Common (step 4):
$ colitur rubrics 2038 | grep '^2038-03-06'
- 2038-03-06 common-of-non-virgins-1 common false true
+ 2038-03-06 common-of-non-virgins-1 common false true common
A weekday with no proper of its own resumes the preceding Sunday's, never
its own observed slug -- 1 December 2025 is the Monday after Advent I, and
Advent's ferias have no Mass of their own (step 3):
$ colitur rubrics 2025 | grep '^2025-12-01'
- 2025-12-01 ef-advent-sunday-1 preceding-sunday false false
+ 2025-12-01 ef-advent-sunday-1 preceding-sunday false false common
3 January 2026 above ("votive") is the RG 78/309(a) Saturday Mass of Our
Lady, said IN PLACE of the day's own office's Mass while the office (an
@@ -257,9 +257,9 @@ diocesan overlay's local patron observed instead (no proper or Common of his
own in the fixture), the chain falls all the way back to step 3:
$ colitur rubrics 2026 --overlay fixtures/overlay-example-diocesan.sexp | grep '^2026-07-11'
- 2026-07-11 ef-time-after-pentecost-sunday-6 preceding-sunday false true
+ 2026-07-11 ef-time-after-pentecost-sunday-6 preceding-sunday false true common
$ colitur rubrics 2026 | grep '^2026-07-11'
- 2026-07-11 ef-time-after-pentecost-6-saturday votive false true
+ 2026-07-11 ef-time-after-pentecost-6-saturday votive false true bvm
`--lang`/`--raw`/`--sigla-*` are refused rather than silently ignored, unlike
`readings`: this row resolves no display name and no citation for any of
@@ -343,13 +343,13 @@ prints for the identical day, so the two cannot silently drift apart
again in either direction:
$ colitur --help | grep '^ rubrics date'
- rubrics date, formulary slug, source, creed, gloria -- TAB-separated
+ rubrics date, formulary slug, source, creed, gloria, preface -- TAB-separated
$ colitur --help | sed -n '/^ rubrics date/{n;p}'
- 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true
+ 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true[TAB]nativity
$ colitur rubrics 2026 | grep '^2026-01-01' | sed $'s/\t/[TAB]/g'
- 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true
+ 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true[TAB]nativity
--version prints the version alone, to standard output, exit 0. Deliberately
not embedded in the help text above: this pin would then have to be edited
@@ -589,8 +589,17 @@ addition: 2027-01-01's own record wraps [gloria] onto its own new line,
while 2027-01-02's fits it on the same line as [creed] and [formulary] --
172 of 365 records happened to cross a wrap boundary, the rest did not.
+9197 -> 9252 (preface, celebrant-rubrics-phase1 Phase 3): {!Liturgical_day.t}
+gained a [preface] field, the same seam [gloria] just above used -- all
+365 of 2027's records print a new [(preface (<variant>))] token (checked
+directly, [grep -c "(preface"]), [(preface ())] on exactly one day
+(2027's own Good Friday, 26 March -- this engine resolves no Mass at all
+that day, {!Rite_ef.Rubrics_ef.preface}'s own citation). Same cosmetic
+reflow mechanics as every entry above, not a fixed one-line-per-record
+addition.
+
$ colitur emit --format sexp --from 2027 --to 2027 | wc -l
- 9197
+ 9252
$ colitur emit --format xml --from 2027 --to 2027 | head -2
<?xml version="1.0" encoding="UTF-8"?>
diff --git a/test/test_calendar.ml b/test/test_calendar.ml
index 157841b..9059574 100644
--- a/test/test_calendar.ml
+++ b/test/test_calendar.ml
@@ -104,11 +104,13 @@ module Fixture = struct
the sanctoral side. *)
let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = (None, [])
- (* No fixture here exercises the Creed or Gloria rubrics either -- a rite
- that has not implemented them returns [false] explicitly,
- {!Rite.t.creed}/{!Rite.t.gloria}'s own documented default. *)
+ (* No fixture here exercises the Creed, Gloria or preface rubrics either
+ -- a rite that has not implemented them returns [false]/[None]
+ explicitly, {!Rite.t.creed}/{!Rite.t.gloria}/{!Rite.t.preface}'s own
+ documented default. *)
let creed ~temporal:_ ~observed:_ ~date:_ = false
let gloria ~temporal:_ ~observed:_ ~date:_ = false
+ let preface ~temporal:_ ~observed:_ ~date:_ = None
let rite : (season, rank) Rite.t =
{ Rite.id = "synthetic-calendar"; vocab; year_start; temporal; anchors = (fun _ -> []);
@@ -120,7 +122,7 @@ module Fixture = struct
(* Not a Roman rite either, so no bissextile-doubling convention:
identity, {!Rite.t.fixed_key}'s own documented default. *)
fixed_key = (fun d -> Some (D.month d, D.day d));
- rules; season_runs = [ A; B ]; transfer_target; readings; creed; gloria }
+ rules; season_runs = [ A; B ]; transfer_target; readings; creed; gloria; preface }
let entry ~month ~day ~slug ~rank =
{ Layer.date = (match Date_spec.fixed ~month ~day with Ok d -> d | Error e -> failwith e);
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
diff --git a/test/test_rubrics_ef.ml b/test/test_rubrics_ef.ml
index d533670..bb3bfed 100644
--- a/test/test_rubrics_ef.ml
+++ b/test/test_rubrics_ef.ml
@@ -746,6 +746,333 @@ let test_exhaustive_gloria_domain_sweep () =
Alcotest.(check bool) "the full domain reached a real number of Requiem days" true (black > 1_000)
end
+(* ---- RG 482-499, the preface -- see lib/rites/rite_ef/rubrics_ef.ml for
+ the rubric quoted in full and every branch's own citation. One
+ end-to-end test per branch, resolved against REAL calendar dates
+ through the shipped data, the same discipline every other section of
+ this file already follows -- most of these dates were cross-checked
+ directly against the FIUV Ordo's own [praef] column
+ (test/fixtures/fiuv-ordo-2025-2026.sexp) before being pinned here, not
+ merely derived from the Latin text in isolation; see [preface]'s own
+ header for the full account of what that cross-check settled. ---- *)
+
+module Pref = Colitur_kernel.Preface
+
+let preface_on y m d = (day_on y m d).LD.preface
+
+let preface_string_on y m d =
+ match preface_on y m d with Some p -> Pref.to_string p | None -> "-"
+
+let check_preface name expected y m d =
+ Alcotest.(check string) name (Pref.to_string expected) (preface_string_on y m d)
+
+let check_no_preface name y m d = Alcotest.(check string) name "-" (preface_string_on y m d)
+
+(* ---- Good Friday: no Mass at all, [None] -- checked ahead of the
+ Requiem/Black-colour branch (this file's own [preface] header explains
+ why the two need different answers, unlike creed/gloria/te_deum). ---- *)
+let test_preface_good_friday_no_mass () = check_no_preface "Good Friday: no Mass, no preface" 2026 4 3
+
+(* ---- RG 499: the Requiem proxy, the SAME two-member {!Colour.Black}
+ population {!test_colour_black_population_is_exactly_two} already
+ asserts, with Good Friday split out above. ---- *)
+let test_preface_499_all_souls () =
+ check_preface "RG 499: All Souls' Day (transferred)" Pref.Requiem 2025 11 3
+
+(* ---- RG 487(a): the two fixed Holy Cross feast triggers. ---- *)
+let test_preface_487a_exaltation () =
+ check_preface "RG 487(a): the Exaltation of the Holy Cross" Pref.Holy_cross 2026 9 14
+
+let test_preface_487a_precious_blood () =
+ check_preface "RG 487(a): the Most Precious Blood" Pref.Holy_cross 2026 7 1
+
+(* ---- RG 491/492: Sacred Heart, Christ the King -- single named days. ---- *)
+let test_preface_491_sacred_heart () = check_preface "RG 491: the Sacred Heart" Pref.Sacred_heart 2026 6 12
+let test_preface_492_christ_the_king () =
+ check_preface "RG 492: Christ the King" Pref.Christ_the_king 2026 10 25
+
+(* ---- RG 490(a): Ascension Day itself. ---- *)
+let test_preface_490a_ascension_day () = check_preface "RG 490(a): Ascension Day" Pref.Ascension 2026 5 14
+
+(* ---- RG 494(a): Trinity Sunday itself. ---- *)
+let test_preface_494a_trinity_sunday () = check_preface "RG 494(a): Trinity Sunday" Pref.Trinity 2026 5 31
+
+(* ---- RG 496: St Joseph's two feasts. ---- *)
+let test_preface_496_joseph_spouse () = check_preface "RG 496: St Joseph, Spouse of the BVM" Pref.St_joseph 2026 3 19
+let test_preface_496_joseph_workman () = check_preface "RG 496: St Joseph the Workman" Pref.St_joseph 2027 5 1
+
+(* ---- RG 495: a genuine Marian FEAST via [marian_slugs] (the Assumption),
+ and the VOTIVE-shaped BVM Saturday Office via [subject = Bvm] -- the
+ latter is the live witness for RG 495's own "et votivis" half on this
+ engine's data (this file's own [preface] header has the full account,
+ corroborated against the Ordo on 3/10 January 2026). ---- *)
+let test_preface_495_assumption () = check_preface "RG 495: the Assumption (marian_slugs)" Pref.Bvm 2026 8 15
+let test_preface_495_bvm_saturday_office () =
+ check_preface "RG 495: the BVM Saturday Office (subject=Bvm, votive-shaped)" Pref.Bvm 2026 7 11
+
+(* ---- RG 484(a)'s own explicit Purification clause: 2 February, checked
+ as a standalone slug trigger regardless of season -- see [preface]'s
+ own header for why this needed to be independent of the Nativity
+ WINDOW below (2 February is nowhere near it). ---- *)
+let test_preface_484a_purification () = check_preface "RG 484(a): the Purification" Pref.Nativity 2026 2 2
+
+(* ---- RG 484(a)/(b) merged: the Nativity octave window, 25 December-1
+ January, PLUS RG 484(b)'s own extra "2 ad 5 ianuarii" days -- one
+ contiguous window. St Stephen (26 December) is the live witness that
+ this OUTRANKS an ordinary saint's own otherwise-Common preface;
+ {!creed}'s own 475(d) test picks the identical date for the identical
+ "occurring inside a privileged window" shape. ---- *)
+let test_preface_484_nativity_day () = check_preface "RG 484(a): Christmas Day itself" Pref.Nativity 2026 12 25
+let test_preface_484_stephen_in_octave () =
+ check_preface "RG 484(b): St Stephen, occurring within the Nativity octave" Pref.Nativity 2026 12 26
+
+let test_preface_484_jan1_octave_day () =
+ check_preface "RG 484(a): 1 January, the Octave Day" Pref.Nativity 2026 1 1
+
+let test_preface_484b_jan2to5 () =
+ (* 2 January 2026 (a plain Christmastide feria, "ef-christmas-1-friday",
+ confirmed via `colitur day 2026`) -- 3 January that year is the BVM
+ Saturday Office instead (RG 495's own "et votivis" outranking this
+ window, this file's own [test_preface_495_bvm_saturday_office] and
+ [preface]'s own header have the full account), so this date is picked
+ specifically to witness the PLAIN de-Tempore grant, uncomplicated by
+ that override. *)
+ check_preface "RG 484(b): 2 January, the extra de-Tempore days" Pref.Nativity 2026 1 2
+
+(* ---- RG 484(b)'s own NARROWER exception: an Apostle/Evangelist inside
+ the Nativity octave is STILL overridden to [Nativity] (unlike every
+ other window, which an Apostle's own preface outranks -- the next test
+ below). John the Evangelist, 27 December, is the live witness this
+ file's own [preface] header cites. ---- *)
+let test_preface_484b_overrides_apostle_in_octave () =
+ check_preface "RG 484(b): St John the Evangelist, inside the Nativity octave, still [Nativity]"
+ Pref.Nativity 2026 12 27
+
+(* ---- RG 497: the Apostle/Evangelist natalicia list, reused from
+ {!creed_apostle_slugs} -- OUTSIDE the Nativity octave, an Apostle keeps
+ his own preface even inside another window (Sts Philip & James, 11 May,
+ inside the Paschaltide/Easter window -- the FIUV Ordo's own witness
+ this file's own [preface] header cites for RG 484(b)'s narrower
+ carve-out, checked from the other direction). ---- *)
+let test_preface_497_barnabas () = check_preface "RG 497: St Barnabas" Pref.Apostles 2026 6 11
+let test_preface_497_peter_paul () = check_preface "RG 497: Sts Peter & Paul" Pref.Apostles 2026 6 29
+let test_preface_497_philip_james_inside_easter_window () =
+ check_preface "RG 497: Sts Philip & James, inside the Easter window, still [Apostles]" Pref.Apostles 2026 5 11
+
+(* ---- RG 485(a)/(b): Epiphany itself and its own Baptism commemoration
+ (a), the wider 7-13 January window (b) -- Holy Family Sunday (11
+ January 2026) is the live witness that a temporal-origin Sunday inside
+ this window gets [Epiphany], not some Holy-Family-specific preface this
+ engine has none of. ---- *)
+let test_preface_485a_epiphany_day () = check_preface "RG 485(a): Epiphany Day" Pref.Epiphany 2026 1 6
+let test_preface_485a_baptism_commemoration () =
+ check_preface "RG 485(a): the Commemoration of the Baptism of the Lord" Pref.Epiphany 2026 1 13
+
+let test_preface_485b_holy_family_sunday () =
+ check_preface "RG 485(b): Holy Family Sunday, inside the 7-13 January window" Pref.Epiphany 2026 1 11
+
+(* ---- RG 486(a)/(b): the Lenten window, Ash Wednesday through the
+ Saturday before Passion Sunday I. ---- *)
+let test_preface_486_ash_wednesday () = check_preface "RG 486(a): Ash Wednesday" Pref.Lent 2026 2 18
+let test_preface_486_lent_saturday_boundary () =
+ check_preface "RG 486: the Saturday immediately before Passion Sunday I" Pref.Lent 2026 3 21
+
+(* ---- RG 487(a)/(b): the Passiontide window, Passion Sunday I through
+ Holy Thursday inclusive. ---- *)
+let test_preface_487_passion_sunday () = check_preface "RG 487: Passion Sunday I" Pref.Holy_cross 2026 3 22
+let test_preface_487_palm_sunday () = check_preface "RG 487: Palm Sunday" Pref.Holy_cross 2026 3 29
+let test_preface_487_holy_thursday () = check_preface "RG 487: Holy Thursday" Pref.Holy_cross 2026 4 2
+
+(* ---- RG 489(a)/(b): the Easter window, the Vigil Mass through the vigil
+ of the Ascension. ---- *)
+let test_preface_489_easter_vigil_mass () = check_preface "RG 489(a): the Easter Vigil Mass" Pref.Easter 2026 4 4
+let test_preface_489_easter_sunday () = check_preface "RG 489(a): Easter Sunday" Pref.Easter 2026 4 5
+let test_preface_489_ascension_vigil () = check_preface "RG 489(b): the vigil of the Ascension" Pref.Easter 2026 5 13
+
+(* ---- RG 490(b): the post-Ascension window. ---- *)
+let test_preface_490b_post_ascension_feria () =
+ check_preface "RG 490(b): the Friday after Ascension" Pref.Ascension 2026 5 15
+
+let test_preface_490b_sunday_after_ascension () =
+ check_preface "RG 490(b): the Sunday after Ascension" Pref.Ascension 2026 5 17
+
+(* ---- RG 493(a)/(b): the Pentecost-octave window, the vigil through the
+ following Saturday (the Ember Saturday). ---- *)
+let test_preface_493_pentecost_vigil () = check_preface "RG 493(a): the vigil of Pentecost" Pref.Holy_spirit 2026 5 23
+let test_preface_493_pentecost_sunday () = check_preface "RG 493(a): Pentecost Sunday" Pref.Holy_spirit 2026 5 24
+let test_preface_493_ember_saturday () =
+ check_preface "RG 493(a): the Ember Saturday within the Pentecost octave" Pref.Holy_spirit 2026 5 30
+
+(* ---- RG 494(b): the Trinity de-Tempore Sundays -- an Advent Sunday
+ ([temporal.season = Advent], [Class1] by construction) and an ordinary
+ Time-after-Pentecost Sunday. Both now read off [temporal]'s own season,
+ not [observed]'s rank -- see the next test for why the distinction is
+ live, not merely stylistic. ---- *)
+let test_preface_494b_advent_sunday () = check_preface "RG 494(b): Advent I Sunday" Pref.Trinity 2026 11 29
+let test_preface_494b_ordinary_class2_sunday () =
+ check_preface "RG 494(b): an ordinary Time-after-Pentecost Sunday" Pref.Trinity 2026 8 9
+
+(* ---- RG 494(b), the RG 16(a)-shaped fix round: All Saints' Day (1
+ November), [Class1], observed OUTRIGHT over the ordinary Sunday it
+ commemorates -- found via the FIUV Ordo (its own entry reads "Trinit.
+ vel de Omnibus Sanctis et Patronis", the "vel..." half an alternate
+ ORATIO reference, the SAME pattern every other "X vel Y" [praef] value
+ in that fixture already follows, never a second genuine preface). No
+ dedicated preface exists for All Saints among RG 484-497's own
+ fourteen, so RG 482's chain falls to the SUNDAY's own de-Tempore grant
+ regardless of which rank actually won the day -- exactly why this
+ branch must read [temporal]'s season, not [observed]'s [Class1] rank,
+ which an EARLIER version of [preface] wrongly required to equal
+ [Class2] and so answered [Common] here instead.
+
+ 1 November 2026 is itself a Sunday (confirmed via `colitur day 2026`:
+ "all-saints class-1 white +ef-time-after-pentecost-sunday-23"), the
+ exact shape this fix concerns -- picked for that reason, not merely
+ because it is All Saints' Day. *)
+let test_preface_494b_all_saints_class1_sunday () =
+ check_preface "RG 494(b): All Saints' Day, Class1, still Trinity (no dedicated preface exists)" Pref.Trinity
+ 2026 11 1
+
+(* ---- RG 498: the Common residual -- Corpus Christi is the deliberately
+ chosen witness (NOT simply "any ordinary weekday"): the 1962 Missal
+ gives it no preface of its own at all, only an OPTIONAL alternative
+ (Sacred Heart's), so it takes the plain Common, confirmed against the
+ Ordo directly (4 June 2026: "comm. vel de Cor Sacratissimo") -- a real
+ trap this engine's own tier1 list does NOT fall into (no
+ "ef-corpus-christi" entry anywhere in it). ---- *)
+let test_preface_498_corpus_christi () = check_preface "RG 498: Corpus Christi (no proper of its own)" Pref.Common 2026 6 4
+
+let test_preface_498_plain_saint () = check_preface "RG 498: a plain sanctoral saint (Lawrence)" Pref.Common 2026 8 10
+
+(* ---- Corroborating real-data invariants, task requirement 5: "every day
+ in Paschaltide should take the Easter preface unless it has a proper
+ one; every Lenten feria the Lent preface". Checked as a SWEEP, not a
+ single date, over a fixed sample span (1583-2200, matching the century+
+ spans other domain checks in this file already use) -- FAST in the
+ default suite, EXHAUSTIVE (1583-9999) gated behind
+ COLITUR_EXHAUSTIVE_SWEEP the same way {!test_exhaustive_gloria_domain_sweep}
+ above already is.
+
+ The invariant actually checked is NOT the brief's own literal phrasing
+ (which is one exception too strong, the identical "pushed back, not
+ silently special-cased" stance {!check_gloria_invariants_for_year}'s own
+ header already takes for [gloria]/432(b)): every Paschaltide-season day
+ takes [Easter], [Ascension] or [Holy_spirit] (its own three seasonal
+ windows), OR one of the season-independent title triggers this engine
+ can construct inside Paschaltide's real date range ([Bvm]/[St_joseph]/
+ [Apostles]/[Requiem]) -- but NEVER [Nativity]/[Epiphany]/[Lent]/[Trinity]/
+ [Sacred_heart]/[Christ_the_king]/[Common], none of which any real date
+ inside Paschaltide can trigger. Symmetrically for Lent: [Lent] or one of
+ [Bvm]/[St_joseph]/[Apostles]/[Requiem], never a preface belonging to a
+ date-disjoint window. Two further invariants the sweep found and kept,
+ not originally asked for but load-bearing: EVERY Christmastide-season
+ day resolves [Nativity], [Epiphany] or [Bvm] (RG 484/485 between them
+ leave no gap at all in that season on their own -- confirmed
+ exhaustively -- but RG 495's own votive-Mass half, live for the BVM
+ Saturday Office, can and does fall on a Christmastide Saturday too;
+ found live at 1584-01-07 while first running this exact sweep, kept as
+ a real witness rather than narrowed away); and Passiontide, uniquely,
+ legitimately
+ ALSO produces [Easter] exactly once a year (Holy Saturday's own Vigil
+ Mass, [n = -1] -- {!Rite_ef.Rubrics_ef.preface}'s own RG 489(a) comment;
+ this engine's day/colour model keeps that date [Passiontide] by season
+ even though the Vigil Mass's own preface has already moved to Easter's,
+ the same acknowledged per-action nuance [gloria]'s own RG 431(c) comment
+ already documents) -- asserted to be EXACTLY once per year, not merely
+ allowed, so a second, unexplained (Passiontide, Easter) day anywhere in
+ the domain still fails loudly. *)
+let check_preface_season_invariants_for_year y (counts : (int * int * int * int) ref) =
+ let days = Cal.year ctx layer y in
+ let christmastide_gaps = ref 0
+ and paschaltide_gaps = ref 0
+ and lent_gaps = ref 0
+ and passiontide_easter = ref 0 in
+ let christmastide_gaps0, paschaltide_gaps0, lent_gaps0, passiontide_easter0 = !counts in
+ christmastide_gaps := christmastide_gaps0;
+ paschaltide_gaps := paschaltide_gaps0;
+ lent_gaps := lent_gaps0;
+ passiontide_easter := passiontide_easter0;
+ Array.iter
+ (fun (d : (V.season, V.rank) LD.t) ->
+ let label = Printf.sprintf "%s (preface=%s)" (Date.to_iso8601 d.LD.date)
+ (match d.LD.preface with Some p -> Pref.to_string p | None -> "NONE")
+ in
+ (match d.LD.temporal.Temporal.season with
+ | V.Christmastide -> (
+ match d.LD.preface with
+ | Some (Pref.Nativity | Pref.Epiphany | Pref.Bvm) -> ()
+ | _ ->
+ incr christmastide_gaps;
+ Alcotest.failf "%s: Christmastide day with neither Nativity, Epiphany nor Bvm" label)
+ | V.Paschaltide -> (
+ match d.LD.preface with
+ | Some (Pref.Easter | Pref.Ascension | Pref.Holy_spirit | Pref.Bvm | Pref.St_joseph
+ | Pref.Apostles | Pref.Requiem) ->
+ ()
+ | _ ->
+ incr paschaltide_gaps;
+ Alcotest.failf "%s: Paschaltide day with an out-of-window preface" label)
+ | V.Lent -> (
+ match d.LD.preface with
+ | Some (Pref.Lent | Pref.Bvm | Pref.St_joseph | Pref.Apostles | Pref.Requiem) -> ()
+ | _ ->
+ incr lent_gaps;
+ Alcotest.failf "%s: Lent day with an out-of-window preface" label)
+ | V.Passiontide ->
+ if d.LD.preface = Some Pref.Easter then begin
+ incr passiontide_easter;
+ let easter = Computus.gregorian_easter (Date.year d.LD.date) in
+ Alcotest.(check int)
+ (Printf.sprintf "%s: the ONLY Passiontide/Easter day is the Vigil Mass (Easter-1)" label)
+ (-1) (Date.to_rata d.LD.date - Date.to_rata easter)
+ end
+ | _ -> ()))
+ days;
+ counts := (!christmastide_gaps, !paschaltide_gaps, !lent_gaps, !passiontide_easter)
+
+let test_preface_season_invariants_sample () =
+ let counts = ref (0, 0, 0, 0) in
+ for y = 1583 to 1782 do
+ check_preface_season_invariants_for_year y counts
+ done;
+ let _, _, _, passiontide_easter = !counts in
+ Alcotest.(check bool) "the 200-year sample found the once-a-year Passiontide/Easter exception" true
+ (passiontide_easter >= 200)
+
+let test_exhaustive_preface_season_domain_sweep () =
+ if Sys.getenv_opt colitur_exhaustive_sweep_env = None then Alcotest.skip ()
+ else begin
+ let counts = ref (0, 0, 0, 0) in
+ for y = 1583 to 9999 do
+ check_preface_season_invariants_for_year y counts
+ done;
+ let christmastide_gaps, paschaltide_gaps, lent_gaps, passiontide_easter = !counts in
+ Printf.printf
+ "preface season sweep 1583..9999: christmastide_gaps=%d paschaltide_gaps=%d lent_gaps=%d \
+ passiontide_easter=%d\n%!"
+ christmastide_gaps paschaltide_gaps lent_gaps passiontide_easter;
+ Alcotest.(check int) "zero Christmastide gaps anywhere in the domain" 0 christmastide_gaps;
+ Alcotest.(check int) "zero Paschaltide out-of-window prefaces anywhere in the domain" 0 paschaltide_gaps;
+ Alcotest.(check int) "zero Lent out-of-window prefaces anywhere in the domain" 0 lent_gaps;
+ (* 8416, not 8417 (the domain's own year count): a genuine, pre-existing
+ DOMAIN-BOUNDARY edge, not a preface defect -- {!Cal.year}'s own
+ liturgical year "opening in civil year 9999" cannot construct any
+ date past 1583-9999 ({!Date.make}'s own validated range), so it
+ returns only 34 days (28 November-31 December), never reaching its
+ own Easter/Holy Saturday (which would fall in year 10000). Found by
+ running this exact sweep: it returned 8416 first, not asserted
+ blindly at 8417 -- checked directly ([Cal.year ctx layer 9999]
+ alone, printed length 34) before writing this comment, the same
+ "measure before adjudicating" discipline every allow-list in this
+ project follows. The identical edge is why
+ docs/research/rules-register.md's own Layer.index citation already
+ reads "both edges, 1582 and 10000, bit during development" --
+ this sweep is a second witness to the same known boundary, not a
+ new one. *)
+ Alcotest.(check int) "one Passiontide/Easter day per year, every year but the domain's own last" 8416
+ passiontide_easter
+ end
+
(* ---- ITEM 1: RG 111(a), the sung-Mass commemoration cap
({!Colitur_kernel.Precedence.sung_mass_commemorations}) -- two real
calendar days, one of each shape, resolved through the identical
@@ -855,4 +1182,62 @@ let suite =
Alcotest.test_case "RG 111(a): an ordinary-only Low-Mass set is empty at Sung Mass" `Quick
test_rg111a_ordinary_only_dropped;
Alcotest.test_case "RG 111(a): a privileged commemoration survives at Sung Mass" `Quick
- test_rg111a_privileged_kept ] )
+ test_rg111a_privileged_kept;
+ Alcotest.test_case "preface: Good Friday, no Mass" `Quick test_preface_good_friday_no_mass;
+ Alcotest.test_case "RG 499: All Souls' Day (transferred)" `Quick test_preface_499_all_souls;
+ Alcotest.test_case "RG 487(a): the Exaltation of the Holy Cross" `Quick test_preface_487a_exaltation;
+ Alcotest.test_case "RG 487(a): the Most Precious Blood" `Quick test_preface_487a_precious_blood;
+ Alcotest.test_case "RG 491: the Sacred Heart" `Quick test_preface_491_sacred_heart;
+ Alcotest.test_case "RG 492: Christ the King" `Quick test_preface_492_christ_the_king;
+ Alcotest.test_case "RG 490(a): Ascension Day" `Quick test_preface_490a_ascension_day;
+ Alcotest.test_case "RG 494(a): Trinity Sunday" `Quick test_preface_494a_trinity_sunday;
+ Alcotest.test_case "RG 496: St Joseph, Spouse of the BVM" `Quick test_preface_496_joseph_spouse;
+ Alcotest.test_case "RG 496: St Joseph the Workman" `Quick test_preface_496_joseph_workman;
+ Alcotest.test_case "RG 495: the Assumption (marian_slugs)" `Quick test_preface_495_assumption;
+ Alcotest.test_case "RG 495: the BVM Saturday Office (subject=Bvm)" `Quick
+ test_preface_495_bvm_saturday_office;
+ Alcotest.test_case "RG 484(a): the Purification" `Quick test_preface_484a_purification;
+ Alcotest.test_case "RG 484(a): Christmas Day itself" `Quick test_preface_484_nativity_day;
+ Alcotest.test_case "RG 484(b): St Stephen, inside the Nativity octave" `Quick
+ test_preface_484_stephen_in_octave;
+ Alcotest.test_case "RG 484(a): 1 January, the Octave Day" `Quick test_preface_484_jan1_octave_day;
+ Alcotest.test_case "RG 484(b): 3 January, the extra de-Tempore days" `Quick test_preface_484b_jan2to5;
+ Alcotest.test_case "RG 484(b): St John the Evangelist, inside the Nativity octave" `Quick
+ test_preface_484b_overrides_apostle_in_octave;
+ Alcotest.test_case "RG 497: St Barnabas" `Quick test_preface_497_barnabas;
+ Alcotest.test_case "RG 497: Sts Peter & Paul" `Quick test_preface_497_peter_paul;
+ Alcotest.test_case "RG 497: Sts Philip & James, inside the Easter window" `Quick
+ test_preface_497_philip_james_inside_easter_window;
+ Alcotest.test_case "RG 485(a): Epiphany Day" `Quick test_preface_485a_epiphany_day;
+ Alcotest.test_case "RG 485(a): the Commemoration of the Baptism of the Lord" `Quick
+ test_preface_485a_baptism_commemoration;
+ Alcotest.test_case "RG 485(b): Holy Family Sunday, inside the 7-13 January window" `Quick
+ test_preface_485b_holy_family_sunday;
+ Alcotest.test_case "RG 486(a): Ash Wednesday" `Quick test_preface_486_ash_wednesday;
+ Alcotest.test_case "RG 486: the Saturday before Passion Sunday I" `Quick
+ test_preface_486_lent_saturday_boundary;
+ Alcotest.test_case "RG 487: Passion Sunday I" `Quick test_preface_487_passion_sunday;
+ Alcotest.test_case "RG 487: Palm Sunday" `Quick test_preface_487_palm_sunday;
+ Alcotest.test_case "RG 487: Holy Thursday" `Quick test_preface_487_holy_thursday;
+ Alcotest.test_case "RG 489(a): the Easter Vigil Mass" `Quick test_preface_489_easter_vigil_mass;
+ Alcotest.test_case "RG 489(a): Easter Sunday" `Quick test_preface_489_easter_sunday;
+ Alcotest.test_case "RG 489(b): the vigil of the Ascension" `Quick test_preface_489_ascension_vigil;
+ Alcotest.test_case "RG 490(b): the Friday after Ascension" `Quick test_preface_490b_post_ascension_feria;
+ Alcotest.test_case "RG 490(b): the Sunday after Ascension" `Quick
+ test_preface_490b_sunday_after_ascension;
+ Alcotest.test_case "RG 493(a): the vigil of Pentecost" `Quick test_preface_493_pentecost_vigil;
+ Alcotest.test_case "RG 493(a): Pentecost Sunday" `Quick test_preface_493_pentecost_sunday;
+ Alcotest.test_case "RG 493(a): the Ember Saturday within the Pentecost octave" `Quick
+ test_preface_493_ember_saturday;
+ Alcotest.test_case "RG 494(b): Advent I Sunday" `Quick test_preface_494b_advent_sunday;
+ Alcotest.test_case "RG 494(b): an ordinary Time-after-Pentecost Sunday" `Quick
+ test_preface_494b_ordinary_class2_sunday;
+ Alcotest.test_case "RG 494(b): All Saints' Day, Class1, still Trinity" `Quick
+ test_preface_494b_all_saints_class1_sunday;
+ Alcotest.test_case "RG 498: Corpus Christi (no proper of its own)" `Quick test_preface_498_corpus_christi;
+ Alcotest.test_case "RG 498: a plain sanctoral saint (Lawrence)" `Quick test_preface_498_plain_saint;
+ Alcotest.test_case "preface domain sanity: Christmastide/Paschaltide/Lent window invariants (sample)"
+ `Quick test_preface_season_invariants_sample;
+ Alcotest.test_case
+ "preface domain sweep 1583..9999: window invariants, committed not sampled" `Slow
+ test_exhaustive_preface_season_domain_sweep ] )
diff --git a/test/test_validate.ml b/test/test_validate.ml
index b53f915..3da8fbb 100644
--- a/test/test_validate.ml
+++ b/test/test_validate.ml
@@ -324,17 +324,19 @@ module Synthetic = struct
{ Colitur_kernel.Mass_formulary.said = Some (Slug.of_string_exn "syn-formulary");
via = Colitur_kernel.Mass_formulary.Own_slug }
- (* No fixture here exercises the Creed or Gloria rubrics -- a rite that
- has not implemented them returns [false] explicitly,
- {!Rite.t.creed}/{!Rite.t.gloria}'s own documented default. Made
- overridable ([?creed]/[?gloria] below) on the same footing as
- [?readings] just above, for Task 6's own fixtures. *)
+ (* No fixture here exercises the Creed, Gloria or preface rubrics -- a
+ rite that has not implemented them returns [false]/[None] explicitly,
+ {!Rite.t.creed}/{!Rite.t.gloria}/{!Rite.t.preface}'s own documented
+ default. Made overridable ([?creed]/[?gloria]/[?preface] below) on
+ the same footing as [?readings] just above, for Task 6's own
+ fixtures. *)
let creed ~temporal:_ ~observed:_ ~date:_ = false
let gloria ~temporal:_ ~observed:_ ~date:_ = false
+ let preface ~temporal:_ ~observed:_ ~date:_ = None
let rite ?(vocab = vocab) ?(anchors = fun _ -> []) ?(season_runs = [ A; B ]) ?(rules = rules)
?(transfer_target = fun _ origin _ -> origin) ?(readings = readings) ?(creed = creed)
- ?(gloria = gloria) temporal : (season, rank) Rite.t =
+ ?(gloria = gloria) ?(preface = preface) temporal : (season, rank) Rite.t =
{ Rite.id = "synthetic"; vocab; year_start; temporal; anchors; rules; season_runs;
(* Not a Roman rite, but a Rite.t must supply SOME Easter now that
movable Date_spec variants exist. The Gregorian one is as good as
@@ -344,7 +346,7 @@ module Synthetic = struct
(* Not a Roman rite either, so no bissextile-doubling convention:
identity, {!Rite.t.fixed_key}'s own documented default. *)
fixed_key = (fun d -> Some (D.month d, D.day d));
- transfer_target; readings; creed; gloria }
+ transfer_target; readings; creed; gloria; preface }
(* Empty by default: every check built before Task 12 exercises the
TEMPORAL-only pass, where an empty layer is exactly the fixture that