aboutsummaryrefslogtreecommitdiff
path: root/test/test_rubrics_ef.ml
diff options
context:
space:
mode:
Diffstat (limited to 'test/test_rubrics_ef.ml')
-rw-r--r--test/test_rubrics_ef.ml1312
1 files changed, 1312 insertions, 0 deletions
diff --git a/test/test_rubrics_ef.ml b/test/test_rubrics_ef.ml
new file mode 100644
index 0000000..77654c0
--- /dev/null
+++ b/test/test_rubrics_ef.ml
@@ -0,0 +1,1312 @@
+(* RG 475-476, the Creed -- 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 clause of 475, plus a 476 negative, resolved
+ against REAL calendar dates through the shipped sanctoral data (the same
+ pipeline `colitur day`/`colitur rubrics` use) -- every expected value
+ below was derived from the rubric's own text and checked against
+ `colitur day <year>`'s real output (slug/rank/subject/weekday) BEFORE
+ this module existed, never read off [Rubrics_ef.creed]'s own answer.
+ Two synthetic unit tests isolate Trap One (RG 475(a) reads [temporal],
+ never [observed]) directly, without depending on finding a real-calendar
+ coincidence.
+
+ One further test (celebrant-rubrics-phase1 review, finding 1) is
+ different in kind from the rest: not an end-to-end [creed] check
+ against a single real date, but a direct sweep of {!Rite_ef.Temporal_ef}
+ output across a century, asserting the unstated invariant [creed]'s own
+ RG 24/25 ferial branches rely on rather than leaving it asserted only
+ in prose -- see its own header comment for the full account. *)
+
+module Cal = Colitur_kernel.Calendar
+module LD = Colitur_kernel.Liturgical_day
+module Date = Colitur_kernel.Date
+module Cel = Colitur_kernel.Celebration
+module Colour = Colitur_kernel.Colour
+module Subject = Colitur_kernel.Subject
+module Slug = Colitur_kernel.Slug
+module Temporal = Colitur_kernel.Temporal
+module Computus = Colitur_kernel.Computus
+module V = Rite_ef.Vocab_ef
+module RE = Rite_ef.Rubrics_ef
+module TE = Rite_ef.Temporal_ef
+
+let mk y m d = match Date.make ~year:y ~month:m ~day:d with Ok t -> t | Error e -> failwith e
+
+(* Loaded once, module-level: every test below is a lookup against the same
+ shipped calendar, and {!Cal.day} recomputes its whole liturgical year on
+ every call (calendar.mli's own documented cost), so at minimum the layer
+ itself should not be reloaded and re-merged per test case. *)
+let layer =
+ match Test_support.load_ef_layer () with Ok l -> l | Error e -> Alcotest.failf "%s" e
+
+let ctx = Test_support.ef_context ()
+
+let creed_on y m d = (Cal.day ctx layer (mk y m d)).LD.creed
+
+let check name y m d expected = Alcotest.(check bool) name expected (creed_on y m d)
+
+(* ------------------------------------------------------------------------ *)
+(* Breviary 237-238 (Te Deum) and RG 431-432 (Gloria) -- Phase 2 of the *)
+(* celebrant-rubrics-phase1 design. See lib/rites/rite_ef/rubrics_ef.ml for *)
+(* both rubrics quoted in full and every branch's own citation. One *)
+(* end-to-end test per clause, resolved against REAL calendar dates through *)
+(* the shipped data -- every expected value below was read off *)
+(* `colitur rubrics <year>`'s real output BEFORE being pinned here, the *)
+(* same discipline the Creed tests above already establish. *)
+
+let day_on y m d = Cal.day ctx layer (mk y m d)
+let gloria_on y m d = (day_on y m d).LD.gloria
+let te_deum_on y m d = RE.te_deum ~temporal:(day_on y m d).LD.temporal ~observed:(day_on y m d).LD.observed
+ ~date:(mk y m d)
+
+let check_gloria name y m d expected = Alcotest.(check bool) name expected (gloria_on y m d)
+let check_te_deum name y m d expected = Alcotest.(check bool) name expected (te_deum_on y m d)
+
+(* ---- RG 475(a): "in qualibet dominica, etsi eius Officium alicui festo
+ locum cedat" ---- *)
+
+let test_475a_ordinary_sunday () =
+ (* 2026-01-25: an ordinary Time-after-Epiphany Sunday, Class2, green,
+ "Dominica III post Epiphaniam" -- confirmed via `colitur day 2026`,
+ no other clause of 475 could apply (Class2, subject Temporal, no
+ octave, no apostle/vigil slug), so [true] here can only come from
+ 475(a) itself. *)
+ check "475(a): an ordinary Sunday" 2026 1 25 true
+
+(* Trap One, isolated directly: RG 475(a)'s own "ETSI EIUS OFFICIUM ALICUI
+ FESTO LOCUM CEDAT" -- even when a feast has displaced the Sunday's own
+ office, the Creed is still said. [observed] below is deliberately shaped
+ so that NONE of 475(b)/(c)/(e) can produce [true] on their own (Class3,
+ subject Saint, a slug on no list this module knows); the only way
+ [creed] can return [true] is by reading [temporal]'s own [weekday],
+ never [observed]. The synthetic [date] (an ordinary July day, itself a
+ Wednesday in 2026) is chosen so nothing about the DATE itself suggests
+ a Sunday either -- proving the function reads [temporal.weekday], not
+ [Date.weekday date]. *)
+let impeded_observed =
+ Cel.make ~slug:(Slug.of_string_exn "some-impeding-feast-of-the-lord") ~rank:V.Class3
+ ~status:Cel.Feast ~colour:Colour.Red ~subject:Subject.Saint ~layer:"synthetic" ()
+
+let synthetic_temporal ~weekday : (V.season, V.rank) Temporal.t =
+ { Temporal.season = V.Time_after_pentecost; week = Some 1; weekday; office = impeded_observed }
+
+let test_475a_reads_temporal_not_observed () =
+ Alcotest.(check bool) "Sunday-shaped [temporal] overrides a non-Sunday-shaped [observed]" true
+ (RE.creed ~temporal:(synthetic_temporal ~weekday:Date.Sun) ~observed:impeded_observed
+ ~date:(mk 2026 7 1));
+ Alcotest.(check bool) "same [observed], non-Sunday [temporal]: false" false
+ (RE.creed ~temporal:(synthetic_temporal ~weekday:Date.Wed) ~observed:impeded_observed
+ ~date:(mk 2026 7 1))
+
+(* ---- RG 475(b): "in festis I classis" ---- *)
+
+let test_475b_class1_feast () =
+ (* 2026-08-15: the Assumption, Class1. *)
+ check "475(b): a I-class feast" 2026 8 15 true
+
+(* ---- RG 475(c): "in festis II classis Domini et B. Mariae Virg." ---- *)
+
+let test_475c_lord () =
+ (* 2026-09-14: Exaltation of the Holy Cross, Class2, subject Lord. *)
+ check "475(c): a II-class feast of the Lord" 2026 9 14 true
+
+let test_475c_bvm_via_marian_slugs () =
+ (* 2026-08-22: Immaculate Heart of Mary, Class2 -- ships [subject =
+ Saint] in data/ef/sanctoral.sexp (confirmed by grep), so this can only
+ come out [true] via {!Rite_ef.Precedence_ef.marian_slugs}, not via
+ [subject = Bvm]. Also carries a real commemoration
+ (+sts-timothy-hippolytus-and-symphorianus-martyrs), a live instance of
+ 476(e): the commemoration plays no part in this answer. *)
+ check "475(c): a II-class BVM feast (via marian_slugs, subject=Saint in the data)" 2026 8 22 true
+
+(* ---- RG 475(d): "per octavas Nativitatis Domini, Paschatis et
+ Pentecostes, etiam in festis occurrentibus et in Missis votivis" ---- *)
+
+let test_475d_octave_even_occurring_feast () =
+ (* 2026-12-26: St Stephen, Class2, "S. Stephani Protomartyris" -- a real
+ saint's feast OCCURRING within the Octave of the Nativity (RG 67's own
+ "Com. octavae Nativitatis", carried as +ef-nativity-octave-day-2 in
+ colitur's own commemoration). RG 475(d)'s own "etiam in festis
+ occurrentibus" is written for exactly this shape: the Creed is said
+ regardless. (This is the one place this suite deliberately diverges
+ from the task brief's own worked example, which expected [false] here
+ -- the brief mis-cited 26 December as 476(b)'s "plain II-class feast"
+ case, missing that RG 475(d) explicitly overrides 476(b) inside the
+ Nativity octave; see the task report.) *)
+ check "475(d): a saint's feast occurring within the Nativity octave" 2026 12 26 true
+
+let test_475d_octave_day_boundary () =
+ (* 2026-01-01: the Octave Day of the Nativity itself (Circumcision),
+ Class1 -- also [true] via 475(b) alone, kept as a boundary check that
+ 1 January is correctly included in the 8-day window. *)
+ check "475(d): 1 January, the Octave Day of the Nativity" 2026 1 1 true
+
+(* ---- RG 475(e): "in festis nataliciis Apostolorum et Evangelistarum,
+ necnon in festis Cathedrae S. Petri et S. Barnabae Ap." ---- *)
+
+let test_475e_apostle_natalicium () =
+ (* 2026-11-30: St Andrew, Class2, subject Saint -- not covered by 475(c)
+ (not Domini/BVM), so [true] here can only come from 475(e)'s own
+ natalicia list. Also carries a real commemoration
+ (+ef-advent-1-monday), another live 476(e) instance. *)
+ check "475(e): an Apostle's own natalicium (Andrew)" 2026 11 30 true
+
+let test_475e_barnabas_named_explicitly () =
+ (* 2026-06-11: St Barnabas, Class3 -- named explicitly by the clause
+ ("S. Barnabae Ap."); at Class3 it could not reach [true] via 475(b) or
+ (c) regardless. *)
+ check "475(e): St Barnabas, named explicitly" 2026 6 11 true
+
+let test_475e_chair_of_peter_named_explicitly () =
+ (* 2027-02-22 (NOT 2026: Feb 22 2026 is impeded by Lent I Sunday, so the
+ Chair is not observed that year -- checked via `colitur day 2026`
+ before picking 2027 instead): the Chair of St Peter, Class2, subject
+ Saint -- NOT a natalicium (Peter's own is 29 June, shared with Paul),
+ so [true] here can only come from the clause's own explicit "Cathedrae
+ S. Petri" naming, not from the natalicium reading in general. *)
+ check "475(e): the Chair of St Peter, named explicitly (not a natalicium)" 2027 2 22 true
+
+let test_475e_excludes_conversion_of_paul () =
+ (* Trap Two, directly: 2027-01-25, the Conversion of St Paul, Class3 --
+ names an Apostle but is NOT his natalicium (his own is 29 June, with
+ Peter); Class3 rules out 475(b)/(c), and this slug is deliberately
+ absent from [creed_apostle_slugs]. Picked 2027 for the same impeded-
+ Sunday reason as the Chair of Peter above (25 January 2026 is itself a
+ Sunday). *)
+ check "475(e) does NOT cover the Conversion of St Paul (not a natalicium)" 2027 1 25 false
+
+(* ---- RG 23 (feriae) / RG 476(a): Ash Wednesday, Holy Week's own feriae,
+ the Chrism/Lord's Supper Mass, the Easter Vigil Mass ---- *)
+
+let test_ash_wednesday_no_creed () =
+ check "RG 23(a)/476: Ash Wednesday, a I-class FERIA, not a festum" 2026 2 18 false
+
+let test_holy_thursday_no_creed () =
+ (* 2026-04-02: "Feria V in Cena Domini" -- RG 23(b)'s own "omnes feriae
+ Hebdomadae sanctae"; also explicitly named by 476(a) ("sive... in
+ Cena Domini"). *)
+ check "RG 23(b)/476(a): Holy Thursday (Mass of the Lord's Supper)" 2026 4 2 false
+
+let test_holy_saturday_easter_vigil_no_creed () =
+ (* 2026-04-04: "Sabbato sanctum" -- RG 23(b) again; also explicitly named
+ by 476(a) ("in Missa Vigiliae paschalis"). *)
+ check "RG 23(b)/476(a): Holy Saturday (the Easter Vigil Mass)" 2026 4 4 false
+
+(* ---- RG 476(f): "in Missis defunctorum" -- the Creed is never said at a
+ Requiem Mass. Whole-branch review finding 1: colitur used to say [true]
+ here on rank alone (475(b), Class1) -- the LMS Ordo layer's own former
+ allow-list entry L1, now closed, was this exact day. See rubrics_ef.ml's
+ own header for why {!Colour.Black} is a sound RG 117 proxy for "this Mass
+ is a Requiem" on the shipped data. ---- *)
+
+let test_476f_all_souls_no_creed () =
+ (* 2025-11-03: All Souls' Day (2 November, a Sunday that year, transferred
+ to the next open day, RG's own Class1/black office unchanged by the
+ transfer) -- `colitur day 2025` confirms class-1/black/
+ commemoration-of-all-souls on this date. Before this fix [creed] here
+ was [true] via 475(b); the Ordo's own text ("No Cr") says otherwise. *)
+ check "RG 476(f): All Souls' Day (transferred), a Requiem, says no Creed" 2025 11 3 false
+
+let test_476f_good_friday_no_creed () =
+ (* 2026-04-03: Good Friday, [colour = Black] (temporal_ef.ml's own
+ [days_between easter d = -2] branch) -- already [false] via the RG
+ 23(b) feria-I-classis exclusion checked earlier in [creed], so this is
+ a belt-and-braces witness that the 476(f) guard, were it ever reached
+ first, would agree rather than a case that depends on it: Good Friday
+ has no Mass in the 1955-restored Holy Week to begin with. *)
+ check "RG 476(f)/RG 23(b): Good Friday, black, says no Creed" 2026 4 3 false
+
+(* Whole-branch review finding 1's own instruction: a test that FAILS if a
+ third {!Colour.Black} celebration is ever introduced, so the RG 476(f)
+ guard's colour proxy cannot silently rot into covering the wrong
+ population. Two independent checks, matching the two places a black
+ celebration could be added:
+
+ DATA: every entry the shipped sanctoral layer (sanctoral.sexp, with
+ adjustments.sexp merged on top -- the same [layer] every other test in
+ this file resolves against) carries directly, fixed AND movable alike
+ ({!Colitur_kernel.Layer.entries} holds the raw [Date_spec.t], not a
+ resolved index, so this needs no date walk at all to cover every civil
+ year).
+
+ CODE: {!Rite_ef.Temporal_ef.temporal}'s own colour logic, which
+ sanctoral.sexp cannot see (Good Friday's black is computed, not data) --
+ swept across a century the same span and for the same coverage reason
+ {!test_ferial_origin_never_carries_lord_bvm_or_apostle_slug} above
+ already uses, since a Colour.Black bug introduced into a rare branch
+ (an Ember day, Rogation Wednesday...) would not show up in one year. *)
+let test_colour_black_population_is_exactly_two () =
+ let black_data_entries =
+ List.filter
+ (fun (e : V.rank Colitur_kernel.Layer.entry) -> e.Colitur_kernel.Layer.cel.Cel.colour = Colour.Black)
+ layer.Colitur_kernel.Layer.entries
+ in
+ Alcotest.(check int) "exactly one Colour.Black entry in the shipped data" 1
+ (List.length black_data_entries);
+ (match black_data_entries with
+ | [ e ] ->
+ Alcotest.(check string) "the one Colour.Black data entry is All Souls" "commemoration-of-all-souls"
+ (Slug.to_string e.Colitur_kernel.Layer.cel.Cel.slug)
+ | _ -> ());
+ let code_black = ref [] in
+ for y = 1583 to 1682 do
+ let dec31 = mk y 12 31 in
+ let d = ref (mk y 1 1) in
+ while Date.compare !d dec31 <= 0 do
+ let t = TE.temporal !d in
+ if t.Temporal.office.Cel.colour = Colour.Black then code_black := (y, !d, t) :: !code_black;
+ d := Date.add_days !d 1
+ done
+ done;
+ Alcotest.(check bool) "the century sweep found at least 100 Colour.Black temporal days" true
+ (List.length !code_black >= 100);
+ List.iter
+ (fun (_, d, (t : (V.season, V.rank) Temporal.t)) ->
+ let easter = Computus.gregorian_easter (Date.year d) in
+ Alcotest.(check int)
+ (Printf.sprintf "%s: the only code-side Colour.Black day is Good Friday (Easter-2)"
+ (Date.to_iso8601 d))
+ (-2) (Date.to_rata d - Date.to_rata easter);
+ Alcotest.(check string)
+ (Printf.sprintf "%s: Good Friday's own slug" (Date.to_iso8601 d))
+ "ef-passiontide-2-friday"
+ (Slug.to_string t.Temporal.office.Cel.slug))
+ !code_black
+
+(* ---- RG 476(b) negative: a plain II-class saint, not Domini/BVM, not an
+ Apostle/Evangelist ---- *)
+
+let test_476b_plain_class2_saint () =
+ (* 2026-08-10: St Lawrence, Class2, subject Saint -- a deacon and martyr,
+ no Apostle/Evangelist connection, not on any list this module reads. *)
+ check "476(b): a plain II-class saint (Lawrence) does not say the Creed" 2026 8 10 false
+
+(* ---- RG 28-34 (vigils): checked ahead of 475(c) so a Class2 vigil that
+ is ALSO on marian_slugs is still excluded ---- *)
+
+let test_vigil_excluded_even_when_class2_and_marian () =
+ (* 2026-08-14: Vigil of the Assumption, Class2 -- IS on
+ {!Rite_ef.Precedence_ef.marian_slugs} (a real Marian entry), so without
+ the vigil check ahead of 475(c) this would wrongly come out [true].
+ Also carries a real commemoration (+eusebius-confessor), a second live
+ 476(e) instance. *)
+ check "vigils are excluded even when Class2 and Marian (Vigil of the Assumption)" 2026 8 14 false
+
+(* ---- RG 476(d): a IV-class office (also exercises RG 78/309(a)'s votive
+ Office of the BVM on Saturday, itself IV class) ---- *)
+
+let test_476d_bvm_saturday_office () =
+ (* 2026-07-11: the unoccupied Saturday's Office of Our Lady, Class4 --
+ already a pinned example in test/cli.t (Task 4). *)
+ check "476(d): the BVM Saturday Office, IV class" 2026 7 11 false
+
+(* One full civil year, walked day by day: RG 475(a)'s own invariant, "every
+ Sunday says the Creed, no exceptions" -- the same sanity check the task
+ asks for at the domain-measurement step, pinned here as a real assertion
+ rather than left to a one-off shell scan. *)
+let test_every_sunday_in_2026_says_the_creed () =
+ let days = Cal.year ctx layer 2026 in
+ Array.iter
+ (fun (d : (V.season, V.rank) LD.t) ->
+ if d.LD.temporal.Temporal.weekday = Date.Sun then
+ Alcotest.(check bool)
+ (Printf.sprintf "%s is a Sunday: creed must be true" (Date.to_iso8601 d.LD.date))
+ true d.LD.creed)
+ days
+
+(* ---- Review finding 1 (celebrant-rubrics-phase1 fix task): RG 24/25's
+ feriae II and III classis (the Advent ferias 17-23 December, the
+ Advent/Lent/September Ember sets, and the numbered Lenten/Passiontide
+ ferias) are excluded from the Creed by NO check in rubrics_ef.ml -- see
+ that file's own RG 23 comment (the [n = -46 ...] branch) for the full
+ account. [creed]'s correct [false] answer for them rests entirely on an
+ unstated property of {!Rite_ef.Temporal_ef}: no ferial-origin office it
+ ever builds carries [subject = Lord] or [subject = Bvm] at [Class2]
+ (RG 475(c)'s own rank guard), and none carries a slug on
+ [RE.creed_apostle_slugs] (RG 475(e), which has no rank guard at all).
+ This test asserts that property directly against real Temporal_ef
+ output, rather than leaving it asserted only in prose, so a future
+ Temporal_ef change that breaks it fails HERE loudly instead of silently
+ changing the Creed.
+
+ Two real, harmless, already-documented exceptions are excluded from the
+ [subject] checks rather than papered over: the Sacred Triduum
+ ([subject = Lord], [Class1] -- verified in temporal_ef.ml's own
+ [triduum_names] branch) and the votive Office of the BVM on Saturday,
+ RG 91 entry 27 ([subject = Bvm], [Class4] -- {!TE}'s own [is_bvm_saturday]
+ citation). Neither can ever reach [creed]'s 475(c) branch: the Triduum
+ is caught earlier by the explicit feria-I-classis check (its own
+ [Class1] is not [Class2]), and the BVM Saturday Office is [Class4], not
+ [Class2], so 475(c)'s own rank guard excludes it regardless of subject.
+ Excluding them here is not a loophole in the test -- it is the precise
+ boundary of what RG 475(c) actually reads, confirmed rather than
+ assumed by restricting the [Lord]/[Bvm] checks to exactly [Class2] (RG
+ 24's own II-class population) and [Class3] (RG 25's III-class
+ population), the two ranks a real feria can carry that are NOT already
+ handled by an explicit branch or a rank mismatch. The apostle-slug
+ check has no such carve-out: it is asserted for every ferial-origin day
+ of every rank, matching 475(e)'s own unguarded text.
+
+ A THIRD exception -- not anticipated when this test was first written,
+ found by actually running it, not by inspection -- had to be excluded
+ from the POPULATION itself, not from the subject checks: RG 17(a)'s own
+ "secus die 2 ianuarii" fallback (2 January, only in a civil year with
+ no 2-5 January Sunday) is a genuine FEAST, [subject = Lord], [Class2],
+ sitting outside {!TE.named}'s table for a purely architectural reason
+ (temporal_ef.ml's own citation: that table's shape cannot express a
+ fallback conditional on a per-year fact) and not always a Sunday
+ either, so neither of [is_ferial_origin]'s two filters caught it on the
+ first run -- it failed on 1584-01-02 before {!is_holy_name_fallback}
+ below was added. Left in as evidence the sweep has real teeth rather
+ than trimmed from the span: excluding a genuine mistaken positive by
+ name, once found, is the correct fix; narrowing the span to dodge it
+ would not have been.
+
+ A day is "ferial-origin" here iff {!TE.named} returns [None] for it
+ (nothing in the fixed/movable table of named feasts, vigils and octave
+ days claims it), it is not a Sunday (RG 21's own definition: "Nomine
+ feriae intelleguntur singuli dies hebdomadae, praeter dominicam" --
+ every day of the week EXCEPT Sunday), and it is not the RG 17(a)
+ fallback date just above. *)
+let contains_substring ~needle haystack =
+ let nl = String.length needle and hl = String.length haystack in
+ let rec loop i = (i + nl <= hl) && (String.sub haystack i nl = needle || loop (i + 1)) in
+ nl = 0 || loop 0
+
+(* RG 17(a)'s own "secus die 2 ianuarii" fallback (temporal_ef.ml's own
+ citation on [holy_name_fallback_date]): a genuine FEAST, [subject =
+ Lord], [Class2] -- legitimately outside {!TE.named}'s table only for an
+ architectural reason (that table's bare [Date.t -> ... option] shape
+ cannot express a fallback conditional on a per-year fact,
+ [holy_name_sunday y = None]), never a ferial one, and it is not always
+ a Sunday, so the weekday filter below does not catch it either. Found
+ by running this test before this exclusion existed: it failed on
+ 1584-01-02, a real year with no 2-5 January Sunday. *)
+let is_holy_name_fallback (d : Date.t) =
+ let y = Date.year d in
+ Date.compare d (TE.holy_name_fallback_date y) = 0 && TE.holy_name_sunday y = None
+
+let is_ferial_origin (d : Date.t) =
+ TE.named d = None && Date.weekday d <> Date.Sun && not (is_holy_name_fallback d)
+
+(* Span: civil years 1583-1682, the domain's own opening century -- every
+ RG 24/25 population recurs every civil year (Advent 17-23, all three
+ non-Whitsun Ember sets, and Lenten/Passiontide ferias are none of them
+ rare or conditional), so one year already gives full COVERAGE; a
+ century instead of one is for confidence the invariant is not an
+ accident of a single Easter date, at a cost (day-by-day calls to the
+ pure {!TE.temporal}, no Calendar/Precedence resolution) cheap enough to
+ stay in the default, unsampled suite. 1583 is also the domain's own
+ lower bound, and this span contains 1598, the earliest possible Easter
+ (22 March) in [1583, 2500] per {!test_easter_extremes} above -- a real,
+ not merely nominal, edge case. Coverage is VERIFIED below via counters,
+ not assumed: a test that never actually walks an Ember day would prove
+ nothing about Ember days. *)
+let test_ferial_origin_never_carries_lord_bvm_or_apostle_slug () =
+ let total = ref 0
+ and advent_ember = ref 0
+ and lent_ember = ref 0
+ and september_ember = ref 0
+ and advent_17_23 = ref 0
+ and lenten_passiontide_class3 = ref 0 in
+ for y = 1583 to 1682 do
+ let dec31 = mk y 12 31 in
+ let d = ref (mk y 1 1) in
+ while Date.compare !d dec31 <= 0 do
+ (if is_ferial_origin !d then begin
+ let t = TE.temporal !d in
+ incr total;
+ let office = t.Temporal.office in
+ let slug = Slug.to_string office.Cel.slug in
+ let label = Printf.sprintf "%s (%s, rank=%s)" (Date.to_iso8601 !d) slug
+ (match office.Cel.rank with
+ | V.Class1 -> "1"
+ | V.Class2 -> "2"
+ | V.Class3 -> "3"
+ | V.Class4 -> "4")
+ in
+ (* Coverage tallies -- checked against zero below. *)
+ if contains_substring ~needle:"advent-ember" slug then incr advent_ember;
+ if contains_substring ~needle:"lent-ember" slug then incr lent_ember;
+ if contains_substring ~needle:"september-ember" slug then incr september_ember;
+ if Date.month !d = 12 && Date.day !d >= 17 && Date.day !d <= 23 then incr advent_17_23;
+ if
+ office.Cel.rank = V.Class3
+ && (t.Temporal.season = V.Lent || t.Temporal.season = V.Passiontide)
+ then incr lenten_passiontide_class3;
+ (* The invariant itself. Apostle slugs: every rank, no carve-out. *)
+ Alcotest.(check bool) (label ^ ": not on the apostle-natalicium slug list") false
+ (List.mem slug RE.creed_apostle_slugs);
+ (* Lord/Bvm subject: only at the two ranks a real RG 24/25 feria
+ can carry -- see this test's own header for why Class1
+ (Triduum) and Class4 (BVM Saturday) are deliberately excluded. *)
+ if office.Cel.rank = V.Class2 || office.Cel.rank = V.Class3 then begin
+ Alcotest.(check bool) (label ^ ": not subject Lord") false (office.Cel.subject = Subject.Lord);
+ Alcotest.(check bool) (label ^ ": not subject Bvm") false (office.Cel.subject = Subject.Bvm)
+ end
+ end);
+ d := Date.add_days !d 1
+ done
+ done;
+ Alcotest.(check bool) "span examined a real number of ferial-origin days" true (!total > 10_000);
+ Alcotest.(check bool) "span reached Advent Ember days" true (!advent_ember > 0);
+ Alcotest.(check bool) "span reached Lent Ember days" true (!lent_ember > 0);
+ Alcotest.(check bool) "span reached September Ember days" true (!september_ember > 0);
+ Alcotest.(check bool) "span reached Advent 17-23 ferias" true (!advent_17_23 > 0);
+ Alcotest.(check bool) "span reached Lenten/Passiontide III-class ferias" true
+ (!lenten_passiontide_class3 > 0)
+
+(* ---- 237(a): the three named Paschaltide days ---- *)
+
+let test_237a_easter_sunday () = check_te_deum "237(a): Easter Sunday" 2026 4 5 true
+let test_237a_low_sunday () = check_te_deum "237(a): Low Sunday" 2026 4 12 true
+let test_237a_pentecost () = check_te_deum "237(a): Pentecost Sunday" 2026 5 24 true
+
+(* ---- 237(f): the vigils of Ascension and Pentecost, both otherwise
+ reachable by 238(b) (the Ascension vigil is Class2) or moot to it
+ (the Pentecost vigil is Class1) ---- *)
+
+let test_237f_ascension_vigil () = check_te_deum "237(f): Vigil of the Ascension" 2026 5 13 true
+let test_237f_pentecost_vigil () = check_te_deum "237(f): Vigil of Pentecost" 2026 5 23 true
+
+(* ---- 237(d): the three privileged octaves, reusing [creed]'s own windows
+ -- a day within the Nativity octave whose OWN office is a real saint
+ (RG 67's "Com. octavae Nativitatis"), and an Easter-week feria outside
+ the narrower 237(a) list ---- *)
+
+let test_237d_nativity_octave_saint () =
+ check_te_deum "237(d): St John within the Nativity octave" 2026 12 27 true
+
+let test_237d_easter_week_feria () = check_te_deum "237(d): Easter Tuesday" 2026 4 7 true
+
+(* ---- 238(b): the omissible (Class2/Class3) vigils -- St Lawrence's own,
+ picked in a year where nothing displaces it, confirmed via
+ `colitur day 2026` first the same way the Creed tests were derived ---- *)
+
+let test_238b_omissible_vigil () =
+ (* 9 August 2026 is itself a Sunday (the vigil impeded, see 237(b)'s own
+ test below, which reuses that exact date) -- 2027 is picked instead,
+ confirmed via `colitur day 2027` first, the same discipline every
+ other date in this file follows. *)
+ check_te_deum "238(b): Vigil of St Lawrence" 2027 8 9 false
+
+(* ---- The Nativity Vigil (I class): NOT literally named by 238(b)'s own
+ "II et III classis" text -- excluded here on the structural RG 21/35
+ taxonomy inference [te_deum]'s own comment states explicitly as an
+ inference, not a citation. Flagged the same way in the task report;
+ checked against the FIUV Ordo's own Te Deum marker for 24 December in
+ test_fiuv_ordo.ml, which is this inference's real corroboration. ---- *)
+
+let test_nativity_vigil_excluded_by_inference () =
+ check_te_deum "Nativity Vigil (I class): excluded, inference not literal 238(b)" 2026 12 24 false
+
+(* ---- 238(c)/RG23: Ash Wednesday and Good Friday (moot -- no Mass, but the
+ RG23 feria-I-classis exclusion still answers [false] regardless of
+ colour) ---- *)
+
+let test_238c_ash_wednesday () = check_te_deum "238(c)/RG23: Ash Wednesday" 2026 2 18 false
+let test_238c_good_friday () = check_te_deum "238(c)/RG23: Good Friday" 2026 4 3 false
+
+(* ---- 237(g): the votive Office of the BVM on Saturday -- reused directly
+ from test/cli.t's own pinned example ---- *)
+
+let test_237g_bvm_saturday () = check_te_deum "237(g): BVM Saturday Office" 2026 7 11 true
+
+(* ---- 237(e): a plain, non-octave, non-Sunday feria of Christmastide (2-5
+ January) ---- *)
+
+let test_237e_christmastide_feria () =
+ check_te_deum "237(e): 2 January, a Christmastide feria" 2026 1 2 true
+
+(* ---- 237(b): an ordinary II-class Sunday outside Septuagesima, and its
+ own explicit exception (a Septuagesima/Sexagesima Sunday, [Class2],
+ excepted by name) ---- *)
+
+let test_237b_ordinary_class2_sunday () =
+ check_te_deum "237(b): an ordinary Time-after-Pentecost Sunday" 2026 8 9 true
+
+(* RE-VERIFIED against the CORRECTED FIUV extraction ([te_deum]'s own
+ 237(a) comment has the full account of the extractor bug an earlier
+ pass of this task found and fixed): 237(b)'s own literal "exceptis
+ dominicis in Septuagesima, in Sexagesima et in Quinquagesima" holds
+ after all -- these two Sundays do NOT say the Te Deum, confirmed
+ against the corrected data, not merely the original transcription
+ alone. *)
+let test_237b_septuagesima_exception () =
+ check_te_deum "237(b)'s own exception: Septuagesima Sunday" 2027 1 24 false
+
+let test_237b_sexagesima_exception () =
+ check_te_deum "237(b)'s own exception: Sexagesima Sunday" 2027 1 31 false
+
+(* ---- 237(c): a genuine sanctoral feast kept during a penitential season
+ (St Paul of the Cross, Class3, outside any privileged window) and one in
+ Ordinary Time (Lawrence, Class2) ---- *)
+
+let test_237c_sanctoral_feast () = check_te_deum "237(c): a sanctoral feast (Lawrence)" 2026 8 10 true
+
+(* Found alongside the FIUV extractor-bug fix ([te_deum]'s own 237(c)
+ comment has the full account): Passion Sunday and Palm Sunday are BOTH
+ entries in {!Temporal_ef.named} (named individually for RG 91 entry 6),
+ which without the [temporal.weekday <> Sun] guard on 237(c)'s own
+ [named<>None] disjunct would wrongly grant them [true] by ACCIDENT of
+ table membership -- neither is a genuine "festum" (RG 35's own "dies
+ dominica" is its own category), and both are [Class1] Sundays 237(b)'s
+ own [Class2] guard already excludes. Confirmed [false] directly against
+ the corrected FIUV extraction. *)
+let test_passion_sunday_not_a_festum () = check_te_deum "Passion Sunday is not a festum for 237(c)" 2026 3 22 false
+let test_palm_sunday_not_a_festum () = check_te_deum "Palm Sunday is not a festum for 237(c)" 2026 3 29 false
+
+(* ---- 238(d): the Requiem proxy, shared with [creed]'s own 476(f) and the
+ SAME two-member {!Colour.Black} population
+ {!test_colour_black_population_is_exactly_two} already asserts ---- *)
+
+let test_238d_all_souls () = check_te_deum "238(d): All Souls' Day (transferred)" 2025 11 3 false
+
+(* ---- RG 431(c): Holy Thursday and the Easter Vigil Mass say the Gloria
+ even though neither day's own Matins says the Te Deum (both are
+ feria-I-classis, [te_deum]'s own 238(c) branch) -- the one place [gloria]
+ and [te_deum] genuinely disagree on real data. Holy Saturday also proves
+ 431(c) outranks 432(b): its own colour is [Violet]
+ (Passiontide's [season_colour]), yet the Gloria is still said. ---- *)
+
+let test_431c_holy_thursday () = check_gloria "431(c): Holy Thursday" 2026 4 2 true
+let test_431c_easter_vigil () = check_gloria "431(c): the Easter Vigil Mass (Holy Saturday)" 2026 4 4 true
+
+(* ---- RG 432(b): violet vestments -- independent of [te_deum], checked
+ against an Advent Sunday (I class, so [te_deum] would ALSO answer
+ [false] here via its own 237(b) rank guard: this is a same-answer
+ witness, not proof of independent teeth -- see the task report for why
+ no fully independent (te_deum=true, violet) witness exists anywhere in
+ the shipped 1583-9999 domain: every real [Colour.Violet] [Feast] entry
+ is one of the five sanctoral vigils, already excluded by [te_deum]'s
+ own [is_vigil] branch either way). ---- *)
+
+let test_432b_violet_sunday () = check_gloria "432(b): Advent I Sunday, violet" 2026 11 29 false
+
+(* ---- RG 432(d): the Requiem proxy, same population as 238(d) above ---- *)
+
+let test_432d_all_souls () = check_gloria "432(d): All Souls' Day (transferred)" 2025 11 3 false
+
+(* ---- RG 431(a)/432(a): mirrors [te_deum] for everything not already
+ decided above -- one true, one false, neither reachable via 431(c)/432(b)/
+ 432(d) ---- *)
+
+let test_431a_mirrors_te_deum_true () =
+ check_gloria "431(a): mirrors [te_deum]=true (a sanctoral feast, Lawrence)" 2026 8 10 true
+
+let test_432a_mirrors_te_deum_false () =
+ check_gloria "432(a): mirrors [te_deum]=false (an ordinary Advent feria)" 2025 12 1 false
+
+(* ---- The Rogation Monday/Tuesday cascade (celebrant-rubrics-phase1 Bug 1
+ fix, 2026-08-22): {!Rite_ef.Temporal_ef.temporal} used to hardcode
+ [Colour.Violet] on these two days with no citation, which silenced
+ [gloria] via 432(b) even though 237(e)'s own Paschaltide-feria branch
+ already said [te_deum]=true underneath. Fixed to [season_colour] (RG
+ 119: white from the Paschal Vigil to the Pentecost Vigil exclusive; RG
+ 128's own violet list never names Rogation days; RG 88 -- the violet
+ belongs to the Rogation MASS, not the Office this field describes).
+ 2028-05-22/23: neither day is impeded that year (test_golden.ml's own
+ [test_rogation_{monday,tuesday}_white_2028] pins the colour side of the
+ same fix), so both resolve to white and both now say the Gloria, MIRROR
+ OF [te_deum] via 431(a) once 432(b)'s violet guard no longer fires --
+ MATCHING the LMS Ordo witness (2023-2024, 2024-05-06, "FERIA IV Cl W
+ ... Gl"). Reverting temporal_ef.ml's fix alone (mutation-checked while
+ building this task) turns both [false], the pre-fix answer. *)
+let test_rogation_monday_gloria_2028 () = check_gloria "Rogation Monday: white -> Gloria said (2028)" 2028 5 22 true
+let test_rogation_tuesday_gloria_2028 () = check_gloria "Rogation Tuesday: white -> Gloria said (2028)" 2028 5 23 true
+
+(* ---- Domain-wide sanity, task requirement 5: "every violet day must be
+ false (RG 432 b), and every Requiem must be false (432 d)". Two
+ invariants, checked over every day {!Cal.year} resolves -- [Cal.year],
+ not [Cal.day] in a loop, for the same cost reason
+ {!test_every_sunday_in_2026_says_the_creed} above already gives.
+
+ FAST (default suite): a fixed 200-year span, 1583-1782 -- large enough
+ to cross multiple Easter cycles and every season repeatedly, cheap
+ enough to stay in `dune test`'s own budget. EXHAUSTIVE (gated the same
+ way {!test_validate.test_exhaustive_domain_sweep} already is, via
+ COLITUR_EXHAUSTIVE_SWEEP): the full 1583-9999 domain, also tallying and
+ printing the true/false distribution so the measurement this task asks
+ for is not merely "did the invariant hold" but has a number attached --
+ see the task report for the printed figures. *)
+(* NOT a blanket "every violet day is [gloria]=false" (the task brief's own
+ phrasing, taken literally, is one exception too strong): RG 431(c) is a
+ NAMED, lex-specialis override for the Easter Vigil Mass, and colitur's
+ own per-day colour model gives Holy Saturday [Colour.Violet]
+ ([gloria]'s own header has the full argument for why -- the historical
+ violet-to-white vestment change happens AT the Gloria itself, a
+ per-action nuance this whole day/colour model already cannot express).
+ So the real invariant, checked here, is "every violet day is
+ [gloria]=false EXCEPT the Easter Vigil (Easter offset -1), which is
+ [gloria]=true BY DESIGN" -- and the exception is asserted to be EXACTLY
+ that one shape, every year, nothing else: [n = -1] is checked directly
+ rather than merely excluded, so a second, unexpected (violet,
+ gloria=true) day anywhere in the domain still fails loudly. Pushed back
+ on the task brief's own simplified phrasing rather than silently
+ special-cased -- see the task report. *)
+let check_gloria_invariants_for_year y (counts : (int * int * int * int) ref) =
+ let days = Cal.year ctx layer y in
+ (* Per-date, NOT once per loop iteration off [y]: a liturgical year
+ "opening in civil year y" ({!Rite.t.year_start}'s own doc comment)
+ runs from that year's Advent into MOST of civil year y+1 -- so
+ [Cal.year ctx layer y]'s own array holds dates whose civil year is
+ y+1 for the whole Christmas-to-Pentecost span, governed by EASTER OF
+ y+1, not y. A single [Computus.gregorian_easter y] computed once here
+ wrongly used year y's own Easter for those dates -- found live: it
+ misidentified 1584-03-31 (Holy Saturday, governed by 1584's Easter,
+ surfaced while processing loop iteration y=1583) as an ordinary
+ violet day instead of the Vigil's own 431(c) exception, because
+ Easter 1583 (not 1584) was subtracted. Fixed by keying off
+ [Date.year d.LD.date] instead, which is always safe here: Holy
+ Saturday and Easter Sunday are never more than a few days apart and
+ never cross a civil-year boundary. *)
+ let violet, black, gloria_true, gloria_false = !counts in
+ let violet = ref violet
+ and black = ref black
+ and gloria_true = ref gloria_true
+ and gloria_false = ref gloria_false in
+ Array.iter
+ (fun (d : (V.season, V.rank) LD.t) ->
+ (if d.LD.gloria then incr gloria_true else incr gloria_false);
+ if d.LD.observed.Cel.colour = Colour.Violet then begin
+ incr violet;
+ let easter = Computus.gregorian_easter (Date.year d.LD.date) in
+ let n = Date.to_rata d.LD.date - Date.to_rata easter in
+ if n = -1 then
+ Alcotest.(check bool)
+ (Printf.sprintf "%s: the Easter Vigil's own 431(c) override, [gloria]=true despite violet"
+ (Date.to_iso8601 d.LD.date))
+ true d.LD.gloria
+ else
+ Alcotest.(check bool)
+ (Printf.sprintf "%s: violet -> [gloria]=false (RG 432(b))" (Date.to_iso8601 d.LD.date))
+ false d.LD.gloria
+ end;
+ if d.LD.observed.Cel.colour = Colour.Rose then
+ (* Gaudete/Laetare -- [gloria] reads [false] here via [te_deum]'s
+ own [Class2] guard on 237(b) (both Rose Sundays are [Class1] BY
+ CONSTRUCTION -- {!Rite_ef.Rubrics_ef.gloria}'s own 432(b)
+ citation has the full "checked, then found redundant, then
+ removed" account of why 432(b) itself does NOT need its own
+ Rose branch). Not folded into the [violet] counter above:
+ keeping the two colours separately tallied is what let this
+ invariant catch the Gaudete/Laetare gap live in the first place
+ (found via the LMS Ordo, 2023-12-17 and 2024-03-10) rather than
+ silently averaging it away inside one shared bucket. *)
+ Alcotest.(check bool)
+ (Printf.sprintf "%s: Rose (Gaudete/Laetare) -> [gloria]=false" (Date.to_iso8601 d.LD.date))
+ false d.LD.gloria;
+ if d.LD.observed.Cel.colour = Colour.Black then begin
+ incr black;
+ Alcotest.(check bool)
+ (Printf.sprintf "%s: Requiem (black) -> [gloria]=false (RG 432(d))" (Date.to_iso8601 d.LD.date))
+ false d.LD.gloria
+ end)
+ days;
+ counts := (!violet, !black, !gloria_true, !gloria_false)
+
+let test_domain_violet_implies_no_gloria_sample () =
+ let counts = ref (0, 0, 0, 0) in
+ for y = 1583 to 1782 do
+ check_gloria_invariants_for_year y counts
+ done;
+ let violet, _, _, _ = !counts in
+ Alcotest.(check bool) "the 200-year sample reached a real number of violet days" true (violet > 1000)
+
+let test_domain_requiem_implies_no_gloria_sample () =
+ (* Separate test name, same underlying sweep as the one immediately above
+ -- {!Cal.year} is only computed once per year regardless (module-level
+ [ctx]/[layer], no per-test reload), so this is not a second pass over
+ the domain, only a second, independently-named assertion on the same
+ tally, matching how {!test_colour_black_population_is_exactly_two}
+ above separates its own DATA and CODE checks into one function while
+ this pair keeps violet and black as two named outcomes. *)
+ let counts = ref (0, 0, 0, 0) in
+ for y = 1583 to 1782 do
+ check_gloria_invariants_for_year y counts
+ done;
+ let _, black, _, _ = !counts in
+ Alcotest.(check bool) "the 200-year sample reached at least one Requiem day" true (black > 0)
+
+let colitur_exhaustive_sweep_env = "COLITUR_EXHAUSTIVE_SWEEP"
+
+let test_exhaustive_gloria_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_gloria_invariants_for_year y counts
+ done;
+ let violet, black, gloria_true, gloria_false = !counts in
+ Printf.printf
+ "gloria domain sweep 1583..9999: violet=%d black=%d gloria_true=%d gloria_false=%d total=%d\n%!"
+ violet black gloria_true gloria_false (gloria_true + gloria_false);
+ Alcotest.(check bool) "the full domain reached a real number of violet days" true (violet > 100_000);
+ 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 497 also covers "conversion-of-st-paul" (25 January, Class3,
+ NOT his own natalicium -- {!creed_apostle_slugs} deliberately excludes
+ it from the CREED for exactly that reason, RG 475(e)'s own natalicium
+ restriction). RG 497 carries no such restriction, and unlike
+ {!test_fiuv_ordo.ml}'s own single window (where 25 January falls on a
+ Sunday, hence impeded), the Latin Mass Society Ordo's three editions
+ (test/test_lms_ordo.ml, Preface-witnesses task, 2026-08-23) each carry
+ an unimpeded 25 January in two of their three civil years, 2024 and
+ 2025, and BOTH independently read "Pr of the Apostles" -- 2024-01-25 is
+ the date pinned here, matching the FIRST of those two witnessed
+ years directly (day names elsewhere in this file default to 2026 for
+ uniformity with the FIUV Ordo's own window, but 2026-01-25 is itself
+ impeded by a Sunday, so it cannot witness this branch at all). ---- *)
+let test_preface_497_conversion_of_st_paul () =
+ check_preface "RG 497: the Conversion of St Paul, not his own natalicium, still [Apostles]" Pref.Apostles 2024 1 25
+
+(* ---- 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
+
+(* ---- BLAST RADIUS, Preface-witnesses task (2026-08-23): adding
+ "conversion-of-st-paul" to {!Rite_ef.Rubrics_ef.preface_apostle_slugs}
+ changes exactly one thing -- the preface on every year's own 25 January
+ WHEN [conversion-of-st-paul] is the day actually observed (impeded, most
+ often by a Sunday, in every other year). Measured directly rather than
+ estimated: every observed occurrence must now read [Apostles], never
+ anything else, and the domain total is printed so the SIZE of this
+ change is on the record, not only its correctness. *)
+let check_conversion_of_st_paul_for_year y (counts : (int * int) ref) =
+ let days = Cal.year ctx layer y in
+ let observed0, wrong0 = !counts in
+ let observed = ref observed0 and wrong = ref wrong0 in
+ Array.iter
+ (fun (d : (V.season, V.rank) LD.t) ->
+ if String.equal (Slug.to_string d.LD.observed.Cel.slug) "conversion-of-st-paul" then begin
+ incr observed;
+ if d.LD.preface <> Some Pref.Apostles then begin
+ incr wrong;
+ Alcotest.failf "%s: conversion-of-st-paul is observed but preface=%s, not Apostles"
+ (Date.to_iso8601 d.LD.date)
+ (match d.LD.preface with Some p -> Pref.to_string p | None -> "NONE")
+ end
+ end)
+ days;
+ counts := (!observed, !wrong)
+
+let test_conversion_of_st_paul_preface_sample () =
+ let counts = ref (0, 0) in
+ for y = 1583 to 1782 do
+ check_conversion_of_st_paul_for_year y counts
+ done;
+ let observed, wrong = !counts in
+ Alcotest.(check int) "zero wrong-preface days in the 200-year sample" 0 wrong;
+ Alcotest.(check bool) "the 200-year sample found real conversion-of-st-paul occurrences" true (observed > 100)
+
+let test_exhaustive_conversion_of_st_paul_preface_sweep () =
+ if Sys.getenv_opt colitur_exhaustive_sweep_env = None then Alcotest.skip ()
+ else begin
+ let counts = ref (0, 0) in
+ for y = 1583 to 9999 do
+ check_conversion_of_st_paul_for_year y counts
+ done;
+ let observed, wrong = !counts in
+ Printf.printf "conversion-of-st-paul preface blast radius 1583..9999: observed=%d wrong=%d\n%!" observed wrong;
+ Alcotest.(check int) "zero wrong-preface days anywhere in the domain" 0 wrong
+ 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
+ [Cal.day] pipeline every other test in this file uses. See
+ precedence.mli's own citation for the rubric and the reasoning for why
+ this is a derivation over [LD.commemorations] (the Low-Mass admitted
+ set), not a second stored field. ---- *)
+
+module Prec = Colitur_kernel.Precedence
+
+let sung_slugs y m d =
+ List.map
+ (fun (c, _) -> Slug.to_string c.Cel.slug)
+ (Prec.sung_mass_commemorations (day_on y m d).LD.commemorations)
+
+let test_rg111a_ordinary_only_dropped () =
+ (* 2026-08-14: Vigil of the Assumption, commemorating [eusebius-confessor]
+ ORDINARILY (no privileged category applies) -- confirmed via `colitur
+ day 2026`. At Low Mass this is admitted (RG 111(c)); at a
+ non-conventual Sung Mass RG 111(a) admits it not at all. *)
+ Alcotest.(check (list string)) "RG 111(a): an ordinary-only Low-Mass set is empty at Sung Mass" []
+ (sung_slugs 2026 8 14)
+
+let test_rg111a_privileged_kept () =
+ (* 2026-04-25: St Mark, commemorating the Major Litanies -- RG 80/109(f),
+ a PRIVILEGED commemoration (data/ef/adjustments.sexp's own [Add
+ major-litanies]) -- confirmed via `colitur day 2026`. Kept at BOTH Low
+ Mass (RG 111(c), one privileged) and Sung Mass (RG 111(a), the same
+ single privileged commemoration). *)
+ Alcotest.(check (list string)) "RG 111(a): the one privileged commemoration survives at Sung Mass"
+ [ "major-litanies" ] (sung_slugs 2026 4 25)
+
+let suite =
+ ( "Rubrics_ef",
+ [ Alcotest.test_case "475(a): ordinary Sunday" `Quick test_475a_ordinary_sunday;
+ Alcotest.test_case "475(a): reads [temporal], not [observed] (Trap One)" `Quick
+ test_475a_reads_temporal_not_observed;
+ Alcotest.test_case "475(b): I-class feast" `Quick test_475b_class1_feast;
+ Alcotest.test_case "475(c): II-class feast of the Lord" `Quick test_475c_lord;
+ Alcotest.test_case "475(c): II-class BVM feast via marian_slugs" `Quick
+ test_475c_bvm_via_marian_slugs;
+ Alcotest.test_case "475(d): octave overrides an occurring feast" `Quick
+ test_475d_octave_even_occurring_feast;
+ Alcotest.test_case "475(d): 1 January octave-day boundary" `Quick test_475d_octave_day_boundary;
+ Alcotest.test_case "475(e): an Apostle's own natalicium" `Quick test_475e_apostle_natalicium;
+ Alcotest.test_case "475(e): Barnabas, named explicitly" `Quick
+ test_475e_barnabas_named_explicitly;
+ Alcotest.test_case "475(e): Chair of Peter, named explicitly" `Quick
+ test_475e_chair_of_peter_named_explicitly;
+ Alcotest.test_case "475(e) excludes the Conversion of St Paul (Trap Two)" `Quick
+ test_475e_excludes_conversion_of_paul;
+ Alcotest.test_case "RG 23/476: Ash Wednesday" `Quick test_ash_wednesday_no_creed;
+ Alcotest.test_case "RG 23/476(a): Holy Thursday" `Quick test_holy_thursday_no_creed;
+ Alcotest.test_case "RG 23/476(a): Holy Saturday / Easter Vigil" `Quick
+ test_holy_saturday_easter_vigil_no_creed;
+ Alcotest.test_case "476(f): All Souls' Day (transferred), a Requiem, no Creed" `Quick
+ test_476f_all_souls_no_creed;
+ Alcotest.test_case "476(f)/RG 23(b): Good Friday, black, no Creed" `Quick
+ test_476f_good_friday_no_creed;
+ Alcotest.test_case "Colour.Black population is exactly two (data + code)" `Quick
+ test_colour_black_population_is_exactly_two;
+ Alcotest.test_case "476(b): plain II-class saint (negative)" `Quick
+ test_476b_plain_class2_saint;
+ Alcotest.test_case "vigils excluded even when Class2 and Marian" `Quick
+ test_vigil_excluded_even_when_class2_and_marian;
+ Alcotest.test_case "476(d): BVM Saturday Office, IV class" `Quick test_476d_bvm_saturday_office;
+ Alcotest.test_case "every Sunday in 2026 says the Creed" `Quick
+ test_every_sunday_in_2026_says_the_creed;
+ Alcotest.test_case "RG 24/25: no ferial-origin office carries Lord/Bvm or an apostle slug" `Quick
+ test_ferial_origin_never_carries_lord_bvm_or_apostle_slug;
+ Alcotest.test_case "237(a): Easter Sunday" `Quick test_237a_easter_sunday;
+ Alcotest.test_case "237(a): Low Sunday" `Quick test_237a_low_sunday;
+ Alcotest.test_case "237(a): Pentecost Sunday" `Quick test_237a_pentecost;
+ Alcotest.test_case "237(f): Vigil of the Ascension" `Quick test_237f_ascension_vigil;
+ Alcotest.test_case "237(f): Vigil of Pentecost" `Quick test_237f_pentecost_vigil;
+ Alcotest.test_case "237(d): St John within the Nativity octave" `Quick
+ test_237d_nativity_octave_saint;
+ Alcotest.test_case "237(d): Easter Tuesday" `Quick test_237d_easter_week_feria;
+ Alcotest.test_case "238(b): Vigil of St Lawrence" `Quick test_238b_omissible_vigil;
+ Alcotest.test_case "Nativity Vigil excluded by structural inference, not literal 238(b)" `Quick
+ test_nativity_vigil_excluded_by_inference;
+ Alcotest.test_case "238(c)/RG23: Ash Wednesday" `Quick test_238c_ash_wednesday;
+ Alcotest.test_case "238(c)/RG23: Good Friday" `Quick test_238c_good_friday;
+ Alcotest.test_case "237(g): BVM Saturday Office" `Quick test_237g_bvm_saturday;
+ Alcotest.test_case "237(e): 2 January, a Christmastide feria" `Quick test_237e_christmastide_feria;
+ Alcotest.test_case "237(b): an ordinary Class2 Sunday" `Quick test_237b_ordinary_class2_sunday;
+ Alcotest.test_case "237(b)'s own exception: Septuagesima Sunday" `Quick test_237b_septuagesima_exception;
+ Alcotest.test_case "237(b)'s own exception: Sexagesima Sunday" `Quick test_237b_sexagesima_exception;
+ Alcotest.test_case "237(c): a sanctoral feast (Lawrence)" `Quick test_237c_sanctoral_feast;
+ Alcotest.test_case "Passion Sunday is not a festum for 237(c)" `Quick test_passion_sunday_not_a_festum;
+ Alcotest.test_case "Palm Sunday is not a festum for 237(c)" `Quick test_palm_sunday_not_a_festum;
+ Alcotest.test_case "238(d): All Souls' Day (transferred)" `Quick test_238d_all_souls;
+ Alcotest.test_case "431(c): Holy Thursday" `Quick test_431c_holy_thursday;
+ Alcotest.test_case "431(c): the Easter Vigil Mass" `Quick test_431c_easter_vigil;
+ Alcotest.test_case "432(b): Advent I Sunday, violet" `Quick test_432b_violet_sunday;
+ Alcotest.test_case "432(d): All Souls' Day (transferred)" `Quick test_432d_all_souls;
+ Alcotest.test_case "431(a): mirrors [te_deum]=true" `Quick test_431a_mirrors_te_deum_true;
+ Alcotest.test_case "432(a): mirrors [te_deum]=false" `Quick test_432a_mirrors_te_deum_false;
+ Alcotest.test_case "Rogation Monday: white -> Gloria said (2028)" `Quick test_rogation_monday_gloria_2028;
+ Alcotest.test_case "Rogation Tuesday: white -> Gloria said (2028)" `Quick test_rogation_tuesday_gloria_2028;
+ Alcotest.test_case "domain sanity: every violet day has [gloria]=false (sample)" `Quick
+ test_domain_violet_implies_no_gloria_sample;
+ Alcotest.test_case "domain sanity: every Requiem day has [gloria]=false (sample)" `Quick
+ test_domain_requiem_implies_no_gloria_sample;
+ Alcotest.test_case "domain sweep 1583..9999: violet/Requiem invariants, committed not sampled"
+ `Slow test_exhaustive_gloria_domain_sweep;
+ 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;
+ 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 497: the Conversion of St Paul, not his own natalicium" `Quick
+ test_preface_497_conversion_of_st_paul;
+ 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;
+ Alcotest.test_case "conversion-of-st-paul preface blast radius (sample)" `Quick
+ test_conversion_of_st_paul_preface_sample;
+ Alcotest.test_case "conversion-of-st-paul preface blast radius 1583..9999, committed not sampled" `Slow
+ test_exhaustive_conversion_of_st_paul_preface_sweep ] )