diff options
| -rw-r--r-- | data/of/expected-divergences-litcal.sexp | 31 | ||||
| -rw-r--r-- | lib/rites/rite_of/precedence_of.ml | 87 | ||||
| -rw-r--r-- | test/test_litcal_of.ml | 43 | ||||
| -rw-r--r-- | test/test_precedence_of.ml | 60 | ||||
| -rw-r--r-- | test/test_rite_of.ml | 65 |
5 files changed, 256 insertions, 30 deletions
diff --git a/data/of/expected-divergences-litcal.sexp b/data/of/expected-divergences-litcal.sexp index e0b8b2a..633d2e0 100644 --- a/data/of/expected-divergences-litcal.sexp +++ b/data/of/expected-divergences-litcal.sexp @@ -178,3 +178,34 @@ (verdict colitur) (note "2035-03-17 (StJoseph): the same known limitation L8 records for grade, IDENTITY axis.") (expected_rows 1)) + +; 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; +; the Immaculate Heart of Mary) a sub-rank of 95, strictly between Tabula +; entries 9 and 10, rather than 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. Cited +; on a genuine primary source, not colitur's own invented rule: the CDWDS's +; own "Notification on the Memorial of the Blessed Virgin Mary, Mother of +; the Church" (24 March 2018 -- the same dicastery and signatories as the +; 11 February 2018 decree itself; published only on vatican.va, confirmed +; NOT in the AAS 2018 combined index) -- "In casu occurrentiae huius +; memoriae cum alia memoria ... in casu occurrentiae cum alia memoria +; alicuius Sancti vel Beati, ... memoria B.M.V. praevalere debet." +; +; litcal's own grade_lcl text is coarser than this: every +; Memoria_obligatoria reads "Memorial" regardless of colitur's own finer +; sub-rank, so the [100;110] bucket cannot represent 95 -- a genuine +; representational gap in litcal's own vocabulary, not a computational +; disagreement (the two engines still agree on WHICH day is observed; the +; identity axis is untouched by this fix and needed no new entry). Fires on +; every fixture row naming either entity where colitur's own band is +; exactly 95, on ANY date that entity is observed, not only a live +; collision date, because the sub-rank is unconditional. 15 rows over this +; fixture's 12 years, measured directly, not guessed. +((id L10) + (citation "CDWDS Notification on the Memorial of the BVM, Mother of the Church (24 March 2018); precedence_of.ml's movable_bvm_memorials") + (verdict colitur) + (note "MaryMotherChurch (11 years) and ImmaculateHeart (4 years): colitur bands both at 95 (between Tabula 9 and 10), which litcal's own coarse \"Memorial\" grade text cannot represent -- a cited design choice, not a defect.") + (expected_rows 15)) diff --git a/lib/rites/rite_of/precedence_of.ml b/lib/rites/rite_of/precedence_of.ml index 5344374..0e2c246 100644 --- a/lib/rites/rite_of/precedence_of.ml +++ b/lib/rites/rite_of/precedence_of.ml @@ -124,6 +124,87 @@ let is_nativity_octave_day (ctx : Vocab_of.season Precedence.context) = let is_lenten_feria (ctx : Vocab_of.season Precedence.context) = ctx.season = Vocab_of.Lent +(* W3 (of-known-wrongs, 2026-08-26). Both are shipped MOVABLE obligatory + memorials of the BVM (data/of/calendar-2002.sexp: [immaculate-heart-of-mary], + Easter_offset 69; data/of/amendments/006-mary-mother-of-the-church.sexp: + [mary-mother-of-the-church], Easter_offset 50), and both can land on a + FIXED obligatory memorial's own date in a real year -- confirmed, not + merely possible: e.g. 2011-06-13 (Mater Ecclesiae vs Anthony of Padua) and + 2015-06-13 (Immaculate Heart vs Anthony of Padua, three years apart via + the other offset). Both candidates then band identically at Tabula + III.10 (100, both universal Memoria_obligatoria) -- a tie {!band} could + not break on its own, so {!Colitur_kernel.Precedence.resolve}'s kernel + tie-break (Slug.compare, alphabetical) silently decided instead: 892 days + for Mater Ecclesiae and 1109 for the Immaculate Heart across 1583-9999 + (measured with the fix applied and compared against the plain alphabetical + order every one of those days would otherwise have taken -- see this + task's own report for the full sweep). + + NEITHER of colitur's two checked primary sources resolves this: the 2018 + decree inscribing Mater Ecclesiae (AAS 110, 437-438) is silent on + universal-calendar coincidence (its own coincidence clause covers only a + PARTICULAR calendar keeping the celebration on a different day at a + higher grade); Normae n. 14's third clause ("Si eodem die plures + inscribuntur in calendario memoriae ad libitum, una tantum celebrari + potest, omissis ceteris.") resolves only memoriae AD LIBITUM colliding + with each other, and n. 14's second clause covers only the Lenten-feria + case. Two OBLIGATORY memorials colliding is genuinely unaddressed by + either. + + RESOLVED on a THIRD, later primary source, found by searching the + corpus (docs/research/of/), not invented: the Congregation for Divine + Worship and the Discipline of the Sacraments' own "Notification on the + Memorial of the Blessed Virgin Mary, Mother of the Church" (24 March + 2018 -- the SAME dicastery, the SAME two signatories, Robertus Card. + Sarah [Praefectus] and Arturus Roche [Archiepiscopus a Secretis], as the + 11 February 2018 decree itself). Published only on the Dicastery's own + site (vatican.va/roman_curia/congregations/ccdds/documents/ + rc_con_ccdds_doc_20180324_notificazione-mater-ecclesiae_{en,it,la,sp}.html); + confirmed NOT listed in the AAS 2018 combined index + (docs/research/of/aas-indices/aas-indice2018.pdf, checked directly via + pdftotext) alongside the decree it follows up, which IS indexed there + (p.437) -- so this is a genuine dicasterial clarification, not an AAS + promulgation, and is cited as such rather than papered over as "AAS n.". + Its own operative Latin: "In casu occurrentiae huius memoriae cum alia + memoria principia generalia normarum universalium de Anno liturgico et + de Calendario (cf. Tabula dierum liturgicorum, n. 60) sequenda sunt. Cum + autem memoria B.M.V. Ecclesiae Matris sit Pentecoste coniuncta, sicut + pariter memoria Immaculati Cordis B.M.V. celebrationi Sacratissimi + Cordis Iesu coniuncta est, in casu occurrentiae cum alia memoria + alicuius Sancti vel Beati, iuxta liturgicam traditionem praestantiae + inter personas, memoria B.M.V. praevalere debet." The Dicastery's own + official English: "In the case where this Memorial coincides with + another Memorial the principles of the Universal Norms... are to be + followed (cf. Table of Liturgical Days, n. 60). Given that the Memorial + of the Blessed Virgin Mary, Mother of the Church is linked to Pentecost, + as the Memorial of the Immaculate Heart of the Blessed Virgin Mary is + similarly linked to the celebration of the Most Sacred Heart of Jesus, + then, in the case where it coincides with another Memorial of a Saint or + Blessed, and following the liturgical tradition of pre-eminence amongst + persons, the Memorial of the Blessed Virgin Mary is to prevail." The + Notification itself names BOTH memorials by this same analogy -- Mater + Ecclesiae's own rule IS the Immaculate Heart's pre-existing one, restated + -- so both are listed here, not only the one this task's own defect + report was filed against. + + THIS IS THEREFORE A RUBRIC, NOT COLITUR'S OWN REASONED CHOICE: a + primary, dicastery-issued clarification of universal application, not an + analogy imported from the EF's own RG 91 entry 14 (which was the + fallback this task's brief authorized if nothing resolving turned up). + + Implemented as a sub-rank strictly BETWEEN Tabula entries 9 and 10 (95, + not 100/110) -- the same "fits between neighbours" technique this + module's own header comment documents for exactly this situation. Never + climbs above entries 1-9: neither memorial's own date range (Easter+50 = + 11 May-14 June; Easter+69 = 30 May-3 July) can ever reach a Tabula 1-9 + day, so 95 is inert there. Deliberately NOT a [subject = Bvm] rule: the + Notification's own reasoning is specifically that EACH of these two + memorials is tied to a movable SOLEMNITY OF THE LORD (Pentecost, the + Sacred Heart) -- a FIXED BVM memorial (e.g. Our Lady of Guadalupe, 12 + December) gets no such boost, because nothing in the Notification's own + text extends this to Marian memorials in general. *) +let movable_bvm_memorials = [ "mary-mother-of-the-church"; "immaculate-heart-of-mary" ] + let band (ctx : Vocab_of.season Precedence.context) (c : Vocab_of.rank Precedence.candidate) : int = let temporal = c.Precedence.origin = Precedence.Temporal in @@ -191,6 +272,12 @@ let band (ctx : Vocab_of.season Precedence.context) temporal && (is_late_advent_feria ctx || is_nativity_octave_day ctx || is_lenten_feria ctx) then 90 (* Tabula II.9 *) + else if + c.cel.rank = Vocab_of.Memoria_obligatoria && List.mem (slug_of c) movable_bvm_memorials + then + 95 (* Between Tabula 9 and 10 -- W3, see [movable_bvm_memorials]'s own + comment above for the Notification this cites and why it is + scoped to these two slugs rather than [subject = Bvm]. *) else if c.cel.rank = Vocab_of.Memoria_obligatoria && not (is_proper layer) then 100 (* Tabula III.10 -- universal or proper; the [layer] test is what separates entry 10 from entry 11. Unlike entries 3, 5 and 7, "or 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 diff --git a/test/test_precedence_of.ml b/test/test_precedence_of.ml index 23c26c2..e2e8540 100644 --- a/test/test_precedence_of.ml +++ b/test/test_precedence_of.ml @@ -524,6 +524,60 @@ let test_all_souls_beats_an_ordinary_sunday () = Alcotest.(check bool) "All Souls, not the Sunday, is observed" true (resolution.Precedence.observed = all_souls_candidate) +(* W3 (of-known-wrongs, 2026-08-26). Mary, Mother of the Church (Easter+50) + and a fixed obligatory memorial (e.g. Anthony of Padua, 13 June) both + used to band at Tabula III.10 (100) whenever they coincide -- a tie + {!Rite_of.Precedence_of.band} could not break, silently resolved instead + by {!Colitur_kernel.Precedence.resolve}'s own generic 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) this fix cites: the Marian memorial now bands at 95 and + always prevails. This is the direct unit-level counterpart to + test_rite_of.ml's own end-to-end + [test_mater_ecclesiae_anthony_tie_2011_resolved] -- this one calls + {!Colitur_kernel.Precedence.resolve} directly, the same shape every + other test in this file uses, and checks BOTH real years the sweep in + this task's own report found (2011 for Mater Ecclesiae, 2015 for the + Immaculate Heart -- three years apart via the other offset, colliding + with the SAME rival, Anthony of Padua, both confirmed via + {!Colitur_kernel.Computus.gregorian_easter} rather than trusted). *) +let test_movable_bvm_memorial_beats_a_fixed_one () = + let anthony = + mk ~slug:"anthony-of-padua-priest-and-doctor" ~rank:Vocab_of.Memoria_obligatoria + ~origin:Precedence.Sanctoral () + in + List.iter + (fun (label, slug, easter_offset, year, expected_date) -> + let movable = + mk ~slug ~rank:Vocab_of.Memoria_obligatoria ~subject:Subject.Bvm + ~origin:Precedence.Sanctoral () + in + Alcotest.(check bool) + (Printf.sprintf "%s: alphabetically \"anthony-of-padua...\" sorts BEFORE %S -- confirms this \ + test exercises the tie-break the fix overrides, not a vacuous check" + label slug) + true (String.compare "anthony-of-padua-priest-and-doctor" slug < 0); + let easter = Computus.gregorian_easter year in + let d = Date.add_days easter easter_offset in + Alcotest.(check string) + (Printf.sprintf "%s: Easter+%d %d really is %s" label easter_offset year expected_date) + expected_date (Date.to_iso8601 d); + let temporal_c = + mk ~slug:"of-ordinary-time-feria" ~rank:Vocab_of.Feria ~origin:Precedence.Temporal () + in + let ctx = { Precedence.date = d; season = Vocab_of.Ordinary_time; weekday = Date.weekday d } in + let resolution = + Precedence.resolve Rite_of.Precedence_of.rules ctx ~temporal:temporal_c + ~sanctoral:[ anthony; movable ] + in + Alcotest.(check bool) + (Printf.sprintf "%s prevails over Anthony of Padua, not the alphabetical order" label) + true (resolution.Precedence.observed = movable)) + [ ("Mary, Mother of the Church vs Anthony of Padua", "mary-mother-of-the-church", 50, 2011, + "2011-06-13"); + ("Immaculate Heart of Mary vs Anthony of Padua", "immaculate-heart-of-mary", 69, 2015, + "2015-06-13") ] + let suite = ( "precedence-of", [ Alcotest.test_case "Tabula part I (entries 1-4)" `Quick test_part1; @@ -549,5 +603,9 @@ let suite = test_target_is_free_of_entries_1_to_8; Alcotest.test_case "fix round 2: shipped All Souls beats an ordinary per-annum Sunday (2025-11-02)" `Quick - test_all_souls_beats_an_ordinary_sunday ] + test_all_souls_beats_an_ordinary_sunday; + Alcotest.test_case + "W3: a movable BVM obligatory memorial beats a fixed one, per the CDWDS's 24 March 2018 \ + Notification" `Quick + test_movable_bvm_memorial_beats_a_fixed_one ] @ List.map QCheck_alcotest.to_alcotest [ prop_band_total ] ) diff --git a/test/test_rite_of.ml b/test/test_rite_of.ml index 42c9c98..1de3614 100644 --- a/test/test_rite_of.ml +++ b/test/test_rite_of.ml @@ -373,21 +373,32 @@ let test_holy_family_fallback_1583_now_correct () = worked example that amendment header itself names (Easter 24 April 2011, so Easter+50 = 13 June -- re-derived here, not trusted from the comment). - KNOWN ARBITRARY, not KNOWN WRONG: unlike the two tests above, there is no - rubric this contradicts. The 2018 decree (AAS 110, 437-438) is silent on - a universal-calendar coincidence with an existing obligatory memorial, and - Normae n. 14's own third clause resolves only the AD-LIBITUM/AD-LIBITUM - case ("Si eodem die plures inscribuntur in calendario memoriae ad - libitum") -- it does not reach two OBLIGATORY memorials. So colitur's - Slug.compare tie-break ("anthony-of-padua-priest-and-doctor" sorts before - "mary-mother-of-the-church") is not this project inventing a rule; it is - the kernel's own generic, rite-agnostic fallback, applied here for lack of - a rite-specific one. This test pins that CURRENT behaviour so a future - Calendar/kernel change, a slug rename, or a future overlay cannot flip - which of the two is observed with no test going red -- the same shape of - invisible-drift risk the EF side's RG 113/[band] fidelity work (`ef-rg16a`, - register ยง6.1) eliminated on that side. *) -let test_mater_ecclesiae_anthony_tie_2011_known_arbitrary () = + RESOLVED (W3, of-known-wrongs, 2026-08-26) ON A RUBRIC, NOT COLITUR'S OWN + JUDGEMENT -- this test used to pin the OPPOSITE outcome ("KNOWN + ARBITRARY", the kernel's generic alphabetical Slug.compare tie-break, + for lack of anything rite-specific). That framing is now WRONG, not + merely superseded: colitur's own two previously-checked primary sources + really are silent (the 2018 decree, AAS 110, 437-438, on universal- + calendar coincidence; Normae n. 14's third clause, which resolves only + the AD-LIBITUM/AD-LIBITUM case, "Si eodem die plures inscribuntur in + calendario memoriae ad libitum") -- but a THIRD primary source, found by + searching the corpus rather than invented, does resolve 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 plainly that when this memorial "coincides with + another Memorial of a Saint or Blessed... the Memorial of the Blessed + Virgin Mary is to prevail." See {!Rite_of.Precedence_of.movable_bvm_ + memorials}'s own comment for the full Latin, the official English, and + why the same rule also covers the Immaculate Heart of Mary (the + Notification names both, by the identical link-to-a-Lord's-solemnity + argument). {!Rite_of.Precedence_of.band} now gives both a sub-rank of + 95, strictly between Tabula entries 9 and 10, so this is no longer a + coin-flip the kernel settles for lack of a rite rule -- it is the rite + rule, cited. This test still pins CURRENT behaviour, for the identical + invisible-drift reason the original comment gave; what changed is which + behaviour is correct to pin. *) +let test_mater_ecclesiae_anthony_tie_2011_resolved () = let easter_2011 = Computus.gregorian_easter 2011 in Alcotest.(check string) "2011 Easter is really 24 April (the Easter+50 trigger)" "2011-04-24" (Date.to_iso8601 easter_2011); @@ -404,18 +415,18 @@ let test_mater_ecclesiae_anthony_tie_2011_known_arbitrary () = let days = Cal.year real_of_rite real_of_layer 2010 in let june13 = Array.to_list days |> List.find (fun d -> Date.compare d.LD.date tie_date = 0) in Alcotest.(check string) - "KNOWN ARBITRARY (no rubric decides this; see this test's own header): the kernel's generic \ - alphabetical tie-break observes Anthony of Padua, not Mary Mother of the Church" - "anthony-of-padua-priest-and-doctor" (slug_of june13.LD.observed); - let mater_ecclesiae_omitted = - List.exists (fun (c, _) -> slug_of c = "mary-mother-of-the-church") june13.LD.omitted + "RESOLVED (W3): the CDWDS's own 24 March 2018 Notification (see this test's own header) makes \ + Mary, Mother of the Church prevail, not Anthony of Padua" + "mary-mother-of-the-church" (slug_of june13.LD.observed); + let anthony_omitted = + List.exists (fun (c, _) -> slug_of c = "anthony-of-padua-priest-and-doctor") june13.LD.omitted in Alcotest.(check bool) - "and Mary, Mother of the Church is recorded in `omitted`, not silently dropped -- {!Colitur_kernel \ + "and Anthony of Padua is recorded in `omitted`, not silently dropped -- {!Colitur_kernel \ .Liturgical_day.t.omitted}'s own contract" - true mater_ecclesiae_omitted; - Alcotest.(check bool) "she carries no commemoration either (OF's own admit always returns [])" false - (List.exists (fun (c, _) -> slug_of c = "mary-mother-of-the-church") june13.LD.commemorations) + true anthony_omitted; + Alcotest.(check bool) "he carries no commemoration either (OF's own admit always returns [])" false + (List.exists (fun (c, _) -> slug_of c = "anthony-of-padua-priest-and-doctor") june13.LD.commemorations) let suite = ( "Rite_of (real data: assembled Rite.t end-to-end via Calendar/Validate)", @@ -434,6 +445,6 @@ let suite = Day is a Sunday (1583)" `Quick test_holy_family_fallback_1583_now_correct; Alcotest.test_case - "KNOWN ARBITRARY pinned: Mary Mother of the Church vs Anthony of Padua tie, 2011-06-13 \ - (Easter+50)" `Quick - test_mater_ecclesiae_anthony_tie_2011_known_arbitrary ] ) + "RESOLVED (W3): Mary Mother of the Church beats Anthony of Padua's tie, 2011-06-13 \ + (Easter+50), per the CDWDS's 24 March 2018 Notification" `Quick + test_mater_ecclesiae_anthony_tie_2011_resolved ] ) |
