diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/cli.t | 33 | ||||
| -rw-r--r-- | test/test_calendar.ml | 10 | ||||
| -rw-r--r-- | test/test_fiuv_ordo.ml | 153 | ||||
| -rw-r--r-- | test/test_rubrics_ef.ml | 387 | ||||
| -rw-r--r-- | test/test_validate.ml | 16 |
5 files changed, 573 insertions, 26 deletions
@@ -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 |
