From 761ae859d73660bcfaa59581312989cb942e704d Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 22 Aug 2026 22:40:07 +0200 Subject: feat(ef): RG 111(a), the sung-Mass commemoration cap Item 1 of Phase 3 (celebrant-rubrics-phase1): the "at Low Mass" commemoration-placement rule the design spec recorded as unread. It is not a new rule -- it is the sung/low axis of RG 111, which colitur already implements. RG 111(a) (LT.txt, "Ratio admittendi commemorationes"): a liturgical day of the first class, AND any non-conventual sung Mass regardless of the day's own class, admits at most one commemoration, and only if it is privileged. (b)/(c)/(d), the same rubric's remaining clauses, give the wider caps colitur's admit already computes -- which is exactly the LOW MASS answer. Exposed as Precedence.sung_mass_commemorations, a pure derivation over the existing Low-Mass admitted list (filter to Privileged, keep the first), not a new stored field on Liturgical_day.t: the input list is already validated and privilege-tagged, so a second field would only create a second place for the same fact to drift out of sync with the first, with no new information gained. Liturgical_day.t.commemorations is now documented as the Low Mass set explicitly, removing the ambiguity its .mli previously left unstated. Tested against synthetic Low-Mass sets (none/one/two privileged, already-first, empty) and two real calendar days resolved through the normal Cal.day pipeline: 2026-08-14 (Vigil of the Assumption, an ordinary-only commemoration, dropped at Sung Mass) and 2026-04-25 (the Major Litanies, RG 80/109(f), privileged, kept at both Masses). --- test/test_precedence.ml | 55 ++++++++++++++++++++++++++++++++++++++++++++++++- test/test_rubrics_ef.ml | 38 +++++++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/test/test_precedence.ml b/test/test_precedence.ml index acc89fc..54ab3f9 100644 --- a/test/test_precedence.ml +++ b/test/test_precedence.ml @@ -98,6 +98,49 @@ let test_temporal_only_day () = Alcotest.(check int) "nothing deferred" 0 (List.length r.P.deferred); Alcotest.(check int) "nothing omitted" 0 (List.length r.P.omitted) +(* RG 111(a): sung_mass_commemorations -- see precedence.mli's own citation. + Pure function, no {!resolve} needed: exercised directly against + hand-built Low-Mass admitted lists, the same shape {!P.resolve} would + produce, rather than through a full resolution. *) +let sung_test_cand rank slug = cand ~rank slug + +let test_sung_mass_no_privileged () = + let low_mass = [ (sung_test_cand Lo "a", P.Ordinary); (sung_test_cand Lo "b", P.Ordinary) ] in + Alcotest.(check (list string)) "no privileged commemoration -> sung Mass keeps none" [] + (List.map (fun (c, _) -> slug_of c) (P.sung_mass_commemorations low_mass)) + +let test_sung_mass_one_privileged () = + let low_mass = + [ (sung_test_cand Lo "ordinary-one", P.Ordinary); (sung_test_cand Hi "privileged-one", P.Privileged) ] + in + Alcotest.(check (list string)) "the ordinary commemoration is dropped, the privileged one kept" + [ "privileged-one" ] + (List.map (fun (c, _) -> slug_of c) (P.sung_mass_commemorations low_mass)) + +let test_sung_mass_privileged_first_already () = + let low_mass = + [ (sung_test_cand Hi "privileged-one", P.Privileged); (sung_test_cand Lo "ordinary-one", P.Ordinary) ] + in + Alcotest.(check (list string)) "a privileged commemoration already first is kept alone" [ "privileged-one" ] + (List.map (fun (c, _) -> slug_of c) (P.sung_mass_commemorations low_mass)) + +let test_sung_mass_empty_low_mass_set () = + Alcotest.(check (list string)) "an empty Low-Mass set stays empty at Sung Mass" [] + (List.map (fun (c, _) -> slug_of c) (P.sung_mass_commemorations [])) + +(* If two privileged commemorations were ever admitted together (not known + to occur on any shipped rite's data -- precedence.mli's own citation), + [sung_mass_commemorations] keeps only the FIRST -- input order is + already RG 113 precedence order, so this is "the highest-precedence + privileged entry survives", not an arbitrary truncation. *) +let test_sung_mass_two_privileged_keeps_first () = + let low_mass = + [ (sung_test_cand Hi "first-privileged", P.Privileged); (sung_test_cand Hi "second-privileged", P.Privileged) ] + in + Alcotest.(check (list string)) "only the first (higher-precedence) privileged commemoration survives" + [ "first-privileged" ] + (List.map (fun (c, _) -> slug_of c) (P.sung_mass_commemorations low_mass)) + let suite = ( "Precedence", [ Alcotest.test_case "highest band wins" `Quick test_highest_band_wins; @@ -107,4 +150,14 @@ let suite = test_commemoration_only_never_observed; Alcotest.test_case "nothing silently lost" `Quick test_nothing_silently_lost; Alcotest.test_case "order independent" `Quick test_order_independent; - Alcotest.test_case "temporal-only day" `Quick test_temporal_only_day ] ) + Alcotest.test_case "temporal-only day" `Quick test_temporal_only_day; + Alcotest.test_case "RG 111(a): sung Mass, no privileged commemoration" `Quick + test_sung_mass_no_privileged; + Alcotest.test_case "RG 111(a): sung Mass keeps the one privileged commemoration" `Quick + test_sung_mass_one_privileged; + Alcotest.test_case "RG 111(a): sung Mass, privileged already first" `Quick + test_sung_mass_privileged_first_already; + Alcotest.test_case "RG 111(a): sung Mass, empty Low-Mass set" `Quick + test_sung_mass_empty_low_mass_set; + Alcotest.test_case "RG 111(a): sung Mass keeps only the first of two privileged" `Quick + test_sung_mass_two_privileged_keeps_first ] ) diff --git a/test/test_rubrics_ef.ml b/test/test_rubrics_ef.ml index 6679702..d533670 100644 --- a/test/test_rubrics_ef.ml +++ b/test/test_rubrics_ef.ml @@ -746,6 +746,38 @@ let test_exhaustive_gloria_domain_sweep () = Alcotest.(check bool) "the full domain reached a real number of Requiem days" true (black > 1_000) end +(* ---- ITEM 1: RG 111(a), the sung-Mass commemoration cap + ({!Colitur_kernel.Precedence.sung_mass_commemorations}) -- two real + calendar days, one of each shape, resolved through the identical + [Cal.day] pipeline every other test in this file uses. See + precedence.mli's own citation for the rubric and the reasoning for why + this is a derivation over [LD.commemorations] (the Low-Mass admitted + set), not a second stored field. ---- *) + +module Prec = Colitur_kernel.Precedence + +let sung_slugs y m d = + List.map + (fun (c, _) -> Slug.to_string c.Cel.slug) + (Prec.sung_mass_commemorations (day_on y m d).LD.commemorations) + +let test_rg111a_ordinary_only_dropped () = + (* 2026-08-14: Vigil of the Assumption, commemorating [eusebius-confessor] + ORDINARILY (no privileged category applies) -- confirmed via `colitur + day 2026`. At Low Mass this is admitted (RG 111(c)); at a + non-conventual Sung Mass RG 111(a) admits it not at all. *) + Alcotest.(check (list string)) "RG 111(a): an ordinary-only Low-Mass set is empty at Sung Mass" [] + (sung_slugs 2026 8 14) + +let test_rg111a_privileged_kept () = + (* 2026-04-25: St Mark, commemorating the Major Litanies -- RG 80/109(f), + a PRIVILEGED commemoration (data/ef/adjustments.sexp's own [Add + major-litanies]) -- confirmed via `colitur day 2026`. Kept at BOTH Low + Mass (RG 111(c), one privileged) and Sung Mass (RG 111(a), the same + single privileged commemoration). *) + Alcotest.(check (list string)) "RG 111(a): the one privileged commemoration survives at Sung Mass" + [ "major-litanies" ] (sung_slugs 2026 4 25) + let suite = ( "Rubrics_ef", [ Alcotest.test_case "475(a): ordinary Sunday" `Quick test_475a_ordinary_sunday; @@ -819,4 +851,8 @@ let suite = Alcotest.test_case "domain sanity: every Requiem day has [gloria]=false (sample)" `Quick test_domain_requiem_implies_no_gloria_sample; Alcotest.test_case "domain sweep 1583..9999: violet/Requiem invariants, committed not sampled" - `Slow test_exhaustive_gloria_domain_sweep ] ) + `Slow test_exhaustive_gloria_domain_sweep; + Alcotest.test_case "RG 111(a): an ordinary-only Low-Mass set is empty at Sung Mass" `Quick + test_rg111a_ordinary_only_dropped; + Alcotest.test_case "RG 111(a): a privileged commemoration survives at Sung Mass" `Quick + test_rg111a_privileged_kept ] ) -- cgit v1.3