diff options
Diffstat (limited to 'test/test_temporal_ef.ml')
| -rw-r--r-- | test/test_temporal_ef.ml | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/test/test_temporal_ef.ml b/test/test_temporal_ef.ml index 751abd2..23a817d 100644 --- a/test/test_temporal_ef.ml +++ b/test/test_temporal_ef.ml @@ -167,12 +167,110 @@ let test_resumed_sundays () = module Cel = Colitur_kernel.Celebration module Sl = Colitur_kernel.Slug module Colr = Colitur_kernel.Colour +module Sub = Colitur_kernel.Subject let office dt = (T.temporal dt).Colitur_kernel.Temporal.office let slug_of dt = Sl.to_string (office dt).Cel.slug let rank_of dt = V.rank_to_string (office dt).Cel.rank let colour_of dt = Colr.to_string (office dt).Cel.colour +let subject_of dt = (office dt).Cel.subject let temporal_week dt = (T.temporal dt).Colitur_kernel.Temporal.week +let la = Colitur_kernel.Lang.of_string_exn "la" +let en = Colitur_kernel.Lang.of_string_exn "en" +let name_la_of dt = Colitur_kernel.Names.find (office dt).Cel.names la + +(* RG 17(b) (Caput III, "De Dominicis"), primary text (scan-verified, + docs/research/rules-register.md §4): "festum S. Familiae Iesu, Mariae, + Ioseph, celebrandum dominica prima post Epiphaniam" -- the Holy Family is + celebrated on the first Sunday after Epiphany, EVERY year (unlike RG + 17(a)'s Holy Name, whose narrower 2-5 January window can be empty and + carries an explicit "vel ea deficiente, die 2 ianuarii" fallback in the + calendarium itself -- 7-13 January can never be empty of a Sunday, so + Holy Family needs, and the calendarium carries, no such fallback). + [T.holy_family_sunday] is [named]'s own [week_origin Time_after_epiphany] + formula, restated for its own citation -- not re-derived a second time. + + Deliberately NOT part of [T.named]: [named] is checked before + [T.sunday_slug]'s own Christmastide branch in [T.temporal]'s dispatch, but + this feast's slug, rank and colour are EXACTLY what that branch already + computes for the 7-13 January Sunday (register's own pre-existing note on + [sunday_slug]: "the key stays lectio's") -- the only field that actually + needed to change is [subject] (silently always [Temporal] before this), + so [T.temporal] applies it as a targeted override inside that branch + rather than duplicating slug/rank/colour a second time through [named]'s + 4-tuple. *) +let test_holy_family () = + (* 2026: Epiphany (6 Jan) is a Tuesday, so Holy Family falls 11 January -- + an ordinary instance, no collision with the Baptism (13 Jan). *) + Alcotest.(check string) "2026: slug is the ordinary I-post-Epiphany Sunday key" + "ef-time-after-epiphany-sunday-1" (slug_of (d 2026 1 11)); + Alcotest.(check string) "2026: II class" "class-2" (rank_of (d 2026 1 11)); + Alcotest.(check string) "2026: white" "white" (colour_of (d 2026 1 11)); + Alcotest.(check bool) "2026: subject is Lord (RG 91 entry 14, \"Festa Domini II \ + classis\" -- the mystery of the Holy Family touches Christ)" + true + (subject_of (d 2026 1 11) = Sub.Lord); + (* fix round 1 (coordinator finding 3): a primary-sourced LATIN name, + zero circularity (the calendarium's own January table and the Mass + propers' own heading, both photographic scans, word for word: + "Sanctae Familiae Iesu, Mariae, Ioseph"). No ENGLISH name -- deliberate + (temporal_ef.ml's own comment), and every OTHER temporal-cycle + candidate still carries no name of any kind. *) + Alcotest.(check (option string)) "2026: Latin name from the calendarium/propers, verbatim" + (Some "Sanctae Familiae Iesu, Mariae, Ioseph") (name_la_of (d 2026 1 11)); + Alcotest.(check (option string)) "2026: no English name (deliberate -- would read the oracle it is \ + compared against)" + None (Colitur_kernel.Names.find (office (d 2026 1 11)).Cel.names en); + Alcotest.(check (option string)) "2026: an ordinary neighbouring Sunday carries no Latin name either" + None (name_la_of (d 2026 1 18)); + (* 2019: Epiphany itself is a Sunday, so the first Sunday AFTER it is the + LATEST possible date, 13 January -- the one date Holy Family collides + with the fixed Commemoration of the Baptism of the Lord (data/ef/ + sanctoral.sexp: "commemoration-of-the-baptism-of-the-lord", also 13 + Jan, II class, subject Lord). [T.temporal]'s own resolution of WHO wins + that day is {!Rite_ef.Precedence_ef}'s business (band/disposition, see + test_precedence_ef.ml), not this function's -- this only proves + [T.temporal] itself still identifies 13 January 2019 as Holy Family's + own date, unconditionally, regardless of what else might compete for + the day once precedence is applied. *) + Alcotest.(check bool) "2019: 6 January is a Sunday" true (D.weekday (d 2019 1 6) = D.Sun); + Alcotest.(check string) "2019: Holy Family falls on the latest possible date, 13 Jan" + "2019-01-13" (D.to_iso8601 (T.holy_family_sunday 2019)); + Alcotest.(check string) "2019: still the same slug" "ef-time-after-epiphany-sunday-1" + (slug_of (T.holy_family_sunday 2019)); + Alcotest.(check bool) "2019: still subject Lord" true (subject_of (T.holy_family_sunday 2019) = Sub.Lord); + (* Every possible weekday for 6 January exercised, proving the formula + never leaves the 7-13 window empty (register's own citation above) -- + not merely the two years already checked above. *) + List.iter + (fun y -> + let hf = T.holy_family_sunday y in + Alcotest.(check bool) (Printf.sprintf "%d: Holy Family is a Sunday" y) true + (D.weekday hf = D.Sun); + Alcotest.(check bool) (Printf.sprintf "%d: Holy Family falls 7-13 January" y) true + (D.month hf = 1 && D.day hf >= 7 && D.day hf <= 13)) + [ 2019; 2020; 2021; 2022; 2023; 2024; 2025; 2026; 2027 ]; + (* Negative control: an ORDINARY Sunday after Epiphany (the 2nd) stays + [subject Temporal] -- the override is scoped to exactly one Sunday a + year, not every Time-after-Epiphany-shaped Sunday. *) + Alcotest.(check bool) "2026: the following Sunday (18 Jan, II after Epiphany) is NOT Lord" + true + (subject_of (d 2026 1 18) = Sub.Temporal) + +(* Three of [test_holy_family]'s own exhaustive-weekday sample, pinned to + their literal ISO dates and independently cross-checked against + `date -d <iso> +%A` (the same discipline test_golden.ml's own header + describes for its own weekday assertions) -- the earliest possible date + (Epiphany a Saturday), the latest (Epiphany a Sunday), and one ordinary + middle case, rather than trusting the structural "always 7-13" check + above on its own without ever anchoring it to real calendar dates. *) +let test_holy_family_no_gap_year () = + Alcotest.(check string) "2024 (6 Jan a Saturday): earliest possible date, 7 Jan" "2024-01-07" + (D.to_iso8601 (T.holy_family_sunday 2024)); + Alcotest.(check string) "2019 (6 Jan a Sunday): latest possible date, 13 Jan" "2019-01-13" + (D.to_iso8601 (T.holy_family_sunday 2019)); + Alcotest.(check string) "2025 (6 Jan a Monday): 12 Jan" "2025-01-12" + (D.to_iso8601 (T.holy_family_sunday 2025)) (* Regression for a Task 14 Validate finding, reworked (register finding 4): [named] no longer carries its own week at all -- [temporal] computes it @@ -432,6 +530,59 @@ let test_anchors_erosion_is_caught () = [ "ef-ascension"; "ef-nativity" ] (missing_from_anchors ~named_slugs ~anchors:eroded_anchors) +(* Holy Family's own anchor coverage, kept SEPARATE from the two tests above: + it is genuinely NOT one of [T.named]'s outputs (test_holy_family's own + comment explains why -- [T.temporal] applies it as a targeted override + inside [T.sunday_slug]'s branch instead), so [named_slugs_for_year]'s scan + cannot see it, and folding it into [test_anchors_cover_all_named_days]/ + [test_anchors_erosion_is_caught] would misrepresent what those two are + actually proving ("everything [named] produces"). This proves the same + two properties directly against [T.holy_family_sunday] instead: the entry + is present, AND deleting it is caught -- {!Validate}'s own production + anchor check (validate.ml, register/spec §5.7) calls [temporal], not + [named], so it is unaffected by this distinction and already covers this + entry regardless; this is this file's own unit-level guard against the + same erosion. + + CORRECTED, fix round 1 (coordinator finding 5): the original version of + this test built [eroded] by [List.filter]-ing the exact pair it then + asserted [List.mem ... eroded = false] against -- tautological (removing + an element from a list and then checking it is not there proves nothing + about a DETECTOR; it proves [List.filter] works). The genuine detector + the OTHER erosion test above actually exercises is [missing_from_anchors] + itself, called with an "expected" set sourced INDEPENDENTLY of + [T.anchors] ([named_slugs_for_year], via [T.named] -- a different + function entirely) -- the interesting claim is that the SAME slug is + confirmed by two independent computations, not merely that a filtered + list lacks what was filtered out of it. Reworked to match: the + "expected" side here is [T.sunday_slug expected_date] -- computed via + season/weekday logic, not read out of [T.anchors]'s own hand-typed + string -- so the presence check below is a genuine cross-check between + two independent sources, and the erosion check reuses the real + [missing_from_anchors] detector rather than re-deriving a bespoke, + self-referential one. *) +let test_holy_family_anchor_present_and_erosion_is_caught () = + let y = 2026 in + let expected_date = T.holy_family_sunday y in + let expected_slug = + match T.sunday_slug expected_date with + | Some s -> s + | None -> Alcotest.fail "sanity: holy_family_sunday must itself be a Sunday" + in + Alcotest.(check (list string)) + "the anchor entry exists: T.sunday_slug's own independent computation is not missing from T.anchors" [] + (missing_from_anchors ~named_slugs:[ expected_slug ] ~anchors:(anchor_slugs y)); + (* Erosion: with the entry struck out of [anchors], [missing_from_anchors] + -- the SAME real detector, not a bespoke re-check -- must now report it + missing, using [expected_slug]'s own independent source ([sunday_slug]) + as the thing being checked FOR, not derived from the erosion itself. *) + let eroded_anchors = List.filter (fun s -> s <> expected_slug) (anchor_slugs y) in + Alcotest.(check (list string)) "the erosion is caught: missing_from_anchors reports it, and only it" + [ expected_slug ] + (missing_from_anchors ~named_slugs:[ expected_slug ] ~anchors:eroded_anchors); + Alcotest.(check string) "sanity: temporal itself is unaffected by the anchors-list erosion" + expected_slug (slug_of expected_date) + let test_totality () = (* Every day of 2026 yields an office without raising. Not a slug re-validation -- Slug.t is a private string validated on construction, @@ -453,6 +604,8 @@ let suite_extra = Alcotest.test_case "nativity octave" `Quick test_nativity_octave; Alcotest.test_case "week numbers" `Quick test_week_numbers; Alcotest.test_case "sunday slugs" `Quick test_sunday_slugs; + Alcotest.test_case "holy family (RG 17(b))" `Quick test_holy_family; + Alcotest.test_case "holy family: no gap year (RG 17(b))" `Quick test_holy_family_no_gap_year; Alcotest.test_case "week/sunday_slug agree" `Quick test_week_sunday_slug_agree; Alcotest.test_case "resumed sundays" `Quick test_resumed_sundays; Alcotest.test_case "ferial slugs and ranks" `Quick test_ferial_slugs_and_ranks; @@ -465,6 +618,8 @@ let suite_extra = Alcotest.test_case "totality" `Quick test_totality; Alcotest.test_case "anchors cover all named days" `Quick test_anchors_cover_all_named_days; + Alcotest.test_case "holy family anchor present, erosion caught" `Quick + test_holy_family_anchor_present_and_erosion_is_caught; Alcotest.test_case "anchors erosion is caught" `Quick test_anchors_erosion_is_caught ] let suite = |
