From 41263d60432fadd2aef3f04de66b60561f89cbec Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 22 Aug 2026 13:38:27 +0200 Subject: fix(ef): RG 476(f) -- the Creed is not said at a Requiem Mass colitur said the Creed IS said on All Souls' Day and every other Requiem-shaped day: RG 475(b)'s "in festis I classis" branch fired on rank alone, with nothing checking RG 476(f), "Non dicitur symbolum ... f) in Missis defunctorum". Allow-listed as L1 in expected-divergences-lms.sexp on the argument that [creed] has no notion of a Requiem Mass in its inputs and could not express the rule without a signature change. That argument does not hold. Colour.Black is used by exactly two celebrations in the whole engine, verified by grepping every occurrence in lib/ and data/: commemoration-of-all-souls (data/ef/sanctoral.sexp) and Good Friday (temporal_ef.ml, already false via the earlier RG 23(b) feria exclusion and has no Mass at all in the 1955-restored Holy Week). So on every day this engine can construct, colour = Black if and only if the Mass is a Requiem -- a citable implication (RG 117 assigns black to Masses of the dead) on this data, not a heuristic. Rite_ef.Rubrics_ef.creed now reads observed.colour as RG 476(f)'s own guard, positioned ahead of 475(b)'s rank branch. A new test, test_colour_black_population_is_exactly_two, sweeps both the shipped data and Temporal_ef's own code across a century and fails loudly the day a third Colour.Black celebration is ever introduced, so the proxy cannot silently rot. L1 is closed in expected-divergences-lms.sexp, citation preserved. Blast radius, measured (full 1583-9999 sweep, grouped by observed slug): 8 416 days change from true to false, every one commemoration-of-all-souls (missing from exactly 1 of the 8 417 domain years, a boundary-year transfer effect at 1583/9999). A further 8 416 Colour.Black days are Good Friday, unaffected (already false before this fix). colitur day/readings stay byte-identical to v0.10.1 (checked directly against a v0.10.1 worktree build, 9 domain-spanning years); colitur rubrics changes on exactly the 8 416 days above. colitur emit --format sexp's pretty-printed line count for 2027 moved 9010 -> 9011 as a side effect: Sexplib.Sexp.to_string_hum wraps by column width, and All Souls' Day 2027's record grew one wrapped line because "false" is one character longer than "true". Cosmetic only -- no field's data changed -- and pinned in test/cli.t with the measurement recorded. --- test/cli.t | 14 ++++++-- test/test_rubrics_ef.ml | 88 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/test/cli.t b/test/cli.t index ac1351b..e2d2eb5 100644 --- a/test/cli.t +++ b/test/cli.t @@ -534,10 +534,20 @@ CSV run rather than one per year: $ colitur emit --format csv --from 2027 --to 2028 | wc -l 732 -sexp and xml are also available: +sexp and xml are also available. This line count moved 9010 -> 9011 +(whole-branch review fix round, RG 476(f)): [emit --format sexp] pretty- +prints with [Sexplib.Sexp.to_string_hum], a column-width wrapping printer, +not a fixed-shape one -- 2027's All Souls' Day (2 November) record grew a +single wrapped line when its own [creed] field's value changed from +[true] to [false] (RG 476(f), the Creed is never said at a Requiem Mass; +see rubrics_ef.ml), because "false" is one character longer than "true" +and pushed that one line's rendered width over to_string_hum's own wrap +threshold. Purely cosmetic -- the record's DATA is unchanged in every +other field, and this is not a claim that [emit]'s FORMAT changed, only +that one record's pretty-printed SHAPE did: $ colitur emit --format sexp --from 2027 --to 2027 | wc -l - 9010 + 9011 $ colitur emit --format xml --from 2027 --to 2027 | head -2 diff --git a/test/test_rubrics_ef.ml b/test/test_rubrics_ef.ml index 5109c69..a778a5a 100644 --- a/test/test_rubrics_ef.ml +++ b/test/test_rubrics_ef.ml @@ -26,6 +26,7 @@ module Colour = Colitur_kernel.Colour module Subject = Colitur_kernel.Subject module Slug = Colitur_kernel.Slug module Temporal = Colitur_kernel.Temporal +module Computus = Colitur_kernel.Computus module V = Rite_ef.Vocab_ef module RE = Rite_ef.Rubrics_ef module TE = Rite_ef.Temporal_ef @@ -175,6 +176,87 @@ let test_holy_saturday_easter_vigil_no_creed () = by 476(a) ("in Missa Vigiliae paschalis"). *) check "RG 23(b)/476(a): Holy Saturday (the Easter Vigil Mass)" 2026 4 4 false +(* ---- RG 476(f): "in Missis defunctorum" -- the Creed is never said at a + Requiem Mass. Whole-branch review finding 1: colitur used to say [true] + here on rank alone (475(b), Class1) -- the LMS Ordo layer's own former + allow-list entry L1, now closed, was this exact day. See rubrics_ef.ml's + own header for why {!Colour.Black} is a sound RG 117 proxy for "this Mass + is a Requiem" on the shipped data. ---- *) + +let test_476f_all_souls_no_creed () = + (* 2025-11-03: All Souls' Day (2 November, a Sunday that year, transferred + to the next open day, RG's own Class1/black office unchanged by the + transfer) -- `colitur day 2025` confirms class-1/black/ + commemoration-of-all-souls on this date. Before this fix [creed] here + was [true] via 475(b); the Ordo's own text ("No Cr") says otherwise. *) + check "RG 476(f): All Souls' Day (transferred), a Requiem, says no Creed" 2025 11 3 false + +let test_476f_good_friday_no_creed () = + (* 2026-04-03: Good Friday, [colour = Black] (temporal_ef.ml's own + [days_between easter d = -2] branch) -- already [false] via the RG + 23(b) feria-I-classis exclusion checked earlier in [creed], so this is + a belt-and-braces witness that the 476(f) guard, were it ever reached + first, would agree rather than a case that depends on it: Good Friday + has no Mass in the 1955-restored Holy Week to begin with. *) + check "RG 476(f)/RG 23(b): Good Friday, black, says no Creed" 2026 4 3 false + +(* Whole-branch review finding 1's own instruction: a test that FAILS if a + third {!Colour.Black} celebration is ever introduced, so the RG 476(f) + guard's colour proxy cannot silently rot into covering the wrong + population. Two independent checks, matching the two places a black + celebration could be added: + + DATA: every entry the shipped sanctoral layer (sanctoral.sexp, with + adjustments.sexp merged on top -- the same [layer] every other test in + this file resolves against) carries directly, fixed AND movable alike + ({!Colitur_kernel.Layer.entries} holds the raw [Date_spec.t], not a + resolved index, so this needs no date walk at all to cover every civil + year). + + CODE: {!Rite_ef.Temporal_ef.temporal}'s own colour logic, which + sanctoral.sexp cannot see (Good Friday's black is computed, not data) -- + swept across a century the same span and for the same coverage reason + {!test_ferial_origin_never_carries_lord_bvm_or_apostle_slug} above + already uses, since a Colour.Black bug introduced into a rare branch + (an Ember day, Rogation Wednesday...) would not show up in one year. *) +let test_colour_black_population_is_exactly_two () = + let black_data_entries = + List.filter + (fun (e : V.rank Colitur_kernel.Layer.entry) -> e.Colitur_kernel.Layer.cel.Cel.colour = Colour.Black) + layer.Colitur_kernel.Layer.entries + in + Alcotest.(check int) "exactly one Colour.Black entry in the shipped data" 1 + (List.length black_data_entries); + (match black_data_entries with + | [ e ] -> + Alcotest.(check string) "the one Colour.Black data entry is All Souls" "commemoration-of-all-souls" + (Slug.to_string e.Colitur_kernel.Layer.cel.Cel.slug) + | _ -> ()); + let code_black = ref [] in + for y = 1583 to 1682 do + let dec31 = mk y 12 31 in + let d = ref (mk y 1 1) in + while Date.compare !d dec31 <= 0 do + let t = TE.temporal !d in + if t.Temporal.office.Cel.colour = Colour.Black then code_black := (y, !d, t) :: !code_black; + d := Date.add_days !d 1 + done + done; + Alcotest.(check bool) "the century sweep found at least 100 Colour.Black temporal days" true + (List.length !code_black >= 100); + List.iter + (fun (_, d, (t : (V.season, V.rank) Temporal.t)) -> + let easter = Computus.gregorian_easter (Date.year d) in + Alcotest.(check int) + (Printf.sprintf "%s: the only code-side Colour.Black day is Good Friday (Easter-2)" + (Date.to_iso8601 d)) + (-2) (Date.to_rata d - Date.to_rata easter); + Alcotest.(check string) + (Printf.sprintf "%s: Good Friday's own slug" (Date.to_iso8601 d)) + "ef-passiontide-2-friday" + (Slug.to_string t.Temporal.office.Cel.slug)) + !code_black + (* ---- RG 476(b) negative: a plain II-class saint, not Domini/BVM, not an Apostle/Evangelist ---- *) @@ -381,6 +463,12 @@ let suite = Alcotest.test_case "RG 23/476(a): Holy Thursday" `Quick test_holy_thursday_no_creed; Alcotest.test_case "RG 23/476(a): Holy Saturday / Easter Vigil" `Quick test_holy_saturday_easter_vigil_no_creed; + Alcotest.test_case "476(f): All Souls' Day (transferred), a Requiem, no Creed" `Quick + test_476f_all_souls_no_creed; + Alcotest.test_case "476(f)/RG 23(b): Good Friday, black, no Creed" `Quick + test_476f_good_friday_no_creed; + Alcotest.test_case "Colour.Black population is exactly two (data + code)" `Quick + test_colour_black_population_is_exactly_two; Alcotest.test_case "476(b): plain II-class saint (negative)" `Quick test_476b_plain_class2_saint; Alcotest.test_case "vigils excluded even when Class2 and Marian" `Quick -- cgit v1.3