From a6daed7820a97f4032f21964b5b1bb30fe00b36b Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 18 Aug 2026 12:11:05 +0200 Subject: fix(ef): ubaldus and didacus are III-class feasts, not commemorations Both carried rank Class3 with status Commemoration_only, bootstrapped from lectio, which inherits it from missalemeum, which is generated from Divinum Officium. The Missal's own universal calendarium ranks both "III classis" outright, with no commemoration rubric: "S. Ubaldi Ep. et Conf., III classis" (16 May) and "S. Didaci Conf., III classis" (13 November), corroborated in the second scan and in a published 1962 Ordo. Universal by construction, not by assumption: the calendarium carries no "pro aliquibus locis" marker in its 573 lines while the wider Missal carries 79 of them elsewhere. Each saint also has his own Mass entry in the Proprium, which a bare commemoration never has -- a commemoration carries an oration only. Found by auditing ALL 290 fixed-date entries against that calendarium, day by day, anchored on the Roman calends column because the arabic day column is OCR-wrecked and the dominical-letter column vanishes in some months. These two were the only status defects in the whole file. The 15 days the automated pass could not cover were read by hand and all agree. Neither needed a proper authored: each takes its Mass from a Common with only its own Oratio proper, per its own rubric at its own date. ubaldus shares patrick's Common exactly. didacus needed one newly transcribed -- common-of-a-confessor-not-a-bishop-2, the "Altera Missa" Iustus, 1 Cor 4:9-14 and Luke 12:32-34, read independently in both scans, which agree, so the page-image third reading this file's method requires on disagreement was not triggered. Blast radius, full 1583-9999 sweep, every day classified: 13 036. That is 5 564 ubaldus observed, 7 196 didacus observed, and 276 where the Ascension Vigil still wins on 16 May but its single RG 111(c) slot passes from rogation-wednesday to ubaldus -- correct and already pinned, since the Rogation days are the MINOR Litanies and so an ordinary commemoration, which RG 113's table order ranks below a III-class feast. Cited as C38 (70 rows) and M30 (3); C8, C17 and M18 shrink accordingly, each with the reason recorded rather than the count silently adjusted. --- test/test_differential.ml | 18 ++++++++++++++++++ test/test_lectionary_ef.ml | 10 ++++++++++ test/test_oracle.ml | 37 ++++++++++++++++++++++++++++++++++++- test/test_sanctoral_ef.ml | 8 +++++++- 4 files changed, 71 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/test/test_differential.ml b/test/test_differential.ml index 849bde4..25a690d 100644 --- a/test/test_differential.ml +++ b/test/test_differential.ml @@ -912,6 +912,19 @@ let good_friday_slug = "ef-passiontide-2-friday" Paschaltide and is correctly white already. *) let violet_vigil_slugs = [ "vigil-of-st-lawrence"; "vigil-of-the-assumption" ] +(* C38 (ef-sanctoral-status): `ubaldus` (16 May) and `didacus` (13 November) + were promoted from Commemoration_only to Feast on the Missal's own + universal calendarium, which ranks both "III classis" outright. lectio + inherits the Commemoration_only status from missalemeum, so where colitur + now observes the saint, lectio still observes the feria -- hence the slug, + rank and both citations differ together. Two exact shapes, not one: + 16 May falls in Paschaltide, whose ferial colour is ALREADY white, so no + colour difference arises there; 13 November is Time after Pentecost, green, + so the white confessor changes it. Both shapes are spelled out rather than + collapsed into a subset check, so a colour difference appearing on 16 May + (or vanishing on 13 November) would surface as unexplained. *) +let promoted_feast_slugs = [ "ubaldus"; "didacus" ] + (* C26 (task 8, fix round 2, coordinator review, Important): Passion week's own real Tuesday Mass (tools/bootstrap_lectionary.ml's own [passion_tuesday_entry] comment has the full Missal citation and both @@ -1294,6 +1307,11 @@ let layer_c_reason (l : row) (c : row) diffs = else if String.equal c.slug good_friday_slug && diffs = [ First_f; Gospel_f; Colour_f ] then Some "C36" else if List.mem c.slug violet_vigil_slugs && diffs = [ Colour_f ] then Some "C37" + else if + List.mem c.slug promoted_feast_slugs + && (diffs = [ Slug_f; Rank; First_f; Gospel_f ] + || diffs = [ Slug_f; Rank; First_f; Gospel_f; Colour_f ]) + then Some "C38" else if List.mem c.slug holy_week_slugs && diffs = [ First_f; Gospel_f ] then Some "C23" else if String.equal c.slug passion_tuesday_slug && diffs = [ First_f; Gospel_f ] then Some "C26" else if String.equal c.slug advent_ember_sat_slug && diffs = [ First_f; Gospel_f ] then Some "C27" diff --git a/test/test_lectionary_ef.ml b/test/test_lectionary_ef.ml index a5a0f6e..d56d187 100644 --- a/test/test_lectionary_ef.ml +++ b/test/test_lectionary_ef.ml @@ -442,11 +442,21 @@ let test_step4_assignment_table () = Alcotest.(check (list (pair string string))) "the Commons assignment table, exactly" [ ("benedict", "common-of-abbots"); + ("didacus", "common-of-a-confessor-not-a-bishop-2"); ("frances-rome", "common-of-non-virgins-2"); ("gregory-the-great", "common-of-supreme-pontiffs"); ("isidore-of-seville", "common-of-doctors"); ("patrick", "common-of-a-confessor-bishop-1"); + (* ef-sanctoral-status: ubaldus and didacus were promoted from + Commemoration_only to Feast on the Missal's own universal + calendarium, which ranks both "III classis" outright with no + commemoration rubric. Each takes its Mass from a Common with only + its own Oratio proper, per its own rubric at its own date, so + neither needed a proper authored -- only an assignment. ubaldus + shares patrick's Common exactly ("Missa Statuit, de Communi + Confessoris Pontificis I loco"). *) ("sts-felicitas-perpetua", "common-of-non-virgins-1"); + ("ubaldus", "common-of-a-confessor-bishop-1"); ("vincent-ferrer", "common-of-a-confessor-not-a-bishop-1") ] assigned diff --git a/test/test_oracle.ml b/test/test_oracle.ml index 129c43b..7ee3848 100644 --- a/test/test_oracle.ml +++ b/test/test_oracle.ml @@ -1334,6 +1334,20 @@ let m25_dates = [ "2026-03-09"; "2026-03-24"; "2027-03-01"; "2027-03-16" ] fix seen from the lectio side, and register 6.20 on the shared lineage. Spans all three oracle windows. 2026-08-09 is absent because it is a Sunday that year and the vigil never occurs; checked with `date -d`. *) +(* M30 (ef-sanctoral-status) -- `ubaldus` (16 May) and `didacus` (13 Nov), + promoted from Commemoration_only to Feast on the Missal's own universal + calendarium ("III classis" outright, no commemoration rubric, and a Mass + entry of their own in the Proprium). missalemeum keeps them as + commemorations, so on these days colitur observes the SAINT where the + oracle observes the feria: rank, observed identity and both citations + differ together, and the commemoration colitur used to emit is gone. + Colour differs only on 13 November, where the ferial green gives way to + the confessor's white; 16 May is in Paschaltide, already white. The same + population as C38 in the lectio differential -- and, as there, the oracle + here is colitur's own discarded ancestor rather than an independent + witness (register 6.20). *) +let m30_dates = [ "2026-05-16"; "2026-11-13"; "2027-11-13" ] + let m29_dates = [ "2026-08-14"; "2027-08-09"; "2027-08-14"; "2035-08-09"; "2035-08-14"; "2038-08-09"; "2038-08-14" ] @@ -1493,6 +1507,11 @@ let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = && subset diffs [ Observed_identity_unresolved; First_mismatch; Gospel_mismatch ] then Some "M27" else if List.mem c.c_date m29_dates && subset diffs [ Colour_f ] then Some "M29" + else if List.mem c.c_date m30_dates + && subset diffs + [ Rank; Colour_f; Comm_presence; Comm_count; Observed_identity_mismatch; + Comm_identity_unresolved; First_mismatch; Gospel_mismatch ] + then Some "M30" else None (* ---------------------------------------------------------------------- *) @@ -1844,7 +1863,19 @@ let classify_2038 (c : colitur_row) (o : oracle_row) diffs = nh = 0 || at 0 in let first_is s = match c.c_first with Some f -> contains f s | None -> false in - if contains title "Mass of the B. V. M." then Some "BVM-SATURDAY-MASS (M26 shape 1)" + (* ef-sanctoral-status: `didacus`, promoted to Feast on the Missal's own + calendarium. 13 November 2038 is a SATURDAY, so before the promotion the + day kept RG 78's votive Office of the BVM (nothing else occupied it) and + after it the III-class feast does. Classified here explicitly, ahead of + every other branch, because without it the day falls through to the + BVM-Saturday branch and is labelled as the very office the promotion + REMOVED -- a label that would read as "still a BVM Saturday" and hide the + real cause. Same population as M30 in the 2026-2027 window and C38 in the + lectio differential. 16 May 2038 is a Sunday, so `ubaldus` is impeded and + never reaches this window at all. *) + if c.c_date = "2038-11-13" then + Some "PROMOTED-FEAST-RG-CALENDARIUM (M30, colitur; see C38)" + else if contains title "Mass of the B. V. M." then Some "BVM-SATURDAY-MASS (M26 shape 1)" else if first_is "Col 1:12-20" then Some "CHRIST-THE-KING-WEEK (M26 shape 2b)" else if diffs = [ Colour_f ] && List.mem c.c_observed_slug m21_2038_slugs then Some "COLOUR (M21)" else if first_is "Judith" then Some "JUDITH (M28, register 6.6)" @@ -2069,6 +2100,10 @@ let test_2038_class_counts () = Paschaltide. 2 of 2 possible -- neither 9 nor 14 August 2038 is a Sunday, so both vigils are actually observed that year. *) ("VIGIL-COLOUR-RG128 (M29, colitur; see C37)", 2); + (* ef-sanctoral-status: 13 November 2038 is a Saturday, so the + promoted `didacus` displaces RG 78's BVM Office there. 1 of 2 + possible: 16 May 2038 is a Sunday and `ubaldus` is impeded. *) + ("PROMOTED-FEAST-RG-CALENDARIUM (M30, colitur; see C38)", 1); ("COMM-NAME-FORM (same entity, different English name)", 2); ("COMM-ONLY-TIE-BREAK (RG-under-determined convention -- register 6.16)", 1); ("JOSEPH-ANNUNCIATION-TRANSFER-ORDER (C14's own collision)", 2); diff --git a/test/test_sanctoral_ef.ml b/test/test_sanctoral_ef.ml index ebf96dc..5c46edf 100644 --- a/test/test_sanctoral_ef.ml +++ b/test/test_sanctoral_ef.ml @@ -47,7 +47,13 @@ let test_load_and_counts () = let commemoration_only = List.filter (fun e -> e.L.cel.Cel.status = Cel.Commemoration_only) l.L.entries in - Alcotest.(check int) "104 Commemoration_only" 104 (List.length commemoration_only); + (* 102, not 104: ef-sanctoral-status promoted `ubaldus` (16 May) and + `didacus` (13 November) to Feast. Both are ranked "III classis" outright + in the Missal's own universal calendarium, with no commemoration rubric, + and both have their own Mass entry in the Proprium taking its readings + from a Common. They were the ONLY two defects found by auditing all 290 + fixed-date entries against that calendarium -- see the register. *) + Alcotest.(check int) "102 Commemoration_only" 102 (List.length commemoration_only); let class1 = List.filter (fun e -> e.L.cel.Cel.rank = V.Class1) l.L.entries in Alcotest.(check int) "12 Class1" 12 (List.length class1); let temporal_subjects = List.filter (fun e -> e.L.cel.Cel.subject = Sub.Temporal) l.L.entries in -- cgit v1.3 From e07d0b1f49e1bb006b12a5e612453101d9e641a7 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 18 Aug 2026 12:30:08 +0200 Subject: fix(ef): the seasonal commemoration comes first (RG 113 sentence one) "113. Commemoratio de Tempore fit primo loco. In admittendis et ordinandis aliis commemorationibus, servetur ordo tabellae praecedentiae." Only the second sentence was implemented, adopted on ef-rg16a as the admission and ordering criterion. The first -- the commemoration OF THE SEASON is made in the first place -- was not, so ordering ran through band alone for every commemoration including the seasonal one. Reconciled with RG 110(c), which adds the inseparable Peter/Paul pair "ante omnes alias commemorationes". Both rubrics are primary-source certain: the photographic scans' OCR is illegible at RG 110(c), but docs/research/LT.txt, the electronic transcription, carries the Rubricae Generales complete, and O'Connell's footnote 39 turns out to render that clause exactly rather than gloss it. They share a technical term. RG 113's own sentence fixes the sense of "aliae commemorationes" as the ones other than the de Tempore one it has just placed primo loco, and RG 110(c) sits three paragraphs earlier in the same code. Read consistently, the pair heads the OTHERS, not the whole list: season, then the pair, then the table order. So this runs after rg110_additions, and List.partition's stability keeps the pair adjacent while the seasonal commemoration steps in front of it. Blast radius, two full 1583-9999 sweeps diffed: 3 533 days, every one ORDER-ONLY -- the commemoration set is identical on both sides of every changed day, checked as sorted multisets, zero days where anything else moved. All are 22 February with chair-of-st-peter observed. Exactly the population measured before the rule was written. The order-sensitive test row is in test_precedence_ef.ml's admit_cases, which is still the only place in the suite that asserts commemoration order at all. It needed a mixed-origin candidate: every other candidate in that table is origin = Temporal by default, so the rule is an identity on them. --- lib/rites/rite_ef/precedence_ef.ml | 41 +++++++++++++++++++++++++++++++++++++- test/test_precedence_ef.ml | 28 ++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index dd75765..b69ee52 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -1532,6 +1532,45 @@ let rg110_find_companion comms companion_slug = codebase's test suite where commemoration order is asserted at all -- flagged here, and in CLAUDE.md's own "know what each layer cannot see" section, as a genuine, permanent limit, not merely this fix's own gap. *) +(* RG 113, sentence ONE -- "Commemoratio de Tempore fit primo loco" (the + commemoration of the Season is made in the first place), verified in the + electronic transcription docs/research/LT.txt and in both photographic + scans; register §6.26. + + §4 of the register and this file's own {!admit} already implement RG 113's + SECOND sentence ("In admittendis et ordinandis ALIIS commemorationibus, + servetur ordo tabellae praecedentiae") as the admission/ordering criterion, + adopted on the ef-rg16a branch. The first sentence was never implemented: + ordering was by {!band} alone, for every commemoration including the + seasonal one. + + RECONCILED WITH RG 110(c), which says the inseparable Peter/Paul pair is + added "ante omnes alias commemorationes". The two rubrics share a technical + term: RG 113's own sentence fixes the sense of *aliae commemorationes* as + "the ones other than the de Tempore commemoration it has just placed primo + loco", and RG 110(c) sits three paragraphs earlier in the same code. Read + consistently, RG 110(c) puts the pair at the head of the OTHERS, not ahead + of the season. Hence this runs AFTER {!rg110_additions}, not before: + RG 110(c)'s own "immediate" adjacency (trigger then companion, fix round 1, + register §6.3) is preserved by {!List.partition}'s stability, and the + seasonal commemoration then steps in front of the pair rather than + splitting it. + + [origin] is the exact discriminator the kernel already carries -- a + candidate is {!Precedence.Temporal} iff it came from the rite's temporal + cycle, which is what "de Tempore" means. Measured over 1583-9999, no day + ever carries more than one such commemoration (RG 112(c), "an Office, Mass + or commemoration of the season excludes another seasonal commemoration", + holding empirically), so this promotes at most one entry; [partition] is + used rather than a find-and-move so the zero case and the never-observed + many case are both total without a special branch. *) +let rg113_season_first + (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list) : + (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list = + let de_tempore (c, _) = c.Precedence.origin = Precedence.Temporal in + let seasonal, others = List.partition de_tempore comms in + seasonal @ others + let rg110_additions (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege * int) list) ~(observed : Vocab_ef.rank Precedence.candidate) (normal : (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list) : @@ -1702,7 +1741,7 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) as the non-Sunday II-class case, just with room for two. *) List.map drop_band (take 2 sorted) in - rg110_additions comms ~observed normal + rg113_season_first (rg110_additions comms ~observed normal) (* Task 11: RG 96 -- where an impeded I-class feast lands (docs/research/ rules-register.md §4, "Transfer/translation"). [band] decides who is diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index 790dab6..3f3e070 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -1170,12 +1170,40 @@ let unrelated_comm_synth = contest; it adds Paul regardless. *) let lent_feria_synth = (cand ~origin:P.Temporal ~rank:V.Class3 "ef-lent-2-monday-synth", P.Privileged, 180) +(* RG 113 sentence ONE (docs/research/rules-register.md §6.26): + "Commemoratio de Tempore fit primo loco." The commemoration OF THE SEASON + is made in the first place; the second sentence's table order governs the + OTHERS. Every other candidate in this table is [origin = P.Temporal] by + [cand]'s own default, so the rule is an identity on them and no existing + row's expectation moves; these two candidates exist to make a MIXED-origin + row possible, which is the only shape that can witness it. + + This file's [admit_cases] is the ONLY place in the whole suite that + asserts commemoration ORDER -- the differential never compares + commemorations, the oracle sorts both sides into a multiset, and + test_golden.ml's own [describe] deliberately sorts its comms field. So a + row here is the only thing standing between this rule and a silent + regression. *) +let sanctoral_hi = + { (cand ~rank:V.Class2 "ef-sanctoral-hi") with P.origin = P.Sanctoral } + +let seasonal_lo = cand ~rank:V.Class3 "ef-seasonal-lo" + let admit_cases = [ (* RG 111 (§4): "I class: none save one privileged." *) ( "I-class day, only an ordinary commemoration due -> none admitted", observed_class1, observed_class1, [ po ordinary_hi P.Ordinary ], [] ); + (* RG 113 s1: the de Tempore commemoration leads, even though the + sanctoral one outranks it on RG 91's table (Class2 beats Class3) and + would therefore come first under sentence TWO alone. Both slots of a + III-class day are filled, so this row tests ORDER, not selection. *) + ( "III-class day, seasonal + sanctoral both admitted -> the SEASONAL one \ + first (RG 113 s1), despite the sanctoral one's higher table position", + observed_class3, observed_class3, + [ po sanctoral_hi P.Ordinary; po seasonal_lo P.Ordinary ], + [ "ef-seasonal-lo"; "ef-sanctoral-hi" ] ); ( "I-class day, ordinary + privileged both due -> only the privileged \ one, regardless of the ordinary one's higher dignity", observed_class1, observed_class1, -- cgit v1.3