From 6e62e499aed1c5a31a18bcf5e9eaa7f2b0c4ff2e Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 27 Aug 2026 00:37:45 +0200 Subject: fix(of): break the movable-BVM-memorial tie per a CDWDS Notification Mary, Mother of the Church (Easter+50) and the Immaculate Heart of Mary (Easter+69) are both movable obligatory memorials that can land on a FIXED obligatory memorial's own date -- e.g. 2011-06-13 and 2015-06-13, both against Anthony of Padua. Both candidates banded identically at Tabula III.10 (100, both universal Memoria_obligatoria), a tie band could not break, so Precedence.resolve's kernel-level alphabetical Slug.compare tie-break silently picked instead -- 892 days for Mater Ecclesiae and 1109 for the Immaculate Heart across 1583-9999, all one shape (measured against the pre-fix alphabetical order directly, zero unexplained). Searched the corpus rather than inventing a rule. colitur's two previously-checked primary sources really are silent: the 2018 decree (AAS 110, 437-438) never addresses universal-calendar coincidence, and Normae n. 14's third clause resolves only two AD-LIBITUM memorials colliding. A third source resolves it: the CDWDS's own "Notification on the Memorial of the Blessed Virgin Mary, Mother of the Church" (24 March 2018 -- same dicastery and signatories as the decree; published only on vatican.va, confirmed absent from the AAS 2018 combined index) states that when this memorial "coincides with another Memorial of a Saint or Blessed... the Memorial of the Blessed Virgin Mary is to prevail," and extends the identical reasoning to the Immaculate Heart of Mary by name. This is a rubric, not colitur's own reasoned choice. Implemented as a sub-rank of 95, strictly between Tabula entries 9 and 10 -- the same "fits between neighbours" technique this module already uses elsewhere -- scoped to the two named slugs rather than [subject = Bvm], since the Notification's own reasoning is specifically about a memorial's link to a movable solemnity of the Lord, not a blanket Marian precedence. Golden pin updated: test_rite_of.ml's 2011-06-13 tie now pins Mary, Mother of the Church as observed (was Anthony of Padua, labelled "known arbitrary"), with the header rewritten to state the outcome now rests on the Notification, not a coin-flip. The litcal oracle allow-list gains L10: litcal's own grade vocabulary has no representation for a 95 sub-rank (everything Memoria_obligatoria reads "Memorial"), a representational gap in litcal, not a computational disagreement -- the identity axis is untouched and needed no new entry. --- test/test_litcal_of.ml | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) (limited to 'test/test_litcal_of.ml') diff --git a/test/test_litcal_of.ml b/test/test_litcal_of.ml index 6a638a7..9dfc5ba 100644 --- a/test/test_litcal_of.ml +++ b/test/test_litcal_of.ml @@ -640,6 +640,44 @@ let is_l7_2033_tie_identity (l : litcal_row) = exact date shape) -- NOT fixed here, per this task's own brief. *) let is_l8_l9_joseph_2035 (l : litcal_row) = String.equal l.event_key "StJoseph" && String.equal l.date "2035-03-17" +(* L10 -- W3 (of-known-wrongs, 2026-08-26). Precedence_of.band now gives the + two SHIPPED movable BVM obligatory memorials (Mary, Mother of the Church, + event_key "MaryMotherChurch"; the Immaculate Heart of Mary, event_key + "ImmaculateHeart") a sub-rank of 95 -- strictly between Tabula entries 9 + and 10, never the ordinary 100 -- so a real collision with a FIXED + obligatory memorial (e.g. 2011-06-13, Mater Ecclesiae vs Anthony of + Padua) resolves in the Marian memorial's favour instead of falling to + Colitur_kernel.Precedence.resolve's own alphabetical tie-break. See + Rite_of.Precedence_of.movable_bvm_memorials's own comment for the primary + source (the CDWDS's own 24 March 2018 Notification) and the full + reasoning. + + litcal's own [grade_lcl] text is coarser than this: EVERY + Memoria_obligatoria reads "Memorial", with no way to represent colitur's + own finer sub-rank -- so [expected_bands]'s [100;110] bucket cannot match + 95. This is a genuine, cited REPRESENTATIONAL gap in litcal's own + vocabulary, not a computational disagreement: the IDENTITY axis (which + day is actually observed) is untouched by this fix and stays unexplained + here on purpose -- [test_identity_matches_or_is_explained] needed no new + entry, confirmed by running it after this fix with no change. + + Fires on every fixture row naming either event_key where colitur's own + band is exactly 95 -- ANY date that entity is observed, not only an + actual collision date, because the sub-rank is unconditional (see + [movable_bvm_memorials]'s own comment for why: neither entity's date + range can ever reach a Tabula 1-9 day, so 95 is always safe and never + needs a live-collision guard). 15 rows over this fixture's 12 years, + measured directly against a real `dune test` run, not guessed: + MaryMotherChurch in 11 of 12 years (2033's own occurrence is a DIFFERENT, + already-explained shape, band 30 not 95 -- not this entity's date at + all that year; see L6) and ImmaculateHeart in 4 (the other 8 years' + ImmaculateHeart-window rows are excluded upstream by litcal's own + "optional memorial" grade quirk, [expected_bands]'s [None] case above, + or by L6/L7's own 2033 tie). *) +let is_l10_movable_bvm_subrank (l : litcal_row) band = + (String.equal l.event_key "MaryMotherChurch" || String.equal l.event_key "ImmaculateHeart") + && band = 95 + let test_grade_matches_or_is_explained () = let litcal = litcal_rows () in let allow_list = load_allow_list () in @@ -663,6 +701,7 @@ let test_grade_matches_or_is_explained () = else if is_l5_mary_magdalene l then bump "L5" else if is_l6_2033_tie_grade l then bump "L6" else if is_l8_l9_joseph_2035 l then bump "L8" + else if is_l10_movable_bvm_subrank l band then bump "L10" else unexplained := Printf.sprintf "%s: colitur band=%d, litcal grade=%S (event_key=%S, expected one of [%s])" l.date band @@ -679,7 +718,7 @@ let test_grade_matches_or_is_explained () = match List.assoc_opt id by_id with | None -> Alcotest.failf "allow-list entry %s is used by the comparator but not declared in %s" id allow_list_path | Some e -> Alcotest.(check int) (Printf.sprintf "%s expected_rows (grade)" id) e.expected_rows actual) - [ "L4"; "L5"; "L6"; "L8" ] + [ "L4"; "L5"; "L6"; "L8"; "L10" ] (* The complement, mirroring [test_unwitnessed_ordinary_time_is_counted]: every row [expected_bands] returns [None] for, classified by which of @@ -796,7 +835,7 @@ let test_no_holy_family_2033_divergence () = (* [is_holy_family_2033]'s own header. *) (* ---------------------------------------------------------------------- *) -let recognized_allow_ids = [ "L1"; "L4"; "L5"; "L6"; "L7"; "L8"; "L9" ] +let recognized_allow_ids = [ "L1"; "L4"; "L5"; "L6"; "L7"; "L8"; "L9"; "L10" ] let test_allow_list_has_no_orphan_entries () = let allow_list = load_allow_list () in -- cgit v1.3