From 60e87914718dd2c8e69bb89d8cc48d6bef9bfc74 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 22 Aug 2026 16:40:07 +0200 Subject: feat(ef): implement the calendarium's bissextile February shift The Missale Romanum's calendarium footnote for February (LT.txt:5011-5014, scan-corroborated scan2.txt:3050-3058): in a leap year the sixth kalends of March (24 February) is doubled rather than a 29th day appended, so St Matthias moves 24->25 February and St Gabriel of Our Lady of Sorrows 27->28, with 24 February itself carrying no fixed office that year. data/ef/expected-divergences-lms.sexp entry L3 tracked this as an open gap. Implemented as the rubric's own general mechanism (every fixed entry from 24 through 28 February shifts one civil day later in a leap year), not as "move these two named saints": the two readings are indistinguishable on shipped data (nothing else is fixed in that window) and diverge only for a future --overlay entry in the same range, which the mechanism reading covers correctly and a two-saints special case would not. This project has already been bitten once (RG 16(a)) by a rule implemented against shipped data's coincidental shape rather than the rubric itself. Kernel stays rite-agnostic: Rite.t gains a fixed_key field (Date.t -> (int * int) option), the same seam easter already establishes, read only by Layer.on_date's FIXED half -- the movable half (Easter_offset/Nth_weekday) is untouched. Default is the identity mapping, an optional argument on on_date, so every existing caller and every rite that supplies nothing is byte-identical to before this field existed. Date.is_leap is exposed in date.mli (already existed in date.ml) so the rite reads the kernel's own single-sourced leap-year definition. The EF implementation lives in Rite_ef.Temporal_ef.bissextile_fixed_key, fully cited, wired into Rite_ef.context. Blast radius, measured over the full 1583-9999 domain (Calendar.year run twice per civil year, real fixed_key vs identity, every day diffed): all 2,041 leap years in the domain show a visible change for each saint; 6,983 individual liturgical days change total, zero unclassified, across four shapes (Matthias vacates 24 Feb in 1,803 years / occupies 25 Feb in 1,811; Gabriel vacates 27 Feb in 1,699 / occupies 28 Feb in 1,670). L3's own prior estimate (1,650 years, observed-outright only) is confirmed close on the same definition (1,677 measured); the broader observed-or- commemorated count is higher, not lower, showing the narrow estimate undercounted visible impact rather than overcounting it. The lectio differential (2005-2050 fixture) gains a new cited entry, C40 in data/ef/expected-divergences.sexp: lectio implements no such shift, so every leap year in its window now diverges on the two shifted days (21 rows, not the naive 44 -- the comparator never compares commemorations, so a side where the shifted saint has zero comparator-visible footprint on both engines produces no row). The LMS Ordo's 2023-2024 fixture -- the window L3 was originally found in -- now matches on the Creed comparison; its formulary-override bucket count is corrected 182->181 (24 February leaves the counted Proper population, becoming Own_slug-sourced). The missalemeum oracle fixtures (2026-2027, 2038, 2035) cover no leap year and are unaffected. L3 is closed: converted from an active allow-list record to a prose closure paragraph (L1's own precedent), citation preserved verbatim, fix and measured blast radius recorded. The id-list assertion narrows to [L4] alone, and the now-dead "2024-02-24" -> "L3" date mapping is removed. Six golden values pinned across four new test cases: Matthias in a leap year (both the vacated and occupied sides) and a common-year control; Gabriel likewise, deliberately choosing a leap year where he is admitted only as an ordinary commemoration rather than winning outright, a different shape from Matthias's. Mutation-tested: forcing fixed_key back to identity reddens exactly five test cases -- the differential's Layer C count-pin (C40 drops to 0 actual vs 21 expected), the LMS Ordo's Creed and formulary-bucket checks, and both new golden pins -- confirming the tests actually exercise the fix. Reverted; dune test, the exhaustive sweep, and make check are all green. --- test/test_lms_ordo.ml | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) (limited to 'test/test_lms_ordo.ml') diff --git a/test/test_lms_ordo.ml b/test/test_lms_ordo.ml index 84c4776..219e51f 100644 --- a/test/test_lms_ordo.ml +++ b/test/test_lms_ordo.ml @@ -502,13 +502,15 @@ let make_suite ~label ~fixture_path ~fixture_sha256 ~window_first ~window_last ~ this function declared [by_id]/[explained_counts] but never actually populated the latter (the mechanism this comment sits in) -- L1 was already CLOSED by the time that task shipped, so it had nothing real - to explain and the gap went unexercised. L3/L4 (Witnesses task, - 2026-08-22, both 2023-2024-only) are the first live tests of it. *) - let allow_list_id_for_date d = - if String.equal d "2024-02-24" then Some "L3" - else if String.equal d "2023-12-24" then Some "L4" - else None - in + to explain and the gap went unexercised. L4 (Witnesses task, + 2026-08-22, 2023-2024-only) is the first live test of it. L3's own + "2024-02-24" branch (bissextile-shift task, 2026-08-22) is REMOVED, + not left dead: the fix makes that date's Creed match outright, so the + branch would never fire again, and a stale date->id mapping for a + now-closed entry is exactly the kind of thing this file's own + "must be visible as a real change" discipline (this comment's own + opening sentence) argues against leaving in place. *) + let allow_list_id_for_date d = if String.equal d "2023-12-24" then Some "L4" else None in let test_creed_matches_or_is_explained () = let ordo = ordo_rows fixture_path in let colitur = colitur_rows ~year_lo ~year_hi ~window_first ~window_last in @@ -651,17 +653,29 @@ let allow_list_path = "../data/ef/expected-divergences-lms.sexp" ever looks at its own explained_counts. Fixed to the two ids this task added (L3, L4) -- L1 is CLOSED/removed and L2 is deliberately prose-only (see the sexp file's own header on why), so neither is expected here. *) -let test_allow_list_ids_are_exactly_l3_l4 () = +(* L3 CLOSED (bissextile-shift task, 2026-08-22): removed from the active + sexp record set -- see its own now-prose closure note in + data/ef/expected-divergences-lms.sexp for the fix and the measured + blast radius. Only L4 remains active. *) +let test_allow_list_ids_are_exactly_l4 () = let ids = List.sort String.compare (List.map (fun e -> e.id) (load_allow_list allow_list_path)) in - Alcotest.(check (list string)) "the LMS allow-list declares exactly L3 and L4" [ "L3"; "L4" ] ids + Alcotest.(check (list string)) "the LMS allow-list declares exactly L4" [ "L4" ] ids -let suite_allow_list = ("lms-ordo-allow-list", [ Alcotest.test_case "declares exactly L3 and L4" `Quick test_allow_list_ids_are_exactly_l3_l4 ]) +let suite_allow_list = ("lms-ordo-allow-list", [ Alcotest.test_case "declares exactly L4" `Quick test_allow_list_ids_are_exactly_l4 ]) let suite_2023_2024 = make_suite ~label:"lms-ordo-2023-2024" ~fixture_path:"fixtures/lms-ordo-2023-2024.sexp" ~fixture_sha256:"c8d9d4b790f6b4438e932f70dad8ebe9a0316662f1eb02c26248e04c7be1a62b" ~window_first:"2023-12-01" ~window_last:"2024-12-31" ~year_lo:2022 ~year_hi:2025 ~allow_list_path ~expected_rows:397 - ~expected_bvm_votive:12 ~expected_proper:182 ~expected_common:2 ~expected_preceding_sunday:61 + (* expected_proper 182 -> 181 (bissextile-shift task, 2026-08-22): 24 + February 2024, previously Matthias's own Proper Mass, is now the + Ember Saturday of Lent's own temporal proper -- Own_slug-sourced, not + Proper, so it leaves this bucket's population rather than merely + changing which slug's proper it is (see this file's own header on why + Own_slug is excluded). Matthias himself moves to 25 February, an + I-class Sunday that admits him not even as a commemoration (RG16(a)), + so his own Mass is not said anywhere in this window any more. *) + ~expected_bvm_votive:12 ~expected_proper:181 ~expected_common:2 ~expected_preceding_sunday:61 ~expected_ascension_week:1 let suite_2024_2025 = -- cgit v1.3