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. --- data/ef/expected-divergences-lms.sexp | 131 ++++++++++++++++++++++++---------- lib/rites/rite_ef/rubrics_ef.ml | 58 +++++++++++++-- lib/rites/rite_ef/rubrics_ef.mli | 5 +- test/cli.t | 14 +++- test/test_rubrics_ef.ml | 88 +++++++++++++++++++++++ 5 files changed, 249 insertions(+), 47 deletions(-) diff --git a/data/ef/expected-divergences-lms.sexp b/data/ef/expected-divergences-lms.sexp index 96c9e50..e327419 100644 --- a/data/ef/expected-divergences-lms.sexp +++ b/data/ef/expected-divergences-lms.sexp @@ -15,41 +15,96 @@ ; backs, the SAME convention the missalemeum file's own header states: ; not always "colitur". ; -; L1 -- 2025-11-03 (All Souls' Day, transferred from 2 November, a Sunday -; that year, to the next available day -- RG's own transfer of a -; Commemoration when it falls on a Sunday). colitur (Rite_ef.Rubrics_ef. -; creed) says the Creed IS said (true): observed.rank = Class1, so RG -; 475(b)'s "in festis I classis" branch fires with nothing to stop it. -; The Ordo says it is NOT said (false): "Mon 3 ALL SOULS' DAY I Cl B / No -; Gl Sequence No Cr Pr of the Dead No Blessing" -- confirmed directly -; against the extracted pdftotext text, not merely the fixture row. -; -; The Ordo is right and colitur is not, on the rubric itself: RG 476(f), -; "Non dicitur symbolum ... f) in Missis defunctorum" -- the Creed is -; never said at a Requiem Mass, and All Souls' Day is definitionally one -; (colour black in colitur's own output on this very date). This is NOT a -; new defect this task found and is choosing to leave open -- it is -; rubrics_ef.ml's OWN documented scope limit, stated in that file's header -; comment before this task existed: "476(f) (Requiem Masses, also not -; modelled) ... genuinely inapplicable to this implementation -- a -; documented scope limit, not a defect." [creed] has no notion of "this -; Mass is a Requiem" anywhere in its own inputs (temporal/observed/date), -; so it cannot express 476(f) without a signature change this task's own -; constraints do not authorise (lib/ is read-only here except for this -; allow-list). Recorded here as the SPECIFIC real-world day the scope -; limit is finally witnessed on, rather than left as an abstract note in -; a doc comment nobody's data ever exercised. -; -; Domain-wide count: unmeasured (out of this task's scope -- would need a -; sweep of every I-class Requiem-shaped day 1583-9999, which does not -; exist as a distinct predicate in this codebase to sweep on). The two -; days this WOULD also apply to in principle -- 2 November itself in any -; year it does not fall on a Sunday, and the plain All Souls' Day office -; whenever it is not transferred -- are outside this fixture's own -; window's reach for a second occurrence, so only the one real day above -; is witnessed and adjudicated here. -((id L1) - (citation "RG 476(f): \"Non dicitur symbolum ... f) in Missis defunctorum\" -- the Creed is never said at a Mass of the dead; rubrics_ef.ml's own header already documents this Mass-type as unmodelled") - (verdict lms_ordo) - (note "colitur's [creed] has no notion of a Requiem Mass in its inputs (temporal/observed/date), so RG 475(b)'s I-class branch fires unopposed on All Souls' Day (Class1, black). A pre-existing, self-documented scope limit (rubrics_ef.ml's own header), witnessed here for the first time on a real date rather than left as an abstract note: 2025-11-03, All Souls' Day transferred from Sunday 2 November. The Ordo (\"No Gl Sequence No Cr Pr of the Dead\") is correct on the rubric.") - (expected_rows 1)) +; L1 -- CLOSED, FIXED (whole-branch review finding 1, 2026-08-22): was +; 2025-11-03 (All Souls' Day, transferred from 2 November, a Sunday that +; year). colitur used to say the Creed IS said (true) on rank alone +; (475(b), Class1) where the Ordo, correctly, says it is not: "Mon 3 ALL +; SOULS' DAY I Cl B / No Gl Sequence No Cr Pr of the Dead No Blessing". +; +; This entry's own PREVIOUS text argued the divergence could not be fixed +; without a signature change, because [creed]'s inputs (temporal/observed/ +; date) have no notion of "this Mass is a Requiem". That argument was +; wrong: {!Colour.Black} is used by exactly TWO celebrations in the whole +; engine (verified by grepping every [colour Black]/[Colour.Black] site in +; lib/ and data/) -- [commemoration-of-all-souls] (data/ef/sanctoral.sexp) +; and Good Friday (temporal_ef.ml, already [false] via the RG 23(b) feria +; exclusion regardless of colour, 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 guess. [Rite_ef.Rubrics_ef.creed] now reads +; [observed.colour] as RG 476(f)'s own guard, positioned ahead of 475(b)'s +; rank branch; see rubrics_ef.ml's own header and the guard's own comment +; for the full account, and test_rubrics_ef.ml's +; [test_colour_black_population_is_exactly_two] for the test that fails +; loudly the day a third [Colour.Black] celebration is ever added, so this +; proxy cannot silently rot. +; +; BLAST RADIUS, measured (full 1583-9999 sweep of {!Colitur_kernel. +; Calendar.year}, grouped by [observed]'s own slug): 8 416 days changed +; 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, not a third population member: see the "know what +; each layer cannot see"-style caveats elsewhere in this project for other +; instances of the same edge). A further 8 416 [Colour.Black] days are +; Good Friday ([ef-passiontide-2-friday]), unchanged by this fix (already +; [false] before it, via the earlier feria exclusion) -- confirmed +; [creed_true_among_black = 0] after the fix, over the WHOLE domain, not +; merely the fixture's own 2024-2025 window. +; +; `colitur day`/`colitur readings` are unaffected (neither prints [creed]); +; `colitur rubrics` output changes on exactly the 8 416 days above. + +; L2 -- OPEN, colitur's own gap, found while building L1's fix (whole- +; branch review finding 2, 2026-08-22). NOT an active entry in this +; file's own machine-checked sense (no code loads or cross-checks an +; "L2" id anywhere -- this file's own [allow_entry] machinery is scoped +; to the CREED axis, {!test_lms_ordo.test_creed_matches_or_is_explained} +; alone; a second, unrelated axis reusing the same id-lookup mechanism +; would let one test's "declared but never fired" check spuriously trip +; on the other axis's own entry). Recorded here as prose only, the same +; discipline this file's own closed L1 note above and +; expected-divergences.sexp's own closed C-entries already use for a +; citation worth preserving without a live parser hook -- the REAL +; enforcement is test_lms_ordo.ml's own [test_formulary_override_matches], +; which asserts this population is exactly 3 days directly. +; +; Strengthening test_lms_ordo.ml's formulary-override check from PRESENCE +; to the actual STRING (this same finding 2) surfaced a real, separate +; defect in step 3 of the reading chain ({!Rite_ef.Lectionary_ef. +; readings}), independent of the Creed axis above. Three real days in the +; fixture window -- 2025-05-30 (Friday), 2025-06-02 (Monday), 2025-06-03 +; (Tuesday), all between Ascension Thursday and the following Sunday -- +; have no proper of their own and fall to {!Colitur_kernel. +; Mass_formulary.Preceding_sunday}. colitur resumes the nearest CALENDAR +; Sunday (the "5th"/"6th Sunday after Easter", Paschaltide's own week +; 6/7), but the Missal, and the Ordo, keep saying ASCENSION'S OWN Mass +; through this stretch instead: "Mass of the Ascension" on all three +; days, never a numbered-Sunday title. Step 3 has no notion of "inside +; Ascension's own after-feast period" -- it always resumes the nearest +; preceding Sunday, numbered or not. +; +; STRUCTURAL, not a fixture artefact: the Ascension is a fixed Easter+39 +; (a Thursday inside Paschaltide week 6), so every feria whose own +; Preceding_sunday fallback would otherwise resolve to week 6 or 7 falls +; in this same stretch, every year -- this is not a coincidence of +; 2024-2025 alone, though only this fixture's own window has been checked +; against a real Ordo. +; +; NOT FIXED in this task: fixing it means teaching step 3, or the +; lectionary data, about Ascension's own after-feast period -- the same +; shape as the Christmastide "infra octavam Nativitatis" entries +; data/ef/lectionary.sexp already carries directly rather than resolving +; through this fallback -- a lectionary-data change out of a fix round's +; own scope. test/test_lms_ordo.ml's own +; [expected_preceding_sunday_override] excludes exactly these two +; (season, week) pairs from its general mapping, and +; [test_formulary_override_matches] asserts the excluded population is +; exactly 3 real days, all showing "Mass of the Ascension", so a change +; to this population (a fourth day, a different Ordo string) would be +; caught rather than silently absorbed. +; +; Domain-wide count: unmeasured -- "which feria is inside Ascension's own +; after-feast period" is not a distinct predicate anywhere in this +; codebase to sweep on. Structurally recurs every year, so on the order +; of 3 days/year x domain years, not merely 3 total. diff --git a/lib/rites/rite_ef/rubrics_ef.ml b/lib/rites/rite_ef/rubrics_ef.ml index 6345399..ab9da32 100644 --- a/lib/rites/rite_ef/rubrics_ef.ml +++ b/lib/rites/rite_ef/rubrics_ef.ml @@ -26,12 +26,37 @@ engine resolves ONE observed office and ONE Mass per civil day (see Rite.t.readings' own doc comment) -- it has no separate "which votive Mass is said" dimension. So the "vel/et...votivis" halves of 475(a)/(b), - 476(c) entirely, 476(d)'s "et votivis" half, and 476(f) (Requiem Masses, - also not modelled) are genuinely inapplicable to this implementation -- - a documented scope limit, not a defect. 476(e) needs no branch at all: - [creed] below reads only [observed], never a day's admitted - commemorations, so a commemoration can never change its answer by - construction. *) + 476(c) entirely, and 476(d)'s "et votivis" half are genuinely + inapplicable to this implementation -- a documented scope limit, not a + defect. 476(e) needs no branch at all: [creed] below reads only + [observed], never a day's admitted commemorations, so a commemoration + can never change its answer by construction. + + 476(f) ("in Missis defunctorum") is DIFFERENT: this file used to carry + it in the same "not modelled" list above, on the reasoning that + [creed]'s inputs (temporal/observed/date) have no notion of "this Mass + is a Requiem". That reasoning was wrong, found by the LMS Ordo layer + (test_lms_ordo.ml, allow-list entry L1, now closed -- see + expected-divergences-lms.sexp) on 2025-11-03, All Souls' Day: colitur + said the Creed where the Ordo, correctly, does not. [observed] DOES + carry a usable signal -- RG 117 assigns black to Masses of the dead, + and {!Colour.Black} is used by exactly TWO celebrations in this whole + engine, verified by grepping every [colour Black]/[Colour.Black] site + in lib/ and data/: [commemoration-of-all-souls] in + data/ef/sanctoral.sexp, and Good Friday in temporal_ef.ml (which is a + Holy Week FERIA, RG 23(b), already excluded by the [n >= -6 && n <= + -1] branch below regardless of colour, and has no Mass at all in the + 1955-restored Holy Week). So on every day this engine can actually + construct, [colour = Black] if and only if the Mass is a Requiem -- + a citable implication ON THIS DATA, not a heuristic guess. [creed] + below uses it as 476(f)'s own guard. This is a PROXY, not a general + "is this a Requiem Mass" field, and it is only as good as that + two-member population: {!test_rubrics_ef}'s own + [test_colour_black_population_is_exactly_two] fails loudly the day a + third [Colour.Black] celebration is added anywhere, so the proxy + cannot silently rot into covering (or missing) a non-Requiem black + Mass. If that ever happens, this guard needs re-deriving, not merely + re-approving. *) open Colitur_kernel @@ -222,6 +247,27 @@ let creed ~(temporal : (Vocab_ef.season, Vocab_ef.rank) Temporal.t) {!Precedence_ef.marian_slugs} just below. *) Precedence_ef.is_vigil slug then false + else if + (* RG 476(f): "in Missis defunctorum" -- the Creed is never said at a + Requiem Mass, checked here, ahead of 475(b)'s rank branch, the same + "category excludes regardless of rank" position as the feria and + vigil checks immediately above (All Souls' Day is [Class1], so + without this guard 475(b) would grant it [true] unopposed, which is + exactly the defect this guard fixes -- see this file's own header + for the two-member [Colour.Black] population this proxy rests on, + and {!Colour.Black}'s own citation, RG 117, for why colour is the + Requiem signal. Placed before, not after, 475(b)/(c)/(e) so a black + Class1 or Class2 day can never reach them; nothing above this point + (the Nativity/Easter/Pentecost octave override, the Sunday rule, the + feria and vigil exclusions) is ever reachable by a Requiem Mass on + the shipped data either -- a Requiem is never kept on a Sunday or + inside a privileged octave -- so this guard's own position relative + to THOSE branches is moot on real data, checked, not merely assumed: + {!creed_apostle_slugs} and [marian_slugs] contain no Black-coloured + entry, and 2 November can never fall inside any of the three + octaves 475(d) names. *) + observed.Celebration.colour = Colour.Black + then false else if (* RG 475(b): "in festis I classis". Genuine feasts only, by construction of the two exclusions immediately above (feria I diff --git a/lib/rites/rite_ef/rubrics_ef.mli b/lib/rites/rite_ef/rubrics_ef.mli index 16e7491..7a1284b 100644 --- a/lib/rites/rite_ef/rubrics_ef.mli +++ b/lib/rites/rite_ef/rubrics_ef.mli @@ -29,7 +29,10 @@ val creed_apostle_slugs : string list {!Liturgical_day.t.commemorations}: RG 476(e), "ratione alicuius commemorationis in Missa occurrentis" [never say the Creed merely because of a commemoration], holds by construction rather than by a - checked branch. *) + checked branch. RG 476(f) ("in Missis defunctorum") reads + [observed]'s [colour]: see the .ml's own header for why [Colour.Black] + is a sound proxy for "this Mass is a Requiem" on the shipped data, and + what would have to be re-checked if that ever stops being true. *) val creed : temporal:(Vocab_ef.season, Vocab_ef.rank) Temporal.t -> observed:Vocab_ef.rank Celebration.t -> 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