aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/test_calendar.ml90
-rw-r--r--test/test_golden.ml138
-rw-r--r--test/test_oracle.ml125
-rw-r--r--test/test_precedence_ef.ml124
-rw-r--r--test/test_rite_ef.ml53
5 files changed, 497 insertions, 33 deletions
diff --git a/test/test_calendar.ml b/test/test_calendar.ml
index 93cd0bb..22f5f04 100644
--- a/test/test_calendar.ml
+++ b/test/test_calendar.ml
@@ -426,6 +426,90 @@ let test_transfer_guard_records_failure_instead_of_looping () =
in
Alcotest.(check bool) "non-convergence is recorded rather than silently dropped or hung" true stuck
+(* ef-major-litanies task, fix round 1 (F1) -- a regression test for a
+ THIRD settlement channel `build_day`'s own `settled_at` had to learn to
+ recognise: a transferred candidate that reaches its target and is then
+ CAPPED OUT there by the target day's own RG 111 admission-count limit
+ (not `observed`, not surviving as a `commemoration` -- the two channels
+ the first version of this fix checked), rather than settling cleanly.
+ Missing it reproduces the exact original bug ONE LEVEL FURTHER OUT: the
+ origin wrongly reports the transferred candidate as
+ `unconverged_reason`, even though placement genuinely converged.
+
+ Unreachable on real EF data ALONE (the Major Litanies, RG 80, are the
+ only privileged `Commemoration_only` candidate real data carries, and
+ no second one can ever coincide with Easter+2) -- reproduced here the
+ same way the fix-round review did: one synthetic privileged
+ `Commemoration_only` candidate, added directly to the REAL EF layer
+ (not a hand-built synthetic rite -- this bug is about the real Litanies
+ candidate's own real transfer, so the real rite is the honest fixture),
+ on the real Litanies' own real 2011 transfer target (26 April -- Easter
+ 2011 = 24 April, so 25 April is Easter Monday, RG 80's second trigger,
+ landing on Easter+2 = 26 April), with a slug ("aaa-probe") sorting
+ ahead of "major-litanies" so it wins {!Rite_ef.Precedence_ef.admit}'s
+ Class1 "one privileged commemoration only" selection there, capping the
+ Litanies out. *)
+let real_ef_layer_for_transfer_probes =
+ match Colitur_kernel.Layer.load Rite_ef.Vocab_ef.rank_of_sexp "../data/ef/sanctoral.sexp" with
+ | Error e -> failwith ("../data/ef/sanctoral.sexp: " ^ e)
+ | Ok layer -> (
+ match Colitur_kernel.Overlay.load Rite_ef.Vocab_ef.rank_of_sexp "../data/ef/adjustments.sexp" with
+ | Error e -> failwith ("../data/ef/adjustments.sexp: " ^ e)
+ | Ok overlay ->
+ let layer, diagnostics = Colitur_kernel.Overlay.apply layer overlay in
+ if diagnostics <> [] then failwith "unexpected overlay diagnostics loading the real EF layer";
+ layer)
+
+let test_transferred_commemoration_only_capped_out_at_target_settles_cleanly () =
+ let probe_date =
+ match Colitur_kernel.Date_spec.fixed ~month:4 ~day:26 with Ok d -> d | Error e -> failwith e
+ in
+ let probe =
+ { Colitur_kernel.Layer.date = probe_date;
+ cel =
+ Cel.make ~slug:(Sl.of_string_exn "aaa-probe") ~rank:Rite_ef.Vocab_ef.Class1
+ ~status:Cel.Commemoration_only ~colour:Colitur_kernel.Colour.White
+ ~subject:Colitur_kernel.Subject.Saint ~layer:"synthetic-probe" ()
+ }
+ in
+ let augmented_layer = Colitur_kernel.Layer.set real_ef_layer_for_transfer_probes probe in
+ (* Liturgical year "2010" (Advent 2010 -- eve of Advent 2011) covers both
+ 25 and 26 April 2011. *)
+ let year = C.year Rite_ef.context augmented_layer 2010 in
+ let find_date target =
+ match Array.to_list year |> List.find_opt (fun d -> D.compare d.LD.date target = 0) with
+ | Some d -> d
+ | None -> failwith "date not found in resolved year"
+ in
+ let origin = find_date (mk 2011 4 25) and target = find_date (mk 2011 4 26) in
+ let slug_s (c : Rite_ef.Vocab_ef.rank Cel.t) = Sl.to_string c.Cel.slug in
+ Alcotest.(check string) "2011-04-25 is Easter Monday, RG80's second trigger" "ef-easter-1-monday"
+ (slug_s origin.LD.observed);
+ let omitted_s d = List.map (fun (c, r) -> (slug_s c, r)) d.LD.omitted in
+ Alcotest.(check (list (pair string string))) "origin: major-litanies is NOT in [omitted] at all -- it \
+ genuinely, cleanly transferred away, no false 'did not converge'" []
+ (List.filter (fun (s, _) -> s = "major-litanies") (omitted_s origin));
+ let contains_substring s ~needle =
+ let ls = String.length s and ln = String.length needle in
+ let rec at i = i + ln <= ls && (String.sub s i ln = needle || at (i + 1)) in
+ ln = 0 || at 0
+ in
+ Alcotest.(check (list (pair string string))) "origin: no [omitted] entry anywhere claims non-convergence \
+ (the exact regression this test guards against, stated directly rather than only via the slug check \
+ above)" []
+ (List.filter (fun (_, r) -> contains_substring r ~needle:"did not converge") (omitted_s origin));
+ Alcotest.(check (list string)) "origin: [transferred_out] still correctly names major-litanies -> target"
+ [ "major-litanies->2011-04-26" ]
+ (List.map
+ (fun (c, d) -> Printf.sprintf "%s->%s" (slug_s c) (D.to_iso8601 d))
+ origin.LD.transferred_out);
+ Alcotest.(check (list string)) "target: aaa-probe wins the Class1 privileged slot (sorts ahead of \
+ major-litanies at the tied [unclassified] band)" [ "aaa-probe" ]
+ (List.map (fun (c, _) -> slug_s c) target.LD.commemorations);
+ Alcotest.(check bool) "target: major-litanies is capped out into [omitted] there, with the REAL \
+ admission-limit reason, not lost and not mislabelled" true
+ (List.mem ("major-litanies", "omitted: admission limit reached") (List.map (fun (c,r) -> (slug_s c, r)) target.LD.omitted))
+
let suite =
( "Calendar",
[ Alcotest.test_case "year covers every day" `Quick test_year_covers_every_day;
@@ -447,4 +531,8 @@ let suite =
Alcotest.test_case "transfer target outside year is recorded not lost" `Quick
test_transfer_target_outside_year_is_recorded_not_lost;
Alcotest.test_case "transfer guard records failure instead of looping" `Quick
- test_transfer_guard_records_failure_instead_of_looping ] )
+ test_transfer_guard_records_failure_instead_of_looping;
+ Alcotest.test_case
+ "ef-major-litanies fix round 1 (F1): a transferred candidate capped out at its own target settles \
+ cleanly, no false 'did not converge'"
+ `Quick test_transferred_commemoration_only_capped_out_at_target_settles_cleanly ] )
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_oracle.ml b/test/test_oracle.ml
index c0badfe..3a9e499 100644
--- a/test/test_oracle.ml
+++ b/test/test_oracle.ml
@@ -517,25 +517,108 @@ let m3_dates = [ "2027-05-03" ]
sourced from lectio's independently-fixed generator; the divergence no
longer occurs on either 2026-01-28 or 2027-01-28). *)
-(* M5 -- register §6's OWN already-open item, independently confirmed by
- the oracle: "Major Litanies (25 April, RG 80) are not yet computed" --
- missalemeum's commemoration "Pro rogationibus" ("for the Rogations") on
- St Mark's day (25 April, the Major Litanies' fixed date) is exactly the
- missing office. Not a new finding; recorded here so the automated
- comparator does not report it as an unexplained mismatch, and cross-
- referenced from the register so a reader sees both. Verdict missalemeum
- (colitur is missing a real, cited office; register §6 already tracks
- it). Only ONE date below, not two: 25 April 2026 is a Saturday (Mark
- wins outright, colitur's commemoration list is empty where the oracle's
- is not -- a visible presence gap); 25 April 2027 is a Sunday, where the
- Sunday wins on BOTH sides and each side's own single slot goes to a
- DIFFERENT candidate (colitur: Mark himself, admitted under RG 111(b)'s
- "de festo II classis"; missalemeum: the Major Litanies) -- both
- non-empty, same count, so this comparator's deliberately-not-slug-
- identity axes cannot see that year's instance at all (see the task
- report). *)
+(* M5 -- CORRECTED (ef-major-litanies task): the Major Litanies (RG 80,
+ Caput X "De Litaniis maioribus et minoribus") are now built
+ (Rite_ef.Precedence_ef's own [major_litanies_slug]/[disposition]/
+ [privilege_of] RG 109(f) branch/[transfer_target] RG 80 branch,
+ data/ef/adjustments.sexp's matching [Add] entry). This note's own
+ PRIOR text (struck below, kept for the record of what was corrected)
+ claimed missalemeum's single admitted commemoration on 25 April 2027
+ (the Sunday-conflict shape) was "the Major Litanies" -- CHECKED AGAINST
+ THE RAW FIXTURE ROW DIRECTLY while adjudicating this task's own new
+ divergence (test/fixtures/missalemeum-ef-2026-2027.txt, the 2027-04-25
+ line: "...|IV Sunday after Easter|-|St. Mark|Pro rogationibus|1|
+ sancti:04-25:2:r") and found BACKWARDS: missalemeum's own
+ [commemorations] field for that row is "St. Mark", and "Pro
+ rogationibus" (the Litanies) is the one listed in [displaced]. The
+ note had the two swapped -- a genuine source-fidelity slip in this
+ file's own prior authorship, not a re-reading of a primary document,
+ but corrected on the same "read positively, quote it" discipline this
+ project applies to the primary scans. See [M20] below for the fix
+ this correction actually motivates (2027's own now-real divergence,
+ colitur=Litanies vs missalemeum=Mark, adjudicated separately).
+
+ What THIS entry (M5) still covers, unchanged in kind: 25 April 2026 (an
+ ordinary, non-Sunday year -- Mark wins the day outright on both sides,
+ uncontested). Both engines now admit a commemoration there (colitur:
+ [major-litanies]; missalemeum: "Pro rogationibus") -- PRESENCE now
+ agrees (closing the register §6 gap this entry used to track), but the
+ two engines name the SAME real-world observance in different registers:
+ colitur's own English descriptive name (data/ef/adjustments.sexp's own
+ honestly-flagged, not-primary-sourced "The Major Litanies") against
+ missalemeum's Latin-ish "Pro rogationibus" ("for the Rogations") --
+ never going to match as literal strings, and not a rubric dispute at
+ all (RG 80/81 do not prescribe an English title for this observance;
+ this comparator's own [identity_diff] can only ever compare exact
+ strings, the same declared limit [M15]/[M18] already document for
+ temporal-origin candidates, now hit here for a different structural
+ reason -- a real candidate on both sides, just two different
+ vocabularies). Verdict colitur (the identity axis's own declared limit,
+ not an error): {!m5_commemoration_matches} pins WHICH candidate colitur
+ actually admits here, not merely that some [Comm_identity_mismatch]
+ diff exists, the same discipline [M19]'s own identity guard already
+ established. *)
let m5_dates = [ "2026-04-25" ]
+let m5_commemoration_matches (c : colitur_row) =
+ match c.c_commemorations with [ (slug, _, _, _) ] -> String.equal slug "major-litanies" | _ -> false
+
+(* M20 -- ef-major-litanies task, NEW. 25 April 2027: the Sunday-conflict
+ shape (RG 80's own condition -- 25 April is Easter Sunday or Monday --
+ does NOT fire this year, so the Litanies stay put on 25 April itself,
+ which this year happens to be an ordinary II-class Sunday, RG 91 entry
+ 15). St Mark (II class, RG 91 entry 16) also loses to the Sunday. Two
+ losing candidates, ONE slot (RG 111(b)): colitur admits the Litanies,
+ NOT Mark; missalemeum's raw fixture row shows the reverse (verified
+ directly, not inferred -- see [M5]'s own corrected note above for the
+ exact line).
+
+ ADJUDICATED: verdict colitur, RG 109(f) + RG 111(b) (docs/research/
+ rules-register.md §4 "Commemorations", both primary-source-verified
+ word for word, all three documents): RG 109(f), Caput XVI "De
+ Commemorationibus", places "de Litaniis maioribus, in Missa" in the
+ SAME closed list as (a)-(e), with equal grammatical standing -- no
+ textual qualifier narrows its privilege relative to theirs. RG 111(b),
+ same chapter: "in dominicis II classis, una tantum admittitur
+ commemoratio, scilicet de festo II classis, QUÆ TAMEN OMITTITUR SI
+ COMMEMORATIO PRIVILEGIATA FACIENDA SIT" -- the II-class-feast
+ commemoration is DROPPED if a privileged commemoration is due, full
+ stop; the clause names no table-order qualifier ("whichever ranks
+ higher"), only the categorical fact of a privileged commemoration being
+ due. This is the SAME mechanism {!Precedence_ef.admit}'s [Class2, true]
+ branch already implements and this project's OWN prior work already
+ primary-source-verified for this exact clause (precedence_ef.ml's own
+ "Fix, Task 16" comment) -- not new code, not a special case written for
+ the Litanies; St Mark simply meets the same fate any other ordinary
+ Class2 commemoration meets when a privileged one is also due that
+ Sunday (RG 16(a)'s Transfiguration/Sixtus shape is the nearest existing
+ witness, though there the privileged side wins the DAY, not merely the
+ commemoration slot).
+
+ HONESTLY FLAGGED, not overclaimed: this is the FIRST real, independent
+ (non-synthetic) data point this codebase has for "an ordinary Class2
+ feast candidate and a privileged non-feast commemoration candidate,
+ both losing to the identical Sunday" -- every other witness for
+ [admit]'s Class2-Sunday privilege-override branch in this suite's own
+ test_precedence_ef.ml is hand-built (real slugs/ranks, but a
+ constructed collision, not one the calendar itself produces). Read
+ plainly, RG 111(b)'s text supports colitur's outcome; missalemeum's own
+ divergence here is consistent with this project's ALREADY-DOCUMENTED
+ pattern of RG 108-111 gaps in that oracle (M1: RG 33's Sunday omission
+ not implemented; M8: RG 109(a)+RG 111(a) not implemented for an impeded
+ I-class Sunday; M10: RG 109(e) inconsistently applied) -- plausibly one
+ more instance of the same generator not modelling RG 109(f)'s privilege
+ for this one rare, single-date observance, not evidence the general
+ RG 111(b) mechanism itself is mis-read. Recorded as an adjudicated
+ verdict, not a certainty: if a future primary-source pass finds
+ textual grounds narrowing RG 109(f)'s privilege specifically (e.g. a
+ clause this task's own scan reading did not surface), this entry is
+ the one to revisit first. *)
+let m20_dates = [ "2027-04-25" ]
+
+let m20_commemoration_matches (c : colitur_row) =
+ match c.c_commemorations with [ (slug, _, _, _) ] -> String.equal slug "major-litanies" | _ -> false
+
(* M6 -- REMOVED, ef-rebootstrap (2026-08-12): see data/ef/expected-
divergences-missalemeum.sexp's own "FIVE MORE REMOVED" note. Eusebius
Confessor (14 Aug) is now present in data/ef/sanctoral.sexp
@@ -852,7 +935,8 @@ let layer_m_reason (c : colitur_row) (_o : oracle_row) diffs =
observes is temporal-origin, same root cause as M1/M2 above. *)
else if List.mem c.c_date m3_dates && subset diffs [ Colour_f; Observed_identity_unresolved ] then
Some "M3"
- else if List.mem c.c_date m5_dates && diffs = [ Comm_presence ] then Some "M5"
+ else if List.mem c.c_date m5_dates && diffs = [ Comm_identity_mismatch ] && m5_commemoration_matches c then
+ Some "M5"
else if List.mem c.c_date m8_dates && diffs = [ Comm_presence ] then Some "M8"
else if List.mem c.c_date m10_dates && diffs = [ Comm_presence ] then Some "M10"
else if List.mem c.c_date m11_dates && diffs = [ Comm_presence ] then Some "M11"
@@ -880,6 +964,11 @@ let layer_m_reason (c : colitur_row) (_o : oracle_row) diffs =
else if diffs = [ Observed_identity_unresolved ] then Some "M18"
else if List.mem c.c_date m19_dates && diffs = [ Comm_presence ] && m19_commemoration_matches c then
Some "M19"
+ else if
+ List.mem c.c_date m20_dates
+ && subset diffs [ Comm_identity_mismatch; Observed_identity_unresolved ]
+ && m20_commemoration_matches c
+ then Some "M20"
else None
(* ---------------------------------------------------------------------- *)
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