From c7f9b66ffe0c265ab049afc8948317e9daded9e7 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 15:57:14 +0200 Subject: test(ef): reach RG 91 entry 27 through the real pipeline, end-to-end Entry 27's band branch has been unreachable for the right reason since it was written: precedence_ef.ml's own comment on it acknowledged the row existed, but the one test exercising it built a hand-typed candidate, never a real Temporal_ef.temporal output, and nothing built the office itself. Closes that gap on both sides: - The band-table row now sources its candidate from Temporal_ef.temporal directly (of_temporal), the same review-finding-3 discipline the Ember/vigil rows already use, so a future regression in the office's own construction fails this row too, not only the unit-level test. - Two new resolve-level end-to-end tests, using the real band/ disposition/admit triple (not stubs): the office winning a real otherwise-unoccupied Saturday and admitting an ordinary commemoration alongside it (1 August 2026, Holy Machabees -- the task brief's own worked oracle example), and the office genuinely losing to a real competing feast and being omitted under RG 26, with no confusion from the one live data witness that also carries subject Bvm (12 September, Most Holy Name of Mary) -- proving RG 26's rank-keyed omission fires before any subject-keyed rule ever could. - Two golden pins mirroring the same two dates, checked by hand against `colitur day 2026` before being typed in, following this file's own "a golden test blesses whatever it is given, verified independently" discipline. The existing 2038-03-06 pin (RG 91 entry 24 outranking a Class4 candidate) is corrected in passing: that date is itself a Saturday, so the displaced candidate the pin's own omitted-list check names is now precisely entry 27's own shape, not entry 28's bare catch-all -- the win/lose outcome the pin proves is unchanged, only its own more precise citation. Mutation-tested (recorded in the task report): reverting is_bvm_saturday to a constant false reddens the dedicated unit test, the new end-to-end resolve test, and the 2026-08-01 golden pin, while leaving the 2026-09-12 "loses to a real feast" pin untouched, exactly as expected since that date's outcome never depended on the fix. --- test/test_precedence_ef.ml | 102 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 4 deletions(-) (limited to 'test/test_precedence_ef.ml') diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index ed9a933..475ae6e 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -291,10 +291,21 @@ let cases = cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "vigil-of-st-lawrence", 260 ); (* Entry 27 -- RG 91 entry 27 (§4): an otherwise-unoccupied IV-class - Saturday. *) - ( "27 Office of the BVM on Saturday", off 62, - cand ~rank:V.Class4 "ef-time-after-pentecost-1-sat", - 270 ); + Saturday. ef-bvm-saturday task: sourced from [Temporal_ef.temporal]'s + own real output (see [of_temporal], the same review-finding-3 + discipline entry 18's/21's own rows above use), not a hand-typed + slug/rank pair -- this row used to be the one place in this whole + suite where [band]'s entry-27 branch was exercised by a value nothing + in [Temporal_ef.temporal] could actually produce (the office itself + was unbuilt): [band] read [rank = Class4 && weekday = Sat] + correctly, but nothing proved [temporal] ever handed it a candidate + shaped that way for the RIGHT reason. Now it does: [of_temporal (off + 62)] is 6 June 2026, a real otherwise-contested Time-after-Pentecost + Saturday (Norbert, Class3, wins the DAY outright in the real + pipeline -- irrelevant here, since this row tests [band] on the + TEMPORAL CANDIDATE alone, which [Temporal_ef.temporal] builds as the + BVM Saturday office regardless of what wins). *) + ( "27 Office of the BVM on Saturday (from Temporal_ef.temporal)", off 62, of_temporal (off 62), 270 ); (* Entry 28 -- RG 91 entry 28 (§4): the unqualified IV-class catch-all. *) ("28 IV-class feria", off 65, cand ~rank:V.Class4 "ef-time-after-pentecost-1-tue", 280); (* Not an RG 91 row at all: a I-class candidate marked as a vigil, which @@ -1421,6 +1432,81 @@ let test_ii_class_sunday_privileged_witness_admitted_end_to_end () = [ ("ef-some-saint", "omitted: admission limit reached") ] (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted) +(* RG 91 entry 27 / RG 78-79 (Caput IX, "De sancta Maria in sabbato") reached + end-to-end through the REAL pipeline -- this is THE hazard this task's own + brief names explicitly: {!PE.band}'s entry-27 branch has been reachable + only in isolation (the table-driven row above) since it was written, never + through a real [Temporal_ef.temporal] candidate winning an actual day, so + this test is checked for whether it exercises entry 27 for the RIGHT + reason -- proved by the mutation test recorded in the task report + (reverting temporal_ef.ml's own [is_bvm_saturday] branches back to the + generic ferial fallback reddens THIS test, not merely the [band]-table + row, which would still pass unchanged since it is sourced from real + [T.temporal] output either way). + + 1 August 2026, matching the task brief's own worked example against the + oracle exactly: a Time-after-Pentecost Saturday, otherwise unoccupied but + for Holy Machabees (Commemoration_only, Class3, real data/ef/sanctoral.sexp + entry, 1 August) -- the office wins outright (band 270, ahead of Holy + Machabees's own [unclassified]) and the Commemoration_only loser still + rides along as an ordinary commemoration, the SAME shape the oracle's own + row for this date shows ("V Mass of the B. V. M. -- Salve, Sancta Parens", + commemorating Holy Machabees). *) +let test_bvm_saturday_wins_and_commemorates_end_to_end () = + let date = mk 2026 8 1 in + let day_ctx = ctx date in + let bvm_saturday = of_temporal date in + let holy_machabees = + cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~layer:PE.universal_layer + "holy-machabees" + in + let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ holy_machabees ] in + Alcotest.(check string) "the BVM Saturday office is observed, not Holy Machabees" + "ef-time-after-pentecost-9-saturday" (S.to_string resolution.P.observed.P.cel.Cel.slug); + Alcotest.(check string) "observed rank is class-4, unconditional" "class-4" + (V.rank_to_string resolution.P.observed.P.cel.Cel.rank); + Alcotest.(check string) "observed colour is white, overriding the season's own green" "white" + (Col.to_string resolution.P.observed.P.cel.Cel.colour); + Alcotest.(check bool) "observed subject is Bvm" true (resolution.P.observed.P.cel.Cel.subject = Sub.Bvm); + Alcotest.(check (list (pair string string))) "Holy Machabees still rides along as an ordinary commemoration" + [ ("holy-machabees", "Ordinary") ] + (List.map + (fun (c, p) -> (S.to_string c.P.cel.Cel.slug, match p with P.Privileged -> "Privileged" | P.Ordinary -> "Ordinary")) + resolution.P.commemorations); + Alcotest.(check int) "nothing omitted" 0 (List.length resolution.P.omitted) + +(* The other half of the SAME hazard: a real Saturday where RG 78's own + protasis is FALSE (a genuine sanctoral feast wins the day outright), so + the BVM-Saturday-shaped temporal candidate must lose, and be OMITTED under + RG 26 (a temporal Class4 non-vigil loser is never commemorated) exactly + like the plain ferial candidate it replaces always was -- proving [band]/ + [disposition] need no special case for this office at all, the design + claim the task report makes explicitly. 12 September 2026 is a Saturday + where `most-holy-name-of-mary` (data/ef/adjustments.sexp's own [Set_subject + Bvm] entry, Class3 Feast) wins outright -- ALSO tagged [subject = Bvm], + the one live data witness that could in principle confuse a + subject-keyed disposition rule with RG 26's own rank-keyed one: this test + proves they do NOT collide (RG 26 fires first, unconditionally, on the + loser's own [rank = Class4], never reaching a subject comparison at all). *) +let test_bvm_saturday_loses_to_a_real_feast_end_to_end () = + let date = mk 2026 9 12 in + let day_ctx = ctx date in + let bvm_saturday = of_temporal date in + let name_of_mary = + cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Bvm ~layer:PE.universal_layer + "most-holy-name-of-mary" + in + let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ name_of_mary ] in + Alcotest.(check string) "Most Holy Name of Mary is observed, not the BVM Saturday office" + "most-holy-name-of-mary" (S.to_string resolution.P.observed.P.cel.Cel.slug); + Alcotest.(check (list string)) "nothing is commemorated" [] + (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations); + Alcotest.(check (list (pair string string))) + "the BVM-Saturday-shaped temporal candidate is omitted under RG26, not commemorated -- no Bvm-vs-Bvm \ + confusion with RG112(a)-style subject logic" + [ ("ef-time-after-pentecost-15-saturday", "omitted: yielded to a higher day") ] + (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted) + (* RG 16(a) (register §6.0) reached end-to-end through the REAL pipeline ([PE.band], [PE.disposition] AND [PE.admit] together, not any one of them in isolation as this file's own hand-tagged rows test them @@ -1894,6 +1980,14 @@ let suite = Alcotest.test_case "II-class Sunday override: RG109(b) witness admitted over an ordinary saint, end-to-end" `Quick test_ii_class_sunday_privileged_witness_admitted_end_to_end; + Alcotest.test_case + "RG91 e27/RG78: the BVM Saturday office wins and commemorates Holy Machabees, end-to-end \ + (2026-08-01)" + `Quick test_bvm_saturday_wins_and_commemorates_end_to_end; + Alcotest.test_case + "RG91 e27/RG78/RG26: the BVM Saturday office loses to a real feast and is omitted, end-to-end \ + (2026-09-12)" + `Quick test_bvm_saturday_loses_to_a_real_feast_end_to_end; Alcotest.test_case "RG16(a): a Feast of the Lord suppresses the Sunday's own commemoration entirely, \ end-to-end (2028-08-06)" -- cgit v1.3 From dce208f57cfeeee0715cf35389d37e7ce98227ae Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 17:09:27 +0200 Subject: fix(ef): RG 112(d) excludes a same-BVM commemoration (fix round 1, F1) The BVM Saturday Office is itself "de B. Maria Virg."; RG 112(d) (Caput XVI, "De Commemorationibus") excludes another commemoration whose own oration invokes the SAME BVM's intercession. our-lady-of-mt-carmel (16 July, Commemoration_only) was wrongly admitted as an ordinary commemoration alongside the Office on every 16-July-Saturday -- its own collect explicitly invokes "her" (eius) patronage, and both photographic scans print a rubric anticipating exactly this collision (an either/or of Mass texts, not a joint commemoration). Precedence_ef gains marian_slugs (a closed, hand-verified list of sanctoral slugs that are themselves feasts of the BVM) and is_bvm_office (true for subject = Bvm OR a marian_slugs member -- the disjunction is load-bearing: the Office is identified only by subject, since its own slug is deliberately reused from the ordinary ferial fallback; Mt Carmel is identified only by the list, since its own subject tag is Saint, not Bvm). disposition gains a new branch, checked before the pre-existing Commemoration_only catch-all, since the one live witness is Commemoration_only and that branch's own "always Commemorate" had no exception for this shape. Checked exhaustively against every Marian-named sanctoral entry: only Commemoration_only-status entries can ever reach this branch live (a Feast-status one always wins outright instead of losing). Of the two such entries in the data, only Mt Carmel is reachable -- our-lady-of-ransom (24 September) is provably unreachable: 24 September falling on a Saturday forces 1 September to a Thursday, which by construction makes 24 September the September Ember Saturday, Class2, every time, not a sampled coincidence. Re-measured against the full pre-change binary: the original 75,853-day blast radius is unchanged in total, splitting into 74,633 colour-only + 1,220 colour+comms (16-July Saturdays). Mutation-tested: exactly 3 new tests redden, nothing else. --- lib/rites/rite_ef/precedence_ef.ml | 176 +++++++++++++++++++++++++++++++++++-- test/test_differential.ml | 32 ++++++- test/test_golden.ml | 24 ++++- test/test_precedence_ef.ml | 76 ++++++++++++++++ test/test_temporal_ef.ml | 43 ++++++--- 5 files changed, 324 insertions(+), 27 deletions(-) (limited to 'test/test_precedence_ef.ml') diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index 5a2d307..302df98 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -364,14 +364,21 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc else if is_temporal && rank = Class3 && season = Advent then 250 (* 26: III-class vigils (St Lawrence). *) else if rank = Class3 && is_vigil then 260 - (* 27: Office of the BVM on Saturday -- every otherwise-unoccupied IV-class - Saturday, per the historical default that fills it; ordinary Mass - propers still make Rogation Mon/Tue/Wed proper without changing the - Office (RG 88, see temporal_ef.ml's [temporal]), so those never carry - this entry unless they happen to fall on the Saturday itself. Excludes - vigils for the same reason 11-13/14/16/19/20/23/24 do: RG 91 has no - IV-class vigil at all (RG 91's own vigil list, §4 "Vigils", - stops at III class), so one would be an anomaly, not this entry. *) + (* 27: Office of the BVM on Saturday -- RG 78 (Caput IX, "De sancta Maria + in sabbato"), both photographic scans and the electronic + transcription, word for word (docs/research/rules-register.md §4/§6.4; + Rite_ef.Temporal_ef's own [bvm_saturday_names] citation has the full + argument): "78. In sabbatis, in quibus occurrit Officium de feria IV + classis, fit de sancta Maria in sabbato" -- on Saturdays on which the + Office of a IV-class feria occurs, [the Office] is made of Holy Mary + on Saturday instead. Every otherwise-unoccupied IV-class Saturday + reaches this branch; ordinary Mass propers still make Rogation + Mon/Tue/Wed proper without changing the Office (RG 88, see + temporal_ef.ml's [temporal]), so those never carry this entry unless + they happen to fall on the Saturday itself. Excludes vigils for the + same reason 11-13/14/16/19/20/23/24 do: RG 91 has no IV-class vigil at + all (RG 91's own vigil list, §4 "Vigils", stops at III class), so one + would be an anomaly, not this entry. *) else if is_temporal && (not is_vigil) && rank = Class4 && weekday = Date.Sat then 270 (* 28: IV-class ferias -- the unqualified catch-all (temporal_ef.ml's own comment on [ferial_rank] cites the same primary text, "Feriae IV @@ -380,6 +387,148 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc else if (not is_vigil) && rank = Class4 then 280 else unclassified +(* RG 112(d) (Caput XVI, "De Commemorationibus"), the BVM half, fix round 1 + of the ef-bvm-saturday task (coordinator finding F1) -- both photographic + scans, word for word (docs/research/rules-register.md §4's own RG 112 + entry already carries this sub-clause, unbuilt until this fix): *"item, + Officium, Missa aut commemoratio de B. Maria Virg. aut de aliquo Sancto + vel Beato excludit aliam commemorationem aut orationem in qua eiusdem B. + Mariae Virg., vel Sancti aut Beati intercessio imploretur: quod tamen non + valet de oratione dominicae vel feriae, in qua fit invocatio eiusdem + Sancti."* -- the Office, Mass or commemoration OF the Blessed Virgin Mary + (or of some Saint or Blessed) excludes ANOTHER commemoration or oration + in which the intercession OF THE SAME BVM (or Saint or Blessed) is + invoked -- which however does not hold of the oration of a Sunday or + feria, in which invocation of the same Saint occurs. + + Found live by the fix-round review: the BVM Saturday Office (RG 91 entry + 27, [temporal_ef.ml]'s own [bvm_saturday_names]) is itself "de B. Maria + Virg." -- so when it is observed, RG 112(d) excludes any OTHER admitted + candidate whose own oration invokes the SAME BVM's intercession, not + merely commemorates her in passing. Real live witness: 16 July, + "our-lady-of-mt-carmel" (data/ef/sanctoral.sexp, Class3, + [Commemoration_only]) falls on a Saturday 8 times in the 2005-2050 + fixture alone; its own collect, both photographic scans, word for word + (Caput "Die 16 iulii, Beatae Mariae Virginis de Monte Carmelo, + Commemoratio"): *"...concede propitius; ut, cuius hodie Commemorationem + solemni celebramus officio, EIUS muniti praesidiis, ad gaudia sempiterna + pervenire mereamur"* -- "her" ("eius") -- grant, we beseech Thee, that we, + fortified by HER patronage, whose Commemoration we solemnly celebrate + today, may be permitted to arrive at everlasting joys -- an explicit + invocation of the SAME BVM's own intercession/patronage, precisely RG + 112(d)'s own trigger. + + The exception clause ("non valet de oratione dominicae vel feriae") does + NOT rescue this: RG 91 lists entry 27 as its own table row, separate from + entry 28's "feriae IV classis" -- this Office is neither a Sunday nor, + once RG 78 has substituted it in, a plain "feria" in Caput IV's own + sense (RG 21: "Nomine feriae intelleguntur singuli dies hebdomadae" -- + an ordinary weekday's OWN office, which RG 78 replaces, not merely + supplements). + + ALSO found on the very same page, both photographic scans, word for + word, confirming the collision is real and rubric-anticipated (not + merely this codebase's own inference): *"Si Commemoratio B. Mariae + Virg. de Monte Carmelo venerit in sabbato, Missa dici potest aut de + sancta Maria in sabbato, aut propria de Commemoratione B. Mariae Virg. + de Monte Carmelo."* -- if the Commemoration of the BVM of Mount Carmel + falls on a Saturday, the Mass MAY be said EITHER of Holy Mary on + Saturday OR properly of the Commemoration of the BVM of Mount Carmel -- + an explicit EITHER/OR between two MASS TEXTS, not an instruction to + commemorate one in the other's Office. Read together with RG 112(d): + the OFFICE question (is Mt Carmel commemorated at all) and the MASS + question (which of the two propers is said that day) are two different + questions -- RG 112(d) answers the first (excluded); this rubric answers + the second, and is a Mass-propers selection detail of exactly the same + kind as the I-V numbered cycle (RG 309(a)) [bvm_saturday_names]'s own + citation already puts out of scope for the SAME reason (colitur computes + no citations/readings at all yet, Plan 4) -- not modelled here either. + + [marian_slugs] is a CLOSED, HAND-VERIFIED list, not a claim to have read + every one of these entries' own Latin orations on the scan (only + Mt Carmel's, quoted above, was actually verified against the primary + text for this fix) -- built from data/ef/sanctoral.sexp's own [names.en] + field, restricted to entries that are themselves a feast/commemoration + OF the Blessed Virgin Mary in her own right (never merely a feast of + someone else that happens to mention her -- "St. Anne, Mother of the + Blessed Virgin", "St. Joseph, Spouse of the Bl. Virgin Mary", "St. + Anthony Mary Claret" and similarly-named entries are deliberately + EXCLUDED, checked one by one). This is the SAME modelling simplification + {!disposition}'s own pre-existing RG 112(a) branch already makes for + "eiusdem Divinae Personae" (read as simply "both [subject = Lord]", + that branch's own comment: "safe at today's data's own granularity") -- + here read as "titled a feast of the BVM", not verified oration-by- + oration, and said so explicitly rather than overclaimed. TWO entries + found and deliberately EXCLUDED after individual consideration, not + merely omitted: "dedication-of-the-basilica-of-st-mary-major" (5 + August, Class3, Feast) -- a DEDICATION feast (of the building, "In + Dedicatione S. Mariae ad Nives"), whose own oration could not be found + in either scan under this exact heading to confirm it invokes her + intercession the same way an ordinary Marian commemoration does, so left + out per this project's "a wrong citation is worse than a missing one" + rule; and "purification-of-the-blessed-virgin-mary" (2 February, + Class2), already deliberately tagged [subject = Lord] by an earlier + task's own ruling (register §6.0, following the oracle's Sunday- + displacing treatment) for an unrelated reason -- its own RG 112(a) + Lord-vs-Lord exclusion already covers it against another Lord-subject + winner, and at Class2 it can never lose to a Class4 candidate in the + first place (this or any other), so including or excluding it here has + no live consequence either way; excluded for consistency with the + earlier ruling rather than silently overriding it. + + What this rule COVERS: any admitted candidate on {!marian_slugs}' own + closed list, or tagged [subject = Bvm], losing to a WINNER that is + itself either {!marian_slugs}-listed or [subject = Bvm] -- currently + live only for the BVM Saturday Office (subject-tagged, its own slug + deliberately NOT on this list, {!bvm_saturday_names}'s own "Slug" + citation in temporal_ef.ml) as winner against Mt Carmel as loser, plus + (checked, not live: 24 September, "our-lady-of-ransom", the ONLY other + {!Celebration.status.Commemoration_only} Marian entry in the data) -- + PROVEN structurally unreachable: 24 September falling on a Saturday + forces 1 September to be a Thursday (23 days = 3 weeks 2 days earlier), + which by {!third_sunday_of_september}'s own construction makes 24 + September the September Ember Saturday EVERY time, RG 91 entry 18 + (Class2), always outranking entry 27 -- not a sampled coincidence, an + exact day-of-week identity, checked against several sample years above + before being generalised. What this rule does NOT cover, stated + plainly: a hypothetical non-Marian-titled saint whose own oration + happens to invoke the BVM's intercession in passing (RG 112(d)'s own + text does not restrict itself to Marian-TITLED commemorations) -- no + oration text is stored anywhere in this codebase to detect that, and + none is guessed at here. Also does not cover RG 112(d)'s OTHER half (two + commemorations of the SAME non-BVM saint) -- unbuilt, unaffected by this + fix, register §4/§6 already tracks it as open. + + Checked for what this fix does NOT change: {!Precedence.resolve} holds + [Commemoration_only] candidates out of the WINNER contest entirely, so + Mt Carmel could never have been [observed] either before or after this + fix -- only its own admission AS a commemoration changes. Verified live + against real 2033/2005 data (both years 16 July is a Saturday): before + this fix, [comms] included "our-lady-of-mt-carmel:ordinary"; after, + [comms] is empty and the omitted list carries it with reason "omitted: + yielded to a higher day", the same generic reason every other [Omit] + disposition in this function produces. *) +let marian_slugs = + [ "annunciation-of-the-blessed-virgin-mary"; + "assumption-of-the-blessed-virgin-mary"; + "immaculate-conception-of-the-blessed-virgin-mary"; + "immaculate-heart-of-mary"; + "maternity-of-the-blessed-virgin-mary"; + "most-holy-name-of-mary"; + "nativity-of-the-blessed-virgin-mary"; + "our-lady-of-lourdes"; + "our-lady-of-mt-carmel"; + "our-lady-of-ransom"; + "our-lady-of-the-rosary"; + "presentation-of-the-blessed-virgin-mary"; + "queenship-of-the-blessed-virgin-mary"; + "seven-sorrows-of-the-blessed-virgin-mary"; + "visitation-of-the-blessed-virgin-mary" ] + +let is_bvm_office (c : Vocab_ef.rank Precedence.candidate) = + c.Precedence.cel.Celebration.subject = Subject.Bvm + || List.mem (Slug.to_string c.Precedence.cel.Celebration.slug) marian_slugs + (* Task 8: what happens to the day's LOSING candidate (docs/research/ rules-register.md §4, "Occurrence" RG 92-95 and "Vigils" RG 33, plus RG 94; also §6.0/Caput III "De Dominicis" RG 16(a), below). [band] above @@ -602,7 +751,16 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate) let open Vocab_ef in let cel = loser.Precedence.cel in let is_temporal = loser.Precedence.origin = Precedence.Temporal in - if cel.Celebration.status = Celebration.Commemoration_only then + if is_bvm_office winner && is_bvm_office loser then + (* RG 112(d) -- see [marian_slugs]'s own citation above for the full + argument. Checked FIRST, even ahead of the [Commemoration_only] + branch immediately below, because the one live witness + (Mt Carmel, 16 July) IS [Commemoration_only] -- that branch's own + "always Commemorate, nothing overrides it" previously had no + exception for two commemorations invoking the identical BVM, the + exact gap this fix closes. *) + Precedence.Omit + else if cel.Celebration.status = Celebration.Commemoration_only then (* Always -- checked before RG 33's omission and RG 95's transfer so neither can override it: a Commemoration_only entry can never win (Precedence.resolve holds it out of the band contest entirely, see diff --git a/test/test_differential.ml b/test/test_differential.ml index 3f416fd..5093484 100644 --- a/test/test_differential.ml +++ b/test/test_differential.ml @@ -138,6 +138,7 @@ module Slug = Colitur_kernel.Slug module Date = Colitur_kernel.Date module Cel = Colitur_kernel.Celebration module Colour = Colitur_kernel.Colour +module Subject = Colitur_kernel.Subject module V = Rite_ef.Vocab_ef (* Same relative paths test_rite_ef.ml/test_sanctoral_ef.ml use: dune test @@ -216,6 +217,14 @@ type row = { slug : string; rank : string; colour : string; + (* Fix round 1 (coordinator finding F3): ONLY ever populated for colitur's + own row (see [colitur_rows_2005_2050] below) -- lectio's own fixture + format has no subject column at all, so [row_of_line] (the lectio-side + parser) sets this to "-", a placeholder never read on that side. Every + Layer C predicate that reads [subject] must therefore read it off the + COLITUR row [c], never [l] -- the same asymmetry [row]'s other fields + do not have, called out here rather than left implicit. *) + subject : string; } let read_lines path = @@ -232,7 +241,7 @@ let read_lines path = let row_of_line line = match String.split_on_char ' ' line with | date :: weekday :: season :: week :: slug :: rank :: colour :: _others -> - { date; weekday; season; week; slug; rank; colour } + { date; weekday; season; week; slug; rank; colour; subject = "-" } | _ -> Alcotest.failf "malformed fixture line (fewer than 7 fields): %S" line let lectio_rows () = List.map row_of_line (read_lines fixture_path) @@ -271,7 +280,8 @@ let colitur_rows_2005_2050 () = week; slug = Slug.to_string cel.Cel.slug; rank = V.rank_to_string cel.Cel.rank; - colour = Colour.to_string cel.Cel.colour + colour = Colour.to_string cel.Cel.colour; + subject = Subject.to_string cel.Cel.subject } :: !rows | None -> Alcotest.failf "internal error: no resolved day for %s" (Date.to_iso8601 !d)); @@ -535,8 +545,24 @@ let holy_name_fallback_dates_2005_2050 = IV-class Saturday", reduces exactly to this triple), not a short, individually-interesting list of dates the way C14/C15/C16 above are. *) let is_bvm_saturday_row (c : row) diffs = + (* Fix round 1 (coordinator finding F3): [c.subject] added -- the ONLY + field in this predicate that pins WHICH celebration is observed, not + merely its shape. Every other Layer C entry pins a colitur slug (C1's + own [jan_6_13_slug], C6's [nativity_octave_day_slugs], C8/C14/C15/C16's + own literal slug checks); this entry could not, because the office + deliberately REUSES the ordinary ferial slug (Rite_ef.Temporal_ef's own + [bvm_saturday_names] citation, "Slug" paragraph) -- there is no fixed + slug string to pin. [subject = "bvm"] is the field that DOES uniquely + identify the office (set nowhere else the differential's own + [colitur_rows_2005_2050] can produce a Saturday/Class4/white + combination for), closing the gap a shape-only predicate left open: + without this conjunct, a FUTURE bug that made some OTHER white, + Class4, Saturday candidate exist (Christmastide/Paschaltide, where + [season_colour] is already white, so a real bug there could slip + through unnoticed by colour alone) would be silently absorbed here + too. *) diffs = [ Colour_f ] && String.equal c.weekday "saturday" && String.equal c.rank "class-4" - && String.equal c.colour "white" + && String.equal c.colour "white" && String.equal c.subject "bvm" (* [layer_c_reason l c diffs] returns the [data/ef/expected-divergences.sexp] [id] this row-pair's remaining (post Layer A/B) diff set belongs to, or diff --git a/test/test_golden.ml b/test/test_golden.ml index bb4bcfc..fdfc01d 100644 --- a/test/test_golden.ml +++ b/test/test_golden.ml @@ -940,6 +940,24 @@ let test_bvm_saturday_loses_to_most_holy_name_of_mary_2026 () = colour=white subject=bvm name_la=- comms=[] in=- out=[]" (describe d) +(* RG112(d), fix round 1 (coordinator finding F1): PE.marian_slugs's own + citation (precedence_ef.ml) has the full RG112(d) argument, including + the 16 July "Missa dici potest aut ... aut ..." rubric found on both + photographic scans. 16 July 2033 is a real otherwise-unoccupied + Saturday; before this fix, [comms] here read + "[our-lady-of-mt-carmel:ordinary]" (confirmed against real pre-fix + `colitur day 2033` output). *) +let test_bvm_saturday_excludes_mt_carmel_2033 () = + let d = fetch 2033 7 16 in + Alcotest.(check bool) + "2033-07-16: Mt Carmel is in [omitted] (RG112(d)), not silently dropped from the record" true + (omitted_has d "our-lady-of-mt-carmel"); + Alcotest.(check string) + "2033-07-16: the BVM Saturday office observed, white; NO commemoration of Mt Carmel (RG112(d))" + "2033-07-16 saturday season=time-after-pentecost week=5 slug=ef-time-after-pentecost-5-saturday \ + rank=class-4 colour=white subject=bvm name_la=Officium sanctae Mariae in sabbato comms=[] in=- out=[]" + (describe d) + let suite = ( "golden pins (known-tricky years)", [ Alcotest.test_case "Easter extreme: 1598 earliest (22 Mar, Gauss-verified)" `Quick @@ -1004,5 +1022,9 @@ let suite = Alcotest.test_case "RG91 e27/RG78/RG26: the BVM Saturday office loses to Most Holy Name of Mary (subject Bvm too) \ and is omitted, no confusion (2026-09-12)" - `Quick test_bvm_saturday_loses_to_most_holy_name_of_mary_2026 + `Quick test_bvm_saturday_loses_to_most_holy_name_of_mary_2026; + Alcotest.test_case + "RG112(d), fix round 1 (F1): the BVM Saturday office excludes Mt Carmel's own commemoration \ + (2033-07-16)" + `Quick test_bvm_saturday_excludes_mt_carmel_2033 ] ) diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index 475ae6e..6f8b5b5 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -805,6 +805,52 @@ let disposition_cases = "conversion-of-st-paul", cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "commemoration-of-the-baptism-of-the-lord", + "Commemorate(Ordinary)" ); + (* RG112(d), fix round 1 (coordinator finding F1) -- PE.marian_slugs's + own citation has the full argument (both scans, including the 16 + July "Missa dici potest aut ... aut ..." rubric). [of_temporal] on a + REAL 16-July-Saturday (2033) sources the winner from + Temporal_ef.temporal's own real output, the same review-finding-3 + discipline this file's Ember/vigil rows already use -- not a + hand-typed slug/subject pair that could silently drift from what the + office actually builds. The loser is the real data/ef/sanctoral.sexp + entry, [Commemoration_only], the exact shape that made this branch + need to be checked BEFORE the Commemoration_only catch-all. *) + ( "RG112(d): the BVM Saturday Office excludes a commemoration invoking \ + the SAME BVM's intercession -- Omit, not Commemorate (Mt Carmel, \ + 16 July 2033, a real otherwise-unoccupied Saturday)", + of_temporal (mk 2033 7 16), + cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint + ~layer:PE.universal_layer "our-lady-of-mt-carmel", + "Omit" ); + (* Conjunct 1/2, LOSER identity: an ORDINARY Commemoration_only saint + NOT on PE.marian_slugs must still reach the ordinary commemorate + flow even when the winner is the BVM Saturday Office -- proves + RG112(d) excludes a specific same-BVM collision, not every + commemoration the office happens to outrank (the real live shape, + 1 August, Holy Machabees, PE.marian_slugs's own comment). *) + ( "RG112(d) does NOT fire when the LOSER is not a Marian commemoration \ + -- an ordinary saint stays Commemorate(Ordinary) even under the BVM \ + Saturday Office (1 August 2026, Holy Machabees)", + of_temporal (mk 2026 8 1), + cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint + ~layer:PE.universal_layer "holy-machabees", + "Commemorate(Ordinary)" ); + (* Conjunct 2/2, WINNER identity: an ordinary (non-Bvm, non-Marian- + titled) winner must not exclude Mt Carmel either -- RG112(d) is + keyed on the WINNER also being "de B. Maria Virg.", not merely on + the loser's own identity. SYNTHETIC: no live date has an ordinary + Class3 saint winning outright while Mt Carmel also loses that same + day (Mt Carmel's own fixed date, 16 July, has no other real + sanctoral entry to construct this from) -- proves the conjunct is + real, not merely untested, the same "SYNTHETIC" discipline the + RG112(a) rows above already use for their own second conjunct. *) + ( "SYNTHETIC: RG112(d) does NOT fire when only the LOSER is Marian -- \ + an ordinary winner leaves Mt Carmel Commemorate(Ordinary)", + cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer + "ef-synthetic-ordinary-winner", + cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint + ~layer:PE.universal_layer "our-lady-of-mt-carmel", "Commemorate(Ordinary)" ) ] @@ -1507,6 +1553,32 @@ let test_bvm_saturday_loses_to_a_real_feast_end_to_end () = [ ("ef-time-after-pentecost-15-saturday", "omitted: yielded to a higher day") ] (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted) +(* RG112(d), fix round 1 (coordinator finding F1) -- PE.marian_slugs's own + citation has the full argument. End-to-end through the REAL pipeline + (PE.band, PE.disposition AND PE.admit together): 16 July 2033, a real + otherwise-unoccupied Saturday, with the real data/ef/sanctoral.sexp + [our-lady-of-mt-carmel] entry as the day's only competing candidate. + Before this fix, [resolution.P.commemorations] held + [our-lady-of-mt-carmel:Ordinary] (confirmed live against real 2005/2033 + `colitur day` output before this fix existed); after, it is empty and + Mt Carmel is reported omitted, not silently dropped. *) +let test_bvm_saturday_excludes_mt_carmel_end_to_end () = + let date = mk 2033 7 16 in + let day_ctx = ctx date in + let bvm_saturday = of_temporal date in + let mt_carmel = + cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint + ~layer:PE.universal_layer "our-lady-of-mt-carmel" + in + let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ mt_carmel ] in + Alcotest.(check string) "the BVM Saturday office is observed" + "ef-time-after-pentecost-5-saturday" (S.to_string resolution.P.observed.P.cel.Cel.slug); + Alcotest.(check (list string)) "Mt Carmel is NOT commemorated (RG112(d))" [] + (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations); + Alcotest.(check (list (pair string string))) "Mt Carmel is reported omitted, not silently dropped" + [ ("our-lady-of-mt-carmel", "omitted: yielded to a higher day") ] + (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted) + (* RG 16(a) (register §6.0) reached end-to-end through the REAL pipeline ([PE.band], [PE.disposition] AND [PE.admit] together, not any one of them in isolation as this file's own hand-tagged rows test them @@ -1988,6 +2060,10 @@ let suite = "RG91 e27/RG78/RG26: the BVM Saturday office loses to a real feast and is omitted, end-to-end \ (2026-09-12)" `Quick test_bvm_saturday_loses_to_a_real_feast_end_to_end; + Alcotest.test_case + "RG112(d), fix round 1 (F1): the BVM Saturday office excludes Mt Carmel, end-to-end \ + (2033-07-16)" + `Quick test_bvm_saturday_excludes_mt_carmel_end_to_end; Alcotest.test_case "RG16(a): a Feast of the Lord suppresses the Sunday's own commemoration entirely, \ end-to-end (2028-08-06)" diff --git a/test/test_temporal_ef.ml b/test/test_temporal_ef.ml index ff3cc0b..4e97d33 100644 --- a/test/test_temporal_ef.ml +++ b/test/test_temporal_ef.ml @@ -507,25 +507,40 @@ let test_colours () = (6 Jan) is a Tuesday, so the actual first-Sunday-after-Epiphany origin is 11 Jan -- exercising all four sub-stretches of the fix. *) let test_christmastide_feria_slugs () = - (* 26 Dec 2026 and 10 Jan 2026 are both SATURDAYS -- ef-bvm-saturday task - (RG 78, "De sancta Maria in sabbato"): every Christmastide feria - [christmastide_feria_slug] builds is unconditionally Class4 - ([ferial_rank]'s own catch-all outside Advent/Lent/Passiontide), so on a - Saturday {!Rite_ef.Temporal_ef.bvm_saturday_names}'s own override fires - here too -- but per that citation's own "Slug" paragraph, the SLUG is - deliberately left UNCHANGED (reused from this same "-0-"/"-2-" naming, - not replaced): only colour/subject/name change. So the slug assertions - below are unaffected by the fix; what is NEW is the colour/subject - override on these two specific dates, asserted alongside them. *) + (* 26 Dec 2026 is a SATURDAY -- but, fix round 1 (coordinator finding F2), + it can NEVER actually hold the BVM Saturday office in the real + resolved pipeline: 26/27/28 December are St Stephen/St John/the Holy + Innocents (RG 67-70), fixed, unconditional, real data/ef/sanctoral.sexp + entries every single year (rank >= Class2), which always outrank a + Class4 temporal candidate -- so RG 78's own protasis ("in quibus + OCCURRIT Officium de feria IV classis") is never satisfied by ANY day + in the 26-28 December stretch, in any year. [T.temporal] itself has no + visibility into the sanctoral layer at all (by design -- the temporal + candidate is computed independent of what saint's day it is; Precedence + decides the winner afterward), so it still unconditionally tags this + date [subject = Bvm] -- a genuine, correct fact about [T.temporal]'s + own pure computation, but NOT a claim that this shape is ever the real + OBSERVED office, and pinning it with RG-78 citation language as if it + were would overclaim. Only the SLUG is asserted here (unaffected + either way, unconditionally correct); the override demonstration + itself uses 3 January and 10 January below instead -- both, checked + directly against data/ef/sanctoral.sexp, have no competing entry in + any year, so the BVM Saturday office genuinely IS reachable there. *) Alcotest.(check string) "26 Dec (Sat)" "ef-christmas-0-saturday" (slug_of (d 2026 12 26)); - Alcotest.(check string) "26 Dec (Sat): the BVM Saturday office overrides the season colour (white, RG 120(b))" - "white" (colour_of (d 2026 12 26)); - Alcotest.(check bool) "26 Dec (Sat): subject Bvm" true (subject_of (d 2026 12 26) = Sub.Bvm); Alcotest.(check string) "28 Dec (Mon)" "ef-christmas-0-monday" (slug_of (d 2026 12 28)); + (* 3 Jan 2026 is a SATURDAY, genuinely reachable (data/ef/sanctoral.sexp + has no 3 January entry) -- the real demonstration for the "-1-" + stretch that 26 December's own Saturday could not honestly provide. *) + Alcotest.(check string) "3 Jan (Sat): slug stays the ordinary \"-1-\" naming" "ef-christmas-1-saturday" + (slug_of (d 2026 1 3)); + Alcotest.(check string) "3 Jan (Sat): the BVM Saturday office overrides the season colour (white, RG 120(b))" + "white" (colour_of (d 2026 1 3)); + Alcotest.(check bool) "3 Jan (Sat): subject Bvm" true (subject_of (d 2026 1 3) = Sub.Bvm); Alcotest.(check string) "2 Jan (Fri)" "ef-christmas-1-friday" (slug_of (d 2026 1 2)); Alcotest.(check string) "5 Jan (Mon)" "ef-christmas-1-monday" (slug_of (d 2026 1 5)); (* 7-10 Jan precede the actual origin Sunday (11 Jan): colitur-only, not - lectio's "week 1" key, precisely to avoid colliding with 12-13 Jan below. *) + lectio's "week 1" key, precisely to avoid colliding with 12-13 Jan below. + 10 January is ALSO genuinely reachable (no sanctoral entry that date). *) Alcotest.(check string) "7 Jan (Wed, before the origin)" "ef-christmas-2-wednesday" (slug_of (d 2026 1 7)); Alcotest.(check string) "10 Jan (Sat, before the origin)" "ef-christmas-2-saturday" (slug_of (d 2026 1 10)); Alcotest.(check bool) "10 Jan (Sat): subject Bvm" true (subject_of (d 2026 1 10) = Sub.Bvm); -- cgit v1.3