From 84333a8c74ddb3988ad88398632ca5e1c7788e56 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 20:15:42 +0200 Subject: test(ef): commemoration, transfer, and privilege coverage for the Major Litanies test_precedence_ef.ml: a privilege_cases row for RG109(f), deliberately rank Class4 so it cannot pass via the pre-existing "of a I-class day" branch by accident. Three disposition_cases rows: the two RG80 trigger shapes (Transfer, not Commemorate) and a synthetic third row proving the winner-slug guard is specific to the two named trigger days, not "any I-class winner". Two transfer_target unit tests, both using a pathological always-blocking occupant to prove no RG96 search runs at all for this candidate -- a much stronger witness than a cooperative occupant, which could pass by accident. test_golden.ml: three new pinned tests through the real data pipeline -- the ordinary case (2026-04-25, matches the oracle's own M5 date), the four Sunday-displacement years in 2005-2050 (2010, 2021, 2027, 2032, RG111(b)), and the Easter-Monday transfer shape (2011, complementing the Easter-Sunday shape already pinned by the existing 1666/2038 Easter-extreme tests, now widened to include the transfer and its target). test_rite_ef.ml: the existing "no transfer lands inside the Easter octave" property gets a named, cited exemption for the Major Litanies (RG80's own deliberate exception), plus a positive companion test asserting the exemption is not vacuous (exactly two landings in 2005-2050, both Easter+2). That companion test was written but never registered in its own suite list -- caught while mutation-testing the transfer (a mutation that should have failed it passed silently); fixed by registering it. Mutation-tested, each of the three parts separately (recorded in the task's own report): moving the data entity off 25 April fails 7 tests (golden + oracle only -- zero unit tests, since they build synthetic candidates); corrupting the transfer's winner-slug guard fails 6 (the two disposition_cases rows, 3 golden pins, and the newly-registered transfer-count test); flipping the privilege to Ordinary fails 9 (privilege_cases, the disposition SYNTHETIC row, the oracle's count pin, 5 golden pins, and the exhaustive Validate property sweep). --- test/test_golden.ml | 138 +++++++++++++++++++++++++++++++++++++++++++-- test/test_precedence_ef.ml | 124 +++++++++++++++++++++++++++++++++++++--- test/test_rite_ef.ml | 53 ++++++++++++++++- 3 files changed, 301 insertions(+), 14 deletions(-) diff --git a/test/test_golden.ml b/test/test_golden.ml index fdfc01d..7b461bf 100644 --- a/test/test_golden.ml +++ b/test/test_golden.ml @@ -173,7 +173,18 @@ let test_easter_extreme_1598 () = (* 1666: latest Gregorian Easter possible, 25 April -- hand-verified via Gauss's algorithm (a = 13, b = 16, c = 66, ..., h = 29, l = 5, m = 0 -> - 25 April). Same citations as 1598 above. *) + 25 April). Same citations as 1598 above. + + ef-major-litanies task: 25 April = Easter Sunday is RG 80's own FIRST + trigger condition ("si vero eo die occurrit dominica Paschatis... + transferuntur in sequentem feriam III") -- this pin, already the + project's own hand-verified latest-Easter witness, now ALSO carries the + transfer this rule requires: [major-litanies] departs 25 April for + 27 April (Easter+2, "the following Tuesday" -- Precedence_ef.transfer_ + target's own Litanies branch has the full citation). Nothing else about + this day changes: [observed]/[comms]/[in] are exactly as before, + proving the Litanies departure is additive, not disruptive, to the + day Easter itself owns outright. *) let test_easter_extreme_1666 () = check ~msg:"1666-04-24 Holy Saturday: still Passiontide, violet (RG 128)" 1666 4 24 "1666-04-24 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet subject=lord \ @@ -181,7 +192,21 @@ let test_easter_extreme_1666 () = check ~msg:"1666-04-25 latest possible Easter (Gauss-verified): Paschaltide begins, white, class-1" 1666 4 25 "1666-04-25 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white subject=temporal name_la=- comms=[] in=- \ - out=[]" + out=[major-litanies->1666-04-27]"; + (* The transfer's own target, positively pinned (not merely inferred from + [out] above): an ordinary Easter-octave Tuesday, I class, white (RG + 119 -- white "a Missa Vigiliae paschalis usque ad Missam vigiliae + Pentecostis"), with the Litanies as its own sole, privileged + commemoration (RG 109(f); RG 111(a), "I class: none save one + privileged" -- {!PE.admit}'s own [Class1] case). No [transferred_in] + printed: [Liturgical_day.transferred_in] only ever names a candidate + that went on to WIN the day (calendar.ml's own [build_day]), and the + Litanies structurally never can (RG 81, Commemoration_only) -- this is + the SAME asymmetry {!Colitur_kernel.Calendar}'s own [settled_at] fix + (this task) exists to get right on the OTHER side of the ledger. *) + check ~msg:"1666-04-27 Easter+2: the Litanies' own transfer target, commemorated and privileged" 1666 4 27 + "1666-04-27 tuesday season=paschaltide week=1 slug=ef-easter-1-tuesday rank=class-1 colour=white subject=temporal \ + name_la=- comms=[major-litanies:privileged] in=- out=[]" (* 2038: the brief's "late modern case" -- Easter also 25 April (Gauss- verified: a = 5, b = 20, c = 38, ..., h = 29, l = 5, m = 0 -> 25 April, @@ -203,13 +228,24 @@ let test_easter_extreme_1666 () = temporal-cycle-only test regardless (the two concerns -- Easter-extreme arithmetic and a sanctoral occurrence rule -- stay separate pins even though nothing forces that any more). *) +(* ef-major-litanies task: 2038 is this file's own worked example for the + OTHER RG 80 trigger shape checked elsewhere in this task (Easter Monday + = 25 April, e.g. 2011) landing on the identical target (Easter+2) -- + here Easter SUNDAY itself is 25 April, so the "following Tuesday" is + two days later, 27 April, not one; both shapes converge on Easter+2, + never on Easter+1 or Easter+3 (precedence_ef.ml's own [transfer_target] + Litanies branch has the full RG 80 citation and the arithmetic for + both shapes). *) let test_easter_extreme_2038_late_modern () = check ~msg:"2038-04-24 Holy Saturday: still Passiontide, violet (RG 128)" 2038 4 24 "2038-04-24 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet subject=lord \ name_la=Sabbato sancto comms=[] in=- out=[]"; check ~msg:"2038-04-25 late-modern instance of the latest possible Easter (Gauss-verified)" 2038 4 25 "2038-04-25 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white subject=temporal name_la=- comms=[] in=- \ - out=[]" + out=[major-litanies->2038-04-27]"; + check ~msg:"2038-04-27 Easter+2: the Litanies' own transfer target, commemorated and privileged" 2038 4 27 + "2038-04-27 tuesday season=paschaltide week=1 slug=ef-easter-1-tuesday rank=class-1 colour=white subject=temporal \ + name_la=- comms=[major-litanies:privileged] in=- out=[]" (* ------------------------------------------------------------------ *) (* Annunciation transfer, 25 March inside Holy Week (register §4, RG 96 @@ -958,6 +994,91 @@ let test_bvm_saturday_excludes_mt_carmel_2033 () = rank=class-4 colour=white subject=bvm name_la=Officium sanctae Mariae in sabbato comms=[] in=- out=[]" (describe d) +(* ------------------------------------------------------------------ *) +(* RG 80/81/109(f), the Major Litanies (ef-major-litanies task). Every + date below independently derived from the primary text, not read off a + `colitur day` run first and rationalised (this file's own header, rule + 3): Easter's own date for each civil year comes from Computus (already + independently Gauss-cross-checked by this file's own extreme-year + pins), 25 April's Easter-offset and weekday follow arithmetically from + that, and the RULE applied at each offset (RG 80's transfer condition; + RG 91 entries 15/16 for the Sunday-vs-Mark band comparison; RG 111(b)'s + privilege-overrides-ordinary clause) is quoted at + Rite_ef.Precedence_ef's own [disposition]/[admit]/[transfer_target] and + at data/ef/expected-divergences-missalemeum.sexp's own M5/M20 entries, + which independently adjudicate the one substantive rubric question here + (WHO wins the Sunday's single slot) against the missalemeum oracle. *) +(* ------------------------------------------------------------------ *) + +(* The ORDINARY case (~97.7% of years, register's own measurement): 25 + April 2026 is a Saturday (independently checked via `date -d + 2026-04-25 +%A`, this file's own header rule 2), Easter+20 (Easter 2026 + = 5 April, itself independently checked the same way against a + published 2026 Easter date). St Mark (II class, RG 91 entry 16) wins the day outright + over the ordinary Class4 Paschaltide feria (entry 28); the Litanies, + Commemoration_only, is the day's ONLY other candidate and is admitted + uncontested ({!Rite_ef.Precedence_ef.admit}'s [Class2, false] case: one + slot, table order alone, no rival). This is the SAME date and shape + data/ef/expected-divergences-missalemeum.sexp's own [M5] entry + adjudicates against the real missalemeum oracle (verdict colitur, a + naming-convention difference only -- "The Major Litanies" vs + missalemeum's own "Pro rogationibus"). *) +let test_major_litanies_ordinary_2026 () = + check ~msg:"2026-04-25: St Mark observed outright (RG91 e16 > e28); the Major Litanies (RG80/109(f)) ride \ + along uncontested" 2026 4 25 + "2026-04-25 saturday season=paschaltide week=3 slug=mark rank=class-2 colour=red subject=saint name_la=- \ + comms=[major-litanies:privileged] in=- out=[]" + +(* The FOUR Sunday-displacement years in 2005-2050 (this task's own + measurement, register): 25 April is a Sunday, so St Mark (entry 16, II + class) loses to it exactly as any other II-class feast would (entry 15 + < entry 16), becoming an ordinary commemoration CANDIDATE rather than + the observed day -- and then loses the day's own single slot to the + Litanies (RG 111(b): "quæ tamen omittitur si commemoratio privilegiata + facienda sit" -- the ordinary de-festo-II-classis commemoration is + DROPPED once a privileged one is due). [omitted_has] on each date below + confirms Mark is genuinely reported dropped, not silently absent -- + "omitted: admission limit reached", {!Precedence.resolve}'s own generic + reason for a candidate [disposition] admitted to the contest but + [admit] then cut. Two different generic Sunday slugs appear (week 4 in + 2010/2021, week 5 in 2027/2032) because Easter falls on a different + date in each pair -- neither is hand-picked, both are what + [Temporal_ef.sunday_slug] actually computes. *) +let test_major_litanies_displaces_mark_on_ii_class_sunday () = + List.iter + (fun (y, week) -> + let d = fetch y 4 25 in + Alcotest.(check bool) + (Printf.sprintf "%d-04-25: St Mark is in [omitted] (RG111(b)'s privileged-commemoration override), not \ + silently dropped" y) + true (omitted_has d "mark"); + Alcotest.(check string) + (Printf.sprintf "%d-04-25: the Sunday observed (II class); ONLY the Litanies commemorated, Mark \ + displaced (RG111(b))" y) + (Printf.sprintf + "%d-04-25 sunday season=paschaltide week=%d slug=ef-easter-sunday-%d rank=class-2 colour=white \ + subject=temporal name_la=- comms=[major-litanies:privileged] in=- out=[]" + y week week) + (describe d)) + [ (2010, 4); (2021, 4); (2027, 5); (2032, 5) ] + +(* The OTHER RG 80 trigger shape, complementing 1666/2038 above (Easter + Sunday itself = 25 April): here Easter MONDAY = 25 April (Easter = 24 + April 2011, independently Gauss-derivable and already the domain's own + register-cited "Easter Monday" marker, [ef-easter-1-monday]). RG 80's + "following Tuesday" is one day later this time (26 April, not 27) -- + both shapes converge on the SAME offset, Easter+2, never on a fixed + civil-date difference. *) +let test_major_litanies_transfer_2011_easter_monday () = + check ~msg:"2011-04-25 Easter Monday (RG80's SECOND trigger): Litanies depart for the following Tuesday, \ + 26 April" 2011 4 25 + "2011-04-25 monday season=paschaltide week=1 slug=ef-easter-1-monday rank=class-1 colour=white \ + subject=temporal name_la=- comms=[] in=- out=[major-litanies->2011-04-26]"; + check ~msg:"2011-04-26 Easter+2: the Litanies land here, commemorated and privileged, exactly one day after \ + origin (not two, unlike the Easter-Sunday shape)" 2011 4 26 + "2011-04-26 tuesday season=paschaltide week=1 slug=ef-easter-1-tuesday rank=class-1 colour=white \ + subject=temporal name_la=- comms=[major-litanies:privileged] in=- out=[]" + let suite = ( "golden pins (known-tricky years)", [ Alcotest.test_case "Easter extreme: 1598 earliest (22 Mar, Gauss-verified)" `Quick @@ -1026,5 +1147,14 @@ let suite = 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 + `Quick test_bvm_saturday_excludes_mt_carmel_2033; + Alcotest.test_case "RG80/109(f): the Major Litanies, ordinary year, St Mark wins outright (2026-04-25)" + `Quick test_major_litanies_ordinary_2026; + Alcotest.test_case + "RG111(b): the Major Litanies displace St Mark's own commemoration on a II-class Sunday (2010, 2021, \ + 2027, 2032)" + `Quick test_major_litanies_displaces_mark_on_ii_class_sunday; + Alcotest.test_case + "RG80's second trigger: Easter Monday = 25 April, the Litanies transfer to Easter+2 (2011)" `Quick + test_major_litanies_transfer_2011_easter_monday ] ) diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index 6f8b5b5..790dab6 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -555,6 +555,45 @@ let disposition_cases = cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~layer:PE.universal_layer "ef-suppressed-vigil", "Commemorate(Privileged)" ); + (* RG 80 (ef-major-litanies task, NEW) -- {!PE.major_litanies_slug}'s + own citation (precedence_ef.ml, above [privilege_of]) has the full + text. The row immediately above proves "Commemoration_only is + ALWAYS Commemorate" as a GENERAL rule; these three rows prove + [PE.major_litanies_slug] is the ONE deliberate, cited EXCEPTION to + it, and prove the exception is gated on the WINNER's own slug, not + merely on the loser being this particular Commemoration_only + candidate. *) + ( "RG80 first trigger: the Litanies lose to Easter Sunday itself -> \ + Transfer, not Commemorate (25 April IS Easter Sunday this year)", + cand "ef-easter-sunday", + cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer + PE.major_litanies_slug, + "Transfer" ); + ( "RG80 second trigger: the Litanies lose to Easter Monday -> \ + Transfer (25 April IS Easter Monday this year, i.e. Easter itself \ + is 24 April)", + cand "ef-easter-1-monday", + cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer + PE.major_litanies_slug, + "Transfer" ); + (* MUTATION-PROOFING the WINNER-slug guard specifically (not merely + "the Litanies can transfer at all"): losing to a THIRD I-class + temporal winner that is neither of RG 80's two named trigger days + must NOT transfer -- RG 80's own condition is exactly "Easter + Sunday or Easter Monday", not "any I-class day", and 25 April can + structurally never actually coincide with, say, the Nativity in + real data (this row is a deliberately synthetic collision, the + same "proves the guard, not merely the absence of its own bug" + shape {!band}'s own Holy-Family-window row uses) -- if + {!PE.disposition}'s own winner-slug test were ever loosened to + "any Class1 winner", this row would wrongly turn [Transfer] too. *) + ( "SYNTHETIC: the Litanies losing to an UNRELATED I-class day (not \ + Easter Sunday or Monday) is an ordinary Commemoration_only loser \ + -- Commemorate(Privileged) via RG109(f), NOT Transfer", + cand "ef-nativity", + cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer + PE.major_litanies_slug, + "Commemorate(Privileged)" ); (* Totality (SANCTORAL side): the lower ranks the RG 33/RG 95/Task-16 branches never touch still reach the RG 95 "commemorated or omitted" branch's [Commemorate] side, not an unhandled/exceptional case -- RG @@ -865,12 +904,13 @@ let disposition_cases = a plain [Feast]-status [Class1] loser can never reach [Commemorate] at all in this ruleset (RG 95 routes it to [Transfer] instead), so no further row for (b) is added here -- see the task report. Category (f), - "of the Major Rogations, in Mass", has no row at all: no candidate this - codebase can currently construct represents one (see [privilege_of]'s own - comment on (f)) -- the negative row below proves the one slug this engine - DOES compute that could be mistaken for it (the Minor Rogations) is - correctly NOT conflated with it, which is the strongest claim available - without inventing an unfounded slug convention. *) + "of the Major Litanies, in Mass" -- NOW LIVE (ef-major-litanies task): + [PE.major_litanies_slug]'s own data/ef/adjustments.sexp entry is the + real candidate; the positive row below is added alongside the existing + negative one, which still proves the one slug this engine ALSO computes + that could be mistaken for it (the Minor Rogations, RG 87 -- a + different observance RG 109(f) does not name) is correctly NOT + conflated with it. *) let privilege_cases = [ (* (a) RG 109(a) (§4): "of a Sunday". [an_ordinary_sunday] is Class2, not Class1, not within the Nativity octave, not an Ember day, not a @@ -960,6 +1000,26 @@ let privilege_cases = cand "ef-nativity", of_temporal (off (-39)), "Commemorate(Privileged)" ); + (* (f) RG 109(f) (§4; Caput XVI "De Commemorationibus"): "de Litaniis + maioribus, in Missa" -- ef-major-litanies task, NEW. [rank] is + DELIBERATELY [Class4], not [cand]'s own [Class1] default: category + (b) above ("of a I-class day") fires on [rank = Class1] alone and + is checked BEFORE (f) in [privilege_of]'s own branch order, so a + [Class1] row here would reach [Privileged] via (b) regardless of + whether (f) itself is even wired -- proving nothing about (f) + specifically (the exact "test day that is both [X] and [Y] proves + nothing about either" hazard this table's own header warns about, + worked for the FIRST time on this axis rather than Sunday-vs- + I-class). [Class4] (data/ef/adjustments.sexp's own honestly-flagged + placeholder rank, chosen for exactly this reason) rules out (b), + and the slug is neither a Sunday, within the Nativity octave, an + Ember day, nor an Advent/Lent/Passiontide feria -- (f) is this + row's only route to [Privileged], the real proof. *) + ( "(f) the Major Litanies commemoration is privileged (RG109(f))", + cand "ef-nativity", + cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer + PE.major_litanies_slug, + "Commemorate(Privileged)" ); (* Negative, RG 109(f)'s own boundary: the Minor Litanies/Rogations (Monday/Tuesday before Ascension, RG 87 -- [Temporal_ef.temporal] DOES compute these, unlike the Major Litanies RG 109(f) actually @@ -2006,6 +2066,49 @@ let test_transfer_target_does_not_raise_at_domain_ceiling () = true (D.compare target (mk 9999 12 31) > 0) +(* RG 80 (ef-major-litanies task, NEW) -- {!PE.major_litanies_slug}'s own + citation has the full text. [occupant_always_blocking] is deliberately + PATHOLOGICAL (every day reports I-class, forever) -- for every OTHER + candidate this would send [search_from] to its own 400-day structural + ceiling ({!test_transfer_target_terminates_under_pathological_occupant} + above proves exactly that shape). Using it here is the mutation proof + this branch's own comment promises ("running search_from here would be + ACTIVELY WRONG"): if the Litanies' own early-return branch were ever + deleted or bypassed, this test would immediately land somewhere near + [origin + 1000], not Easter+2, and fail loudly -- a MUCH stronger + witness than a cooperative occupant could give, which would pass either + way by accident (Easter+2 is itself never blocking under a realistic + occupant, so a real [Temporal_ef.temporal] could not distinguish "no + search happened" from "a search happened and immediately succeeded"). *) +let test_transfer_target_major_litanies_easter_sunday_shape () = + let easter_2038 = Comp.gregorian_easter 2038 in + Alcotest.(check string) "2038: Easter Sunday IS 25 April (this file's own worked example)" "2038-04-25" + (D.to_iso8601 easter_2038); + let origin = easter_2038 in + let c = + cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer + PE.major_litanies_slug + in + let target = PE.transfer_target c origin occupant_always_blocking in + Alcotest.(check string) "lands on Easter+2 regardless of the pathological occupant (no RG96 search at all)" + (D.to_iso8601 (D.add_days easter_2038 2)) (D.to_iso8601 target); + Alcotest.(check bool) "strictly after origin (rite.mli's own obligation)" true (D.compare target origin > 0) + +let test_transfer_target_major_litanies_easter_monday_shape () = + let easter_2011 = Comp.gregorian_easter 2011 in + Alcotest.(check string) "2011: Easter Sunday is 24 April, so 25 April is Easter Monday" "2011-04-24" + (D.to_iso8601 easter_2011); + let origin = D.add_days easter_2011 1 (* 25 April 2011, Easter Monday *) in + let c = + cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer + PE.major_litanies_slug + in + let target = PE.transfer_target c origin occupant_always_blocking in + Alcotest.(check string) "ALSO lands on Easter+2 (not Easter+3 -- both RG80 trigger shapes converge on the \ + SAME offset), regardless of the pathological occupant" + (D.to_iso8601 (D.add_days easter_2011 2)) (D.to_iso8601 target); + Alcotest.(check bool) "strictly after origin (rite.mli's own obligation)" true (D.compare target origin > 0) + let suite = ( "Precedence_ef", @@ -2098,4 +2201,11 @@ let suite = Alcotest.test_case "transfer_target: terminates and stays forward under a pathological occupant" `Quick test_transfer_target_terminates_under_pathological_occupant; Alcotest.test_case "transfer_target: does not raise probing past the domain ceiling" `Quick - test_transfer_target_does_not_raise_at_domain_ceiling ] ) + test_transfer_target_does_not_raise_at_domain_ceiling; + Alcotest.test_case + "transfer_target: RG80, Easter-Sunday shape -- lands on Easter+2 with NO search, even under a \ + pathological occupant" + `Quick test_transfer_target_major_litanies_easter_sunday_shape; + Alcotest.test_case + "transfer_target: RG80, Easter-Monday shape -- ALSO lands on Easter+2, not Easter+3" `Quick + test_transfer_target_major_litanies_easter_monday_shape ] ) diff --git a/test/test_rite_ef.ml b/test/test_rite_ef.ml index 9a1e6d5..6cd8709 100644 --- a/test/test_rite_ef.ml +++ b/test/test_rite_ef.ml @@ -233,6 +233,21 @@ let sample_years = let rec range a b = if a > b then [] else a :: range (a + 1) b in range 2005 2050 +(* ef-major-litanies task: [major-litanies] (RG 80, Precedence_ef's own + [major_litanies_slug]) is a DELIBERATE, cited exception to this + property, not a bug to catch -- RG 80's own text sends the Major + Litanies to "the following Tuesday" unconditionally in a transfer year, + which is always Easter+2, squarely inside [Easter, Easter+7] + (precedence_ef.ml's own [transfer_target] Litanies branch has the full + citation and the "why not the general RG96 search" argument). Every + OTHER slug this rite ever transfers is still held to the original + property below: RG 96's general walk (and its own named Annunciation + exception) is what this property actually protects, and neither of + those two mechanisms is exempted here -- only this one rite-cited, + hand-verified exception, matched by NAME so a future regression in some + OTHER slug cannot silently hide behind this exemption. *) +let is_major_litanies_slug slug = String.equal slug PE.major_litanies_slug + (* Property 1: no day in the resolved output, across the whole sample, is EVER a transfer's landing point inside [Easter, Easter+7] -- checked two ways. [transferred_out]'s own recorded target is what [transfer_target] @@ -248,7 +263,7 @@ let test_no_transfer_lands_in_easter_octave () = Array.iter (fun (d : (V.season, V.rank) LD.t) -> (match d.LD.transferred_in with - | Some c when in_easter_octave d.LD.date -> + | Some c when in_easter_octave d.LD.date && not (is_major_litanies_slug (slug_of c)) -> violations := (Printf.sprintf "%s transferred_in on %s (Easter+%d)" (slug_of c) (Date.to_iso8601 d.LD.date) (easter_offset d.LD.date)) @@ -256,7 +271,7 @@ let test_no_transfer_lands_in_easter_octave () = | _ -> ()); List.iter (fun (c, target) -> - if in_easter_octave target then + if in_easter_octave target && not (is_major_litanies_slug (slug_of c)) then violations := Printf.sprintf "%s transferred_out from %s to %s (Easter+%d)" (slug_of c) (Date.to_iso8601 d.LD.date) (Date.to_iso8601 target) (easter_offset target) @@ -265,7 +280,37 @@ let test_no_transfer_lands_in_easter_octave () = days) sample_years; Alcotest.(check (list string)) - "no day in [Easter, Easter+7] is ever a transfer's target, 2005-2050" [] (List.rev !violations) + "no day (other than the RG80-cited Major Litanies exception) in [Easter, Easter+7] is ever a transfer's \ + target, 2005-2050" [] + (List.rev !violations) + +(* The positive contrast to the property above: RG 80's own exception DOES + fire, exactly twice in the 2005-2050 sample (register: 194/8417 domain- + wide trigger years; this task's own report has the full count), both + landing on Easter+2 as RG 80's text requires -- proving the exemption + above is not merely masking an absent case. 2011 (Easter Monday = 25 + April, Easter = 24 April) and 2038 (Easter Sunday = 25 April) are this + file's own worked examples (precedence_ef.ml's [major_litanies_slug] + citation). *) +let test_major_litanies_transfers_inside_easter_octave_exactly_when_rg80_requires () = + let layer = real_layer () in + let landings = ref [] in + List.iter + (fun y -> + let days = Cal.year Rite_ef.context layer y in + Array.iter + (fun (d : (V.season, V.rank) LD.t) -> + List.iter + (fun (c, target) -> + if is_major_litanies_slug (slug_of c) then + landings := (Date.to_iso8601 d.LD.date, Date.to_iso8601 target) :: !landings) + d.LD.transferred_out) + days) + sample_years; + Alcotest.(check (list (pair string string))) + "RG80 fires exactly twice in 2005-2050, both landing on Easter+2" + [ ("2011-04-25", "2011-04-26"); ("2038-04-25", "2038-04-27") ] + (List.rev !landings) (* Property 2, and the LIVE case: [PE.transfer_target] called directly, with an origin that genuinely starts the search INSIDE Holy Week -- Holy @@ -381,6 +426,8 @@ let suite = test_transfer_search_does_not_raise_at_domain_ceiling; Alcotest.test_case "no transfer ever lands inside [Easter, Easter+7], 2005-2050" `Quick test_no_transfer_lands_in_easter_octave; + Alcotest.test_case "RG80: the Major Litanies DO transfer inside the octave, exactly twice in 2005-2050" + `Quick test_major_litanies_transfers_inside_easter_octave_exactly_when_rg80_requires; Alcotest.test_case "transfer_target skips the whole Easter octave from inside Holy Week" `Quick test_transfer_target_skips_the_whole_easter_octave; Alcotest.test_case "the search genuinely enters the window (not vacuous)" `Quick -- cgit v1.3