diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/cli.t | 50 | ||||
| -rw-r--r-- | test/dune | 18 | ||||
| -rw-r--r-- | test/test_colitur.ml | 1 | ||||
| -rw-r--r-- | test/test_rite_of.ml | 340 |
4 files changed, 409 insertions, 0 deletions
@@ -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 @@ -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 ] ) |
