aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/cli.t50
-rw-r--r--test/dune18
-rw-r--r--test/test_colitur.ml1
-rw-r--r--test/test_rite_of.ml340
4 files changed, 409 insertions, 0 deletions
diff --git a/test/cli.t b/test/cli.t
index 4ae116f..8671694 100644
--- a/test/cli.t
+++ b/test/cli.t
@@ -199,6 +199,56 @@ A year outside the supported domain is rejected (exit 2):
colitur: year 1000 out of range 1583..9999
[2]
+Task 5 (2026-08-25-colitur-of-phases-3-5): `--rite ef|of` on `day`/`readings`.
+Default stays ef -- every invocation above, written before this flag existed,
+must be byte-identical to what it printed before, so this is asserted
+directly rather than assumed: `--rite ef` is a no-op, and omitting the flag
+entirely resolves to the exact same thing.
+
+ $ colitur day 2026 > /tmp/rite-day-default.out
+ $ colitur day --rite ef 2026 > /tmp/rite-day-ef.out
+ $ cmp /tmp/rite-day-default.out /tmp/rite-day-ef.out && echo identical
+ identical
+ $ colitur readings 2026 > /tmp/rite-readings-default.out
+ $ colitur readings --rite ef 2026 > /tmp/rite-readings-ef.out
+ $ cmp /tmp/rite-readings-default.out /tmp/rite-readings-ef.out && echo identical
+ identical
+ $ colitur day --rite ef 2026 --raw | head -1
+ 2026-01-01 thursday christmastide - ef-circumcision class-1 white
+
+The OF rite resolves too, same two commands, its own vocabulary:
+
+ $ colitur day --rite of 2026 | wc -l
+ 365
+ $ colitur day --rite of 2026 | head -1
+ 2026-01-01 thursday christmas - of-mary-mother-of-god sollemnitas white
+ $ colitur day --rite of 2026 | grep -c '^2026-12-25 '
+ 1
+ $ colitur day --rite of 2026 | grep '^2026-12-25 '
+ 2026-12-25 friday christmas - of-nativity sollemnitas white
+
+ $ colitur readings --rite of 2026 | wc -l
+ 365
+ $ colitur readings --rite of 2026 | head -1
+ 2026-01-01 of-mary-mother-of-god | Num 6:22-27 | Luc 2:16-21
+
+An unrecognised --rite value is a usage error, not a silent fallback to ef
+(exit 2):
+
+ $ colitur day --rite tridentine 2026
+ colitur: unknown --rite "tridentine" (expected "ef" or "of")
+ [2]
+
+`--rite` is refused, not silently ignored, on every command besides
+day/readings (exit 2):
+
+ $ colitur easter --rite of 2026
+ colitur: --rite has no effect on `easter`; refusing rather than ignoring it
+ [2]
+ $ colitur rubrics --rite of 2026
+ colitur: --rite has no effect on `rubrics`; refusing rather than ignoring it
+ [2]
+
Rubrics (Task 4, celebrant-rubrics-phase1): the day's own Mass formulary, one
row per day: date, the formulary's slug (which Mass is actually said, not
always the day's own), and how that was decided
diff --git a/test/dune b/test/dune
index 50bdee2..0a3ade6 100644
--- a/test/dune
+++ b/test/dune
@@ -79,6 +79,24 @@
../data/ef/adjustments.sexp
../data/ef/lectionary.sexp
../data/ef/commons.sexp
+ ; Task 5 (2026-08-25-colitur-of-phases-3-5), `colitur day|readings --rite
+ ; of`: the OF calendar's own runtime files, same "the cram sandbox only
+ ; ever gets what this stanza names" trap as every entry around them.
+ ../data/of/calendar-2002.sexp
+ ../data/of/amendments/001-padre-pio.sexp
+ ../data/of/amendments/002-juan-diego-cuauhtlatoatzin.sexp
+ ../data/of/amendments/003-our-lady-of-guadalupe.sexp
+ ../data/of/amendments/004-john-xxiii-john-paul-ii.sexp
+ ../data/of/amendments/005-mary-magdalene-rank.sexp
+ ../data/of/amendments/006-mary-mother-of-the-church.sexp
+ ../data/of/amendments/007-paul-vi.sexp
+ ../data/of/amendments/008-our-lady-of-loreto.sexp
+ ../data/of/amendments/009-faustina-kowalska.sexp
+ ../data/of/amendments/010-narek-avila-hildegard.sexp
+ ../data/of/amendments/011-martha-mary-lazarus.sexp
+ ../data/of/amendments/012-teresa-of-calcutta.sexp
+ ../data/of/amendments/013-john-henry-newman.sexp
+ ../data/of/lectionary.sexp
; Task 12, `colitur publish`: the cram sandbox only ever gets what this
; stanza names explicitly (unlike a plain `dune build`, it does not fall
; back to the workspace root's own default alias), so schema/day-v1.json
diff --git a/test/test_colitur.ml b/test/test_colitur.ml
index 30e649e..4b7c78a 100644
--- a/test/test_colitur.ml
+++ b/test/test_colitur.ml
@@ -17,6 +17,7 @@ let () =
Test_precedence.suite;
Test_calendar.suite; Test_precedence_ef.suite; Test_sanctoral_ef.suite; Test_calendar_of_data.suite;
Test_rite_ef.suite;
+ Test_rite_of.suite;
Test_rubrics_ef.suite;
Test_rubrics_of.suite;
Test_differential.suite; Test_oracle.suite; Test_oracle.suite_2038; Test_oracle.suite_2035;
diff --git a/test/test_rite_of.ml b/test/test_rite_of.ml
new file mode 100644
index 0000000..90cedc1
--- /dev/null
+++ b/test/test_rite_of.ml
@@ -0,0 +1,340 @@
+(* Task 5 (2026-08-25-colitur-of-phases-3-5): [Rite_of.context], wired
+ together with the REAL data/of/calendar-2002.sexp + all 13 decree
+ overlays + data/of/lectionary.sexp, resolved through
+ {!Colitur_kernel.Calendar} and checked by {!Colitur_kernel.Validate.run}
+ -- the same pipeline `colitur day --rite of` uses, and the same
+ discipline test_validate.ml/test_rite_ef.ml already apply to the EF
+ side: exactly one observed office per day, the year covered once with no
+ gaps, slug-uniqueness within the liturgical year, and transfers reaching
+ a fixed point are all invariants {!Colitur_kernel.Validate.run} already
+ checks generically -- this file supplies the REAL OF rite and data and
+ asserts the result is clean, rather than re-deriving each invariant by
+ hand. *)
+
+module Val = Colitur_kernel.Validate
+module Layer = Colitur_kernel.Layer
+module Overlay = Colitur_kernel.Overlay
+module Cal = Colitur_kernel.Calendar
+module LD = Colitur_kernel.Liturgical_day
+module Slug = Colitur_kernel.Slug
+module Date = Colitur_kernel.Date
+module Cel = Colitur_kernel.Celebration
+module P = Colitur_kernel.Precedence
+module Computus = Colitur_kernel.Computus
+module V = Rite_of.Vocab_of
+
+(* Relative to this test's own build directory (_build/default/test/), same
+ convention test_amendments_of.ml/test_calendar_of_data.ml already use --
+ test/dune declares each of these as a dep of the (test ...) stanza. *)
+let base_path = "../data/of/calendar-2002.sexp"
+let amendments_dir = "../data/of/amendments/"
+let lectionary_path = "../data/of/lectionary.sexp"
+
+(* Decree-chronological order -- the exact list and order
+ test_amendments_of.ml's own [files] table already pins by SHA-256; not
+ re-pinning the hashes here (that is that file's own job), just applying
+ them in the same order so the rite under test here is the one bin/main.ml
+ will assemble. *)
+let amendment_files =
+ [ "001-padre-pio.sexp"; "002-juan-diego-cuauhtlatoatzin.sexp"; "003-our-lady-of-guadalupe.sexp";
+ "004-john-xxiii-john-paul-ii.sexp"; "005-mary-magdalene-rank.sexp"; "006-mary-mother-of-the-church.sexp";
+ "007-paul-vi.sexp"; "008-our-lady-of-loreto.sexp"; "009-faustina-kowalska.sexp";
+ "010-narek-avila-hildegard.sexp"; "011-martha-mary-lazarus.sexp"; "012-teresa-of-calcutta.sexp";
+ "013-john-henry-newman.sexp" ]
+
+let real_of_layer =
+ let base =
+ match Layer.load V.rank_of_sexp base_path with
+ | Ok l -> l
+ | Error e -> failwith (Printf.sprintf "%s: failed to load: %s" base_path e)
+ in
+ let overlays =
+ List.map
+ (fun name ->
+ let path = amendments_dir ^ name in
+ match Overlay.load V.rank_of_sexp path with
+ | Ok o -> o
+ | Error e -> failwith (Printf.sprintf "%s: failed to load: %s" path e))
+ amendment_files
+ in
+ let layer, diagnostics = Overlay.merge base overlays in
+ if diagnostics <> [] then
+ failwith
+ (Printf.sprintf "unexpected amendment diagnostics: %s"
+ (String.concat "; " (List.map Overlay.diagnostic_to_string diagnostics)));
+ layer
+
+(* [Rite_of.context] takes [~lectionary] as a caller-supplied parameter, not
+ a value closed over an internal load -- see rite_of.ml's own citation for
+ why (the exact `colitur easter <year>` startup-death lesson
+ {!Rite_ef.Rite_ef.context}'s own comment records). *)
+let real_of_lectionary =
+ match Colitur_kernel.Lectionary.load lectionary_path with
+ | Ok l -> l
+ | Error e -> failwith (Printf.sprintf "%s: failed to load: %s" lectionary_path e)
+
+let real_of_rite = Rite_of.context ~lectionary:real_of_lectionary
+
+let run year = Val.run real_of_rite real_of_layer ~year
+
+(* data/of/lectionary.sexp's own provenance header names ONE informational
+ temporal-slug gap: [of-nativity] (25 December -- Christmas Day itself)
+ has no entry, because niedziela.pl's own source keys the Nativity's
+ several Masses (Vigil/Night/Dawn/Day) differently than a single flat
+ slug can address, and [Lectionary_of.readings] has no fallback beyond
+ its own three-step chain (unlike EF's RG 299 preceding-Sunday rule,
+ which OF's own OLM Praenotanda gives no equivalent for -- see
+ lectionary_of.mli's own citation). This makes {!Colitur_kernel.Validate
+ .run}'s ["citations-unresolved"] check fire exactly once a year, on 25
+ December, on real shipped data -- a documented, measured gap in
+ lectionary_of's own data file (out of this task's scope to touch or
+ close: lectionary_of.* is excluded from this task's brief), not a defect
+ this module's wiring introduced. Named and filtered explicitly, never
+ silently dropped, so a future SECOND gap (a regression) still reddens
+ this test. *)
+let ends_with ~suffix s =
+ let ls = String.length s and lx = String.length suffix in
+ ls >= lx && String.sub s (ls - lx) lx = suffix
+
+(* No [Str]/regex (deps frozen) -- a plain substring scan. *)
+let contains ~substring s =
+ let ls = String.length s and lx = String.length substring in
+ let rec go i = i + lx <= ls && (String.sub s i lx = substring || go (i + 1)) in
+ lx = 0 || go 0
+
+let is_known_nativity_gap (f : Val.failure) =
+ (f.Val.check = "citations-unresolved" || f.Val.check = "formulary") && ends_with ~suffix:"12-25" f.Val.date
+
+(* A SECOND, NEWLY-FOUND gap this task's own wiring surfaced -- [rite_of] was
+ never previously wired into a real {!Colitur_kernel.Rite.t}, so
+ [Validate.run]'s ["anchor"] check has never before run [Temporal_of
+ .anchors] against [Temporal_of.temporal] for real. Normae n. 35(a)'s own
+ text is "Dominica infra octavam Nativitatis, VEL, EA DEFICIENTE, die 30
+ decembris" -- the Holy Family fallback is a FIXED-DATE fallback (30
+ December, whatever weekday it falls on that year), confirmed by
+ {!Temporal_of.holy_family}'s own [None -> mk y 12 30] branch and its own
+ .mli citation ("this 6-day window CAN be [empty of a Sunday] -- confirmed
+ by the fallback clause's own existence"). But {!Temporal_of.temporal}
+ only ever TESTS [same d (holy_family y)] inside its [sunday_slug d]
+ dispatch arm -- i.e. only when [d] genuinely IS a Sunday -- so in a year
+ whose 26-31 December window has NO Sunday (equivalently: 25 December,
+ Christmas Day, is itself a Sunday -- a 6-day window starting the day
+ after a Sunday omits Sunday from every other position), the fallback
+ date is computed correctly by [holy_family]/[anchors] but [temporal]
+ itself silently falls through to an ordinary ferial slug instead
+ (["of-christmas-0-friday"] et al.) -- exactly the "generic day slug
+ masking a real named feast" shape CLAUDE.md already records EF's own
+ Holy Family/Holy Name gaps as, before they were fixed there. Not rare:
+ this fires whenever Christmas Day is a Sunday, roughly one year in
+ seven (1583, 2005, 2011, 2016, 2022, 2033, 2039, 2044, 2050 all
+ independently confirmed via Python's own proleptic-Gregorian
+ [datetime]). OUT OF THIS TASK'S SCOPE to fix -- temporal_of.ml's own
+ rubric logic is Task 1's deliverable, not Task 5's, and the task brief
+ models exactly this "pin the known-wrong behaviour, do not fix it here"
+ treatment for a different, already-known gap (Normae n.56(f), St
+ Joseph/Palm Sunday) -- so the same discipline is applied to this
+ newly-found one rather than silently working around it or fixing rite
+ logic this task was not chartered to touch. Reported in this task's own
+ report as a concern. *)
+let is_known_holy_family_fallback_gap (f : Val.failure) =
+ f.Val.check = "anchor" && contains ~substring:"of-holy-family" f.Val.detail
+
+let check_year_allowing_known_gaps year =
+ let fs = run year in
+ let is_known f = is_known_nativity_gap f || is_known_holy_family_fallback_gap f in
+ let unexpected = List.filter (fun f -> not (is_known f)) fs in
+ (match unexpected with
+ | [] -> ()
+ | fs ->
+ Alcotest.failf "%d: %s" year
+ (String.concat "; " (List.map Val.failure_to_string (List.filteri (fun i _ -> i < 5) fs))));
+ let nativity_gaps = List.filter is_known_nativity_gap fs in
+ Alcotest.(check int)
+ (Printf.sprintf "%d: exactly the two documented of-nativity checks (citations-unresolved + \
+ formulary), no more" year)
+ 2 (List.length nativity_gaps)
+
+(* Landmark years, the same choice test_validate.ml's own
+ [test_landmark_years] makes for EF: both domain edges plus two ordinary
+ years well inside it. *)
+let test_landmark_years () = List.iter check_year_allowing_known_gaps [ 1583; 2026; 2035; 9998 ]
+
+(* 9999 mirrors test_validate.ml's own [test_year_9999_does_not_raise]: the
+ liturgical year opening in civil year 9999 continues into out-of-domain
+ civil year 10000, so [Val.run]/[Calendar.year] clamp the walk to 31
+ December 9999 rather than raising, and the truncated season run is
+ *expected* to fail the "seasons" check -- pinning that this surfaces as
+ an ordinary failure, not an uncaught exception, and that resolution
+ itself ("coverage"/"lost"/"duplicated") stays clean through the clamp. *)
+let test_year_9999_does_not_raise () =
+ let fs = run 9999 in
+ Alcotest.(check bool) "no coverage failures (temporal stayed total through the clamp)" true
+ (not (List.exists (fun f -> f.Val.check = "coverage") fs));
+ Alcotest.(check bool) "seasons check flags the truncated final year as incomplete" true
+ (List.exists (fun f -> f.Val.check = "seasons") fs)
+
+(* A real transfer-bearing sample, the OF counterpart of test_rite_ef.ml's
+ own 2005-2050 sweep: every impeded Sollemnitas [Precedence_of.disposition]
+ defers is either placed by [transfer_target] and reaches a fixed point
+ (no "unconverged" failure -- Validate's own check), or is recorded
+ [omitted] with a reason, never silently dropped ("lost"/"duplicated"),
+ over the project's own 2005-2050 differential-testing window (CLAUDE.md).
+
+ STRUCTURAL checks only -- ["coverage"; "weekday"; "rank"; "colour";
+ "determinism"; "slugs"; "seasons"; "week"; "lost"; "duplicated";
+ "unconverged"; "admission"; "observed"] -- deliberately NOT the
+ citation-chain ones ("anchor"/"citations"/"citations-unresolved"/
+ "formulary"), and this exclusion is itself a finding, not a shortcut:
+ sweeping the full 46-year window (not just the 4 landmark years above)
+ surfaced that [data/of/lectionary.sexp]'s own coverage was measured
+ against exactly ONE civil year (2026, per that file's own provenance
+ header) -- OTHER years exercise the OTHER weekday-cycle letter for the
+ same temporal slug (OLM n.69.4, {!Lectionary_of.weekday_cycle}: which
+ letter applies flips on the LITURGICAL year label's parity, independent
+ of which civil year is being swept), and at least one such pairing is
+ asymmetric on real shipped data: [of-christmas-0-friday-ii] exists,
+ [of-christmas-0-friday-i] does not (confirmed directly against
+ data/of/lectionary.sexp; first reached in this sweep at 2006-12-29).
+ Separately, 2006-12-24 (a year Christmas Eve itself is a Sunday) also
+ exposed a THIRD newly-found gap: {!Temporal_of.anchors} lists
+ [of-nativity-vigil] on 24 December UNCONDITIONALLY, but
+ {!Temporal_of.named}'s own coded exception (Tabula I.2/Normae n.5:
+ Advent IV outranks the Vigil when they coincide) means
+ {!Temporal_of.temporal} does not always agree. All three are OUT OF
+ THIS TASK'S SCOPE to fix -- lectionary_of.* and data/of/lectionary.sexp
+ are excluded from this task's brief, and temporal_of.ml's own rubric
+ logic is Task 1's deliverable -- reported as concerns in this task's own
+ report rather than fixed here or silently excluded without explanation. *)
+let sample_years =
+ let rec range a b = if a > b then [] else a :: range (a + 1) b in
+ range 2005 2050
+
+let structural_check = function
+ | "anchor" | "citations" | "citations-unresolved" | "formulary" -> false
+ | _ -> true
+
+let check_year_structural_only year =
+ match List.filter (fun f -> structural_check f.Val.check) (run year) with
+ | [] -> ()
+ | fs ->
+ Alcotest.failf "%d: %s" year
+ (String.concat "; " (List.map Val.failure_to_string (List.filteri (fun i _ -> i < 5) fs)))
+
+let test_transfers_reach_a_fixed_point_2005_2050 () =
+ List.iter check_year_structural_only sample_years
+
+let slug_of (c : V.rank Cel.t) = Slug.to_string c.Cel.slug
+
+(* precedence_of.mli's own documented, KNOWN-UNIMPLEMENTED fourth
+ transfer_target rule (final review Important #5, ledger ruling R9):
+ Normae n. 56(f) -- St Joseph's solemnity, falling on Palm Sunday, is
+ ANTICIPATED BACKWARD to 18 March, not carried forward. colitur's
+ [transfer_target] has no backward-search branch (rite.mli's own
+ [transfer_target] contract requires the result be strictly LATER than
+ the origin -- an EF-shaped kernel obligation, per that value's own
+ citation), so it instead sends Joseph forward via the general rule 3,
+ the same target date the Annunciation would already be heading to that
+ same year (Easter + 8).
+
+ This is NOT fixed by this task (explicitly out of scope, per the task
+ brief) -- pinned here, on a REAL affected year, so the current
+ (known-wrong) behaviour is asserted deliberately rather than passing
+ silently as if it were correct. All years with Gregorian Easter = 26
+ March put Palm Sunday on 19 March, coinciding with Joseph; 2062 is the
+ nearest one after today (2026) still inside the differential-adjacent
+ range this project favours for worked examples.
+
+ 2062 is a doubly-worked example, not chosen only for Joseph: that same
+ year, 25 March (the Annunciation) falls on HOLY SATURDAY (Easter - 1),
+ so BOTH solemnities are impeded and BOTH nominally target Easter + 8 via
+ the general rule -- {!Colitur_kernel.Calendar}'s own placement pass
+ settles that collision by landing whichever it processes first (Joseph,
+ departing the earlier date, 19 March) on Easter + 8 and pushing the
+ other to the next admissible day; empirically, on shipped data, it is
+ JOSEPH who is pushed one day further still, to Easter + 9 -- read as
+ further confirmation this is genuinely rule 3's general forward search
+ (sensitive to whatever else is competing that year), not the fixed,
+ collision-proof Easter+8 destination rule 3's cousin (the Annunciation's
+ own Holy-Week branch) names outright -- not as a claim about exactly
+ which of the two wins the tie, which this test does not depend on. *)
+let test_joseph_palm_sunday_2062_known_wrong_forward_transfer () =
+ let easter_2062 = Computus.gregorian_easter 2062 in
+ Alcotest.(check string) "2062 Easter is really 26 March (the Normae n.56(f) trigger)" "2062-03-26"
+ (Date.to_iso8601 easter_2062);
+ let palm_sunday = Date.add_days easter_2062 (-7) in
+ Alcotest.(check string) "Palm Sunday 2062 is really 19 March, coinciding with St Joseph" "2062-03-19"
+ (Date.to_iso8601 palm_sunday);
+ let holy_saturday = Date.add_days easter_2062 (-1) in
+ Alcotest.(check string) "25 March 2062 is really Holy Saturday, coinciding with the Annunciation too"
+ "2062-03-25" (Date.to_iso8601 holy_saturday);
+ let days = Cal.year real_of_rite real_of_layer 2061 in
+ let joseph_day =
+ Array.to_list days |> List.find (fun d -> Date.compare d.LD.date palm_sunday = 0)
+ in
+ Alcotest.(check string) "Palm Sunday itself is observed on 19 March (Joseph impeded, correctly)"
+ "of-palm-sunday" (slug_of joseph_day.LD.observed);
+ let joseph_slug = "joseph-husband-of-the-blessed-virgin-mary" in
+ let departure =
+ List.find_opt (fun (c, _) -> slug_of c = joseph_slug) joseph_day.LD.transferred_out
+ in
+ match departure with
+ | None -> Alcotest.fail "expected Joseph to be transferred_out from 19 March 2062 (impeded by Palm Sunday)"
+ | Some (_, target) ->
+ (* Concrete, not merely qualitative: 2062-04-04 is the actual pinned
+ value on shipped data today (Easter + 9, one day past the
+ Annunciation's own Easter + 8 -- see this test's own header). A
+ future data or Calendar change that moves it is meant to be
+ caught here; the point being pinned is the SHAPE (forward, past
+ Easter, not 18 March), asserted again below in a form that does
+ not depend on the exact day. *)
+ Alcotest.(check string)
+ "KNOWN WRONG (Normae n.56(f) unimplemented, precedence_of.mli): Joseph lands forward, past \
+ Easter (currently Easter+9, one day beyond the Annunciation's own Easter+8), not the \
+ rubrically-correct 18 March (backward)"
+ "2062-04-04" (Date.to_iso8601 target);
+ Alcotest.(check bool) "not 18 March -- confirming this really is the anticipation defect, not a \
+ coincidence" false
+ (Date.compare target (Date.add_days palm_sunday (-1)) = 0);
+ Alcotest.(check bool) "and it lands on or after Easter itself (rule 3's forward search, not a \
+ backward anticipation)" true
+ (Date.compare target easter_2062 >= 0)
+
+(* Second NEWLY-FOUND defect, pinned the same way (see
+ [is_known_holy_family_fallback_gap]'s own citation just above for the
+ full argument): 1583 is the domain floor AND a year Christmas Day falls
+ on a Sunday, so 26-31 December has no Sunday of its own and
+ [Temporal_of.holy_family] falls back to its own fixed 30 December --
+ correctly, per [Temporal_of.anchors] -- but [Temporal_of.temporal]
+ itself never reaches that fallback (its Holy-Family test lives inside
+ the Sunday-only dispatch arm), so 30 December 1583 is observed as an
+ ordinary Friday-of-Christmastide ferial office instead of the Feast of
+ the Holy Family. *)
+let test_holy_family_fallback_1583_known_wrong_ferial () =
+ let christmas_1583 = Date.of_iso8601 "1583-12-25" |> Result.get_ok in
+ Alcotest.(check bool) "1583 Christmas Day really is a Sunday (the Normae n.35(a) trigger)" true
+ (Date.weekday christmas_1583 = Date.Sun);
+ let days = Cal.year real_of_rite real_of_layer 1583 in
+ let dec30 = Array.to_list days |> List.find (fun d -> Date.compare d.LD.date (Date.add_days christmas_1583 5) = 0) in
+ Alcotest.(check string)
+ "KNOWN WRONG (Normae n.35(a) fallback unreached, see [is_known_holy_family_fallback_gap]'s own \
+ citation): 30 December 1583 is an ordinary ferial slug, not of-holy-family"
+ "of-christmas-0-friday" (slug_of dec30.LD.observed);
+ Alcotest.(check bool) "and that really is NOT the Holy Family (confirming this really is the defect)" false
+ (slug_of dec30.LD.observed = "of-holy-family")
+
+let suite =
+ ( "Rite_of (real data: assembled Rite.t end-to-end via Calendar/Validate)",
+ [ Alcotest.test_case "landmark years validate cleanly (modulo two documented gaps)" `Quick
+ test_landmark_years;
+ Alcotest.test_case "year 9999 does not raise; seasons flags the clamp, coverage stays clean" `Quick
+ test_year_9999_does_not_raise;
+ Alcotest.test_case "2005-2050: transfers reach a fixed point, nothing lost or duplicated" `Slow
+ test_transfers_reach_a_fixed_point_2005_2050;
+ Alcotest.test_case
+ "KNOWN OPEN DEFECT pinned, not fixed: St Joseph on Palm Sunday 2062 transfers forward, not \
+ backward per Normae n.56(f)" `Quick
+ test_joseph_palm_sunday_2062_known_wrong_forward_transfer;
+ Alcotest.test_case
+ "NEWLY-FOUND DEFECT pinned, not fixed: Holy Family's Normae n.35(a) 30-December fallback is \
+ unreached when Christmas Day is a Sunday (1583)" `Quick
+ test_holy_family_fallback_1583_known_wrong_ferial ] )