aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/test_differential.ml34
-rw-r--r--test/test_golden.ml173
-rw-r--r--test/test_oracle.ml167
-rw-r--r--test/test_precedence_ef.ml354
-rw-r--r--test/test_temporal_ef.ml155
5 files changed, 759 insertions, 124 deletions
diff --git a/test/test_differential.ml b/test/test_differential.ml
index 89e6873..94adb2c 100644
--- a/test/test_differential.ml
+++ b/test/test_differential.ml
@@ -471,6 +471,35 @@ let nativity_octave_day_slugs =
note for the full account of why that was wrong). *)
let joseph_annunciation_collision_dates_2005_2050 = [ "2008-04-01"; "2035-04-03"; "2046-04-03" ]
+(* C15 -- ef-rg112-rg110 task: SPLIT OUT of C1's own blanket 6-13 January
+ window, not a new kind of divergence -- the same discipline C13 (now
+ closed, see the header above) already established for this exact date,
+ and the SAME check the task brief asked for explicitly ("if C1 now
+ absorbs a different observed celebration, split it out"). Before this
+ task, colitur had no Holy Family office at all: on these seven dates (13
+ January, the one civil day it falls on a Sunday in the 2005-2050 window
+ -- 2008, 2013, 2019, 2030, 2036, 2041, 2047), colitur's own generic
+ Sunday fallback happened to lose outright to the fixed Commemoration of
+ the Baptism of the Lord (RG16(a)'s existing "festum Domini beats an
+ ordinary Sunday" mechanism, subject Lord already present in the
+ REBOOTSTRAPPED data), producing the SAME slug lectio's own tridentine-
+ calendar.ini shows for that date ("commemoration-of-the-baptism-of-the-
+ lord") -- only [Season] differed (RG 72-73 vs lectio's own 6 January
+ boundary), squarely inside C1's own shape. Now that Holy Family is built
+ (RG 17(b)) and correctly outranks the fixed Baptism (RG 91 entry 14,
+ "primum mobilia, deinde fixa"; RG 112(a) excludes the Baptism as a
+ commemoration too, both this task's own precedence_ef.ml changes),
+ colitur's own slug on these seven dates changes to
+ "ef-time-after-epiphany-sunday-1" (Holy Family's own, unchanged from the
+ ordinary-Sunday key it always carried, temporal_ef.ml's own comment on
+ why) -- a GENUINE identity divergence against lectio, which has no Holy
+ Family at all and still shows the fixed Baptism observed there. Gated on
+ the literal 7-date list AND colitur's own slug (the same guard C1/C6/C8/
+ C14 already apply, fix round 1's own finding 1: a predicate must pin
+ WHICH celebration it is about, not only the date). *)
+let holy_family_baptism_collision_dates_2005_2050 =
+ [ "2008-01-13"; "2013-01-13"; "2019-01-13"; "2030-01-13"; "2036-01-13"; "2041-01-13"; "2047-01-13" ]
+
(* [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
[None] if nothing here explains it (a genuine, uncovered failure). *)
@@ -516,6 +545,11 @@ let layer_c_reason (l : row) (c : row) diffs =
every predicate -- geometry, not this pin. *)
&& String.equal c.slug "joseph-spouse-of-the-bl-virgin-mary"
then Some "C14"
+ else if
+ List.mem l.date holy_family_baptism_collision_dates_2005_2050
+ && subset diffs [ Season; Slug_f ]
+ && String.equal c.slug "ef-time-after-epiphany-sunday-1"
+ then Some "C15"
else None
(* ---------------------------------------------------------------------- *)
diff --git a/test/test_golden.ml b/test/test_golden.ml
index 5292427..1724375 100644
--- a/test/test_golden.ml
+++ b/test/test_golden.ml
@@ -93,9 +93,19 @@ let fetch y m d = Cal.day Rite_ef.context real_ef_layer (mk y m d)
let slug_s (c : V.rank Cel.t) = Slug.to_string c.Cel.slug
let rank_s (c : V.rank Cel.t) = V.rank_to_string c.Cel.rank
let colour_s (c : V.rank Cel.t) = Colour.to_string c.Cel.colour
+let subject_s (c : V.rank Cel.t) = Colitur_kernel.Subject.to_string c.Cel.subject
let privilege_s = function Prec.Privileged -> "privileged" | Prec.Ordinary -> "ordinary"
-(* The five fields {!Colitur_kernel.Liturgical_day.t} promises are never
+(* fix round 1 (coordinator finding 4): [subject] added. Two of the three
+ original Holy Family pins (2026-01-11, 2024-01-07) were byte-identical
+ before and after temporal_ef.ml's own fix -- reverting the feature failed
+ only the 2030-01-13 collision pin, because [describe] omitted the ONE
+ field the change touches on an ordinary (non-collision) year. Vacuity
+ flavour #1 ("an assertion already true before the code under test ran"),
+ not #8 ("promoted from actual output") -- the OTHER nine fields' values
+ were independently sourced and remain so; only this tenth was missing.
+
+ The five fields {!Colitur_kernel.Liturgical_day.t} promises are never
silently lost, folded into one comparable line -- see the file header for
why one string, not five checks. *)
let describe (day : (V.season, V.rank) LD.t) =
@@ -110,11 +120,12 @@ let describe (day : (V.season, V.rank) LD.t) =
List.map (fun (c, d) -> Printf.sprintf "%s->%s" (slug_s c) (Date.to_iso8601 d)) day.LD.transferred_out
|> List.sort compare |> String.concat ","
in
- Printf.sprintf "%s %s season=%s week=%s slug=%s rank=%s colour=%s comms=[%s] in=%s out=[%s]"
+ Printf.sprintf "%s %s season=%s week=%s slug=%s rank=%s colour=%s subject=%s comms=[%s] in=%s out=[%s]"
(Date.to_iso8601 day.LD.date)
(Date.weekday_to_string t.Temporal.weekday)
(V.season_to_string t.Temporal.season)
- week (slug_s day.LD.observed) (rank_s day.LD.observed) (colour_s day.LD.observed) comms tin tout
+ week (slug_s day.LD.observed) (rank_s day.LD.observed) (colour_s day.LD.observed)
+ (subject_s day.LD.observed) comms tin tout
let check ~msg y m d expected = Alcotest.(check string) msg expected (describe (fetch y m d))
@@ -139,11 +150,11 @@ let omitted_has (day : (V.season, V.rank) LD.t) slug =
place, not merely that 22 March is white. *)
let test_easter_extreme_1598 () =
check ~msg:"1598-03-21 Holy Saturday: still Passiontide, violet (RG 128)" 1598 3 21
- "1598-03-21 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet \
+ "1598-03-21 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet subject=temporal \
comms=[] in=- out=[]";
check ~msg:"1598-03-22 earliest possible Easter (Gauss-verified): Paschaltide begins, white, class-1" 1598 3
22
- "1598-03-22 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white comms=[] in=- \
+ "1598-03-22 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white subject=temporal comms=[] in=- \
out=[]"
(* 1666: latest Gregorian Easter possible, 25 April -- hand-verified via
@@ -151,11 +162,11 @@ let test_easter_extreme_1598 () =
25 April). Same citations as 1598 above. *)
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 \
+ "1666-04-24 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet subject=temporal \
comms=[] in=- out=[]";
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 comms=[] in=- \
+ "1666-04-25 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white subject=temporal comms=[] in=- \
out=[]"
(* 2038: the brief's "late modern case" -- Easter also 25 April (Gauss-
@@ -180,10 +191,10 @@ let test_easter_extreme_1666 () =
though nothing forces that any more). *)
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 \
+ "2038-04-24 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet subject=temporal \
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 comms=[] in=- \
+ "2038-04-25 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white subject=temporal comms=[] in=- \
out=[]"
(* ------------------------------------------------------------------ *)
@@ -204,7 +215,7 @@ let test_annunciation_transfer_inside_holy_week_2016 () =
actually reaches a live case" for exactly this reason. *)
check ~msg:"2016-03-25 Good Friday: Triduum outranks the Annunciation (RG91 entry 2 > 11), no commemoration"
2016 3 25
- "2016-03-25 friday season=passiontide week=2 slug=ef-passiontide-2-friday rank=class-1 colour=violet \
+ "2016-03-25 friday season=passiontide week=2 slug=ef-passiontide-2-friday rank=class-1 colour=violet subject=temporal \
comms=[] in=- out=[annunciation-of-the-blessed-virgin-mary->2016-04-04]";
(* The general RG 96 walk from 26 March would still be inside the Triduum,
the Easter octave (all I class, entry 2/10) -- carrying the feast PAST
@@ -212,7 +223,7 @@ let test_annunciation_transfer_inside_holy_week_2016 () =
after Low Sunday = Easter + 8 = 27 Mar + 8 = 4 April. *)
check ~msg:"2016-04-04 Easter+8 (Monday after Low Sunday): RG96 Attamen(a) sedes propria fires" 2016 4 4
"2016-04-04 monday season=paschaltide week=2 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \
- colour=white comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]"
+ colour=white subject=saint comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]"
(* ------------------------------------------------------------------ *)
(* 2008: the double transfer (register §4, "cases worth adding"). Easter =
@@ -227,7 +238,7 @@ let test_annunciation_joseph_double_transfer_2008 () =
I-class feria, band 7, above 11) -- impeded, Transfer-disposed. *)
check ~msg:"2008-03-19 Wednesday of Holy Week outranks Joseph (RG91 entry 7 > 11), no commemoration" 2008 3
19
- "2008-03-19 wednesday season=passiontide week=2 slug=ef-passiontide-2-wednesday rank=class-1 colour=violet \
+ "2008-03-19 wednesday season=passiontide week=2 slug=ef-passiontide-2-wednesday rank=class-1 colour=violet subject=temporal \
comms=[] in=- out=[joseph-spouse-of-the-bl-virgin-mary->2008-04-01]";
(* Both Joseph and the Annunciation are band 11 (register: neither is
Immaculate Conception/Assumption, band 4; both are ordinary universal
@@ -255,7 +266,7 @@ let test_annunciation_joseph_double_transfer_2008 () =
check ~msg:"2008-03-31 Easter+8: Annunciation's named sedes propria (RG96 Attamen a) claims the day first"
2008 3 31
"2008-03-31 monday season=paschaltide week=2 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \
- colour=white comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]";
+ colour=white subject=saint comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]";
(* Joseph's own general RG96 walk, finding 31 March already claimed
(occupant reports Class1 there, still blocking per [is_blocking]),
continues one more day to 1 April -- RG 97-98's "in order": the second
@@ -264,7 +275,7 @@ let test_annunciation_joseph_double_transfer_2008 () =
check ~msg:"2008-04-01: Joseph's RG96 walk continues past the now-claimed 31 March (RG97-98 'in order')" 2008
4 1
"2008-04-01 tuesday season=paschaltide week=2 slug=joseph-spouse-of-the-bl-virgin-mary rank=class-1 \
- colour=white comms=[] in=joseph-spouse-of-the-bl-virgin-mary out=[]"
+ colour=white subject=saint comms=[] in=joseph-spouse-of-the-bl-virgin-mary out=[]"
(* ------------------------------------------------------------------ *)
(* RG 96 Attamen (a) is CONDITIONAL ("quando est transferendum post
@@ -288,15 +299,15 @@ let test_annunciation_exception_not_triggered_general_walk_suffices () =
check ~msg:"2057-03-26: general RG96 walk suffices (target before Easter, 22 Apr) -- no Attamen(a) exception"
2057 3 26
"2057-03-26 monday season=lent week=3 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \
- colour=white comms=[ef-lent-3-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]";
+ colour=white subject=saint comms=[ef-lent-3-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]";
check ~msg:"2007-03-26: general RG96 walk suffices (target before Easter, 8 Apr) -- no Attamen(a) exception"
2007 3 26
"2007-03-26 monday season=passiontide week=1 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \
- colour=white comms=[ef-passiontide-1-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]";
+ colour=white subject=saint comms=[ef-passiontide-1-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]";
check ~msg:"2012-03-26: general RG96 walk suffices (target before Easter, 8 Apr) -- no Attamen(a) exception"
2012 3 26
"2012-03-26 monday season=passiontide week=1 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \
- colour=white comms=[ef-passiontide-1-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]"
+ colour=white subject=saint comms=[ef-passiontide-1-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]"
(* After Easter: 2016 (already pinned above, Easter+8 = 4 April) and 2024
(Easter 31 March, Easter+8 = 8 April). In both, the general walk would
@@ -312,7 +323,7 @@ let test_annunciation_exception_triggered_when_walk_would_cross_easter () =
(RG26)"
2024 4 8
"2024-04-08 monday season=paschaltide week=2 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \
- colour=white comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]"
+ colour=white subject=saint comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]"
(* 2016's own pin (test_annunciation_transfer_inside_holy_week_2016) is the
other half of this pair -- not repeated here to avoid asserting the same
date twice. *)
@@ -330,19 +341,19 @@ let test_annunciation_exception_triggered_when_walk_would_cross_easter () =
let test_precious_blood_transfer_2011 () =
check ~msg:"2011-07-01: Sacred Heart (RG91 entry 3) outranks the fixed Precious Blood (entry 11) outright"
2011 7 1
- "2011-07-01 friday season=time-after-pentecost week=2 slug=ef-sacred-heart rank=class-1 colour=white \
+ "2011-07-01 friday season=time-after-pentecost week=2 slug=ef-sacred-heart rank=class-1 colour=white subject=temporal \
comms=[] in=- out=[precious-blood-of-our-lord-jesus-christ->2011-07-04]";
check ~msg:"2011-07-02: the RG96 walk's first blocked day (Visitation, II class) -- Precious Blood not here"
2011 7 2
"2011-07-02 saturday season=time-after-pentecost week=2 slug=visitation-of-the-blessed-virgin-mary \
- rank=class-2 colour=white comms=[processus-and-martinian:ordinary] in=- out=[]";
+ rank=class-2 colour=white subject=saint comms=[processus-and-martinian:ordinary] in=- out=[]";
check ~msg:"2011-07-03: the RG96 walk's second blocked day (an ordinary Sunday, II class) -- still not here"
2011 7 3
"2011-07-03 sunday season=time-after-pentecost week=3 slug=ef-time-after-pentecost-sunday-3 rank=class-2 \
- colour=green comms=[] in=- out=[]";
+ colour=green subject=temporal comms=[] in=- out=[]";
check ~msg:"2011-07-04: the first non-I/II-class day (RG96) -- the Precious Blood lands here" 2011 7 4
"2011-07-04 monday season=time-after-pentecost week=3 slug=precious-blood-of-our-lord-jesus-christ \
- rank=class-1 colour=red comms=[] in=precious-blood-of-our-lord-jesus-christ out=[]"
+ rank=class-1 colour=red subject=lord comms=[] in=precious-blood-of-our-lord-jesus-christ out=[]"
(* ------------------------------------------------------------------ *)
(* All Souls falling on a Sunday (register §4, RG96 Attamen (b), primary-
@@ -357,11 +368,11 @@ let test_all_souls_on_a_sunday_2025 () =
check ~msg:"2025-11-02: All Souls yields to the occurring Sunday (RG91 entry 8), no commemoration (RG94)" 2025
11 2
"2025-11-02 sunday season=time-after-pentecost week=21 slug=ef-time-after-pentecost-sunday-21 rank=class-2 \
- colour=green comms=[] in=- out=[commemoration-of-all-souls->2025-11-03]";
+ colour=green subject=temporal comms=[] in=- out=[commemoration-of-all-souls->2025-11-03]";
check ~msg:"2025-11-03: All Souls transfers to the following Monday, its sedes propria (RG96 Attamen b)" 2025
11 3
"2025-11-03 monday season=time-after-pentecost week=21 slug=commemoration-of-all-souls rank=class-1 \
- colour=black comms=[] in=commemoration-of-all-souls out=[]"
+ colour=black subject=saint comms=[] in=commemoration-of-all-souls out=[]"
(* ------------------------------------------------------------------ *)
(* Christmas falling on a Sunday (RG91 entry 1: Nativity, Easter, Pentecost
@@ -375,7 +386,7 @@ let test_all_souls_on_a_sunday_2025 () =
let test_christmas_on_a_sunday_2022 () =
check ~msg:"2022-12-25 falling on a Sunday: the Nativity (RG91 entry 1) takes the day outright, no contest"
2022 12 25
- "2022-12-25 sunday season=christmastide week=- slug=ef-nativity rank=class-1 colour=white comms=[] in=- \
+ "2022-12-25 sunday season=christmastide week=- slug=ef-nativity rank=class-1 colour=white subject=temporal comms=[] in=- \
out=[]"
(* ------------------------------------------------------------------ *)
@@ -388,18 +399,18 @@ let test_christmas_on_a_sunday_2022 () =
confirmed via `date -d`. *)
let test_holy_thursday_is_white_2026 () =
check ~msg:"2026-04-01 (Wednesday of Holy Week): ordinary Passiontide violet (RG128)" 2026 4 1
- "2026-04-01 wednesday season=passiontide week=2 slug=ef-passiontide-2-wednesday rank=class-1 colour=violet \
+ "2026-04-01 wednesday season=passiontide week=2 slug=ef-passiontide-2-wednesday rank=class-1 colour=violet subject=temporal \
comms=[] in=- out=[]";
check
~msg:
"2026-04-02 Holy Thursday: white (RG128(b)'s named exception + RG122's affirmative statement), not \
Passiontide's violet"
2026 4 2
- "2026-04-02 thursday season=passiontide week=2 slug=ef-passiontide-2-thursday rank=class-1 colour=white \
+ "2026-04-02 thursday season=passiontide week=2 slug=ef-passiontide-2-thursday rank=class-1 colour=white subject=temporal \
comms=[] in=- out=[]";
check ~msg:"2026-04-03 Good Friday: back to violet (RG128) -- Thursday's white is a one-day exception" 2026 4
3
- "2026-04-03 friday season=passiontide week=2 slug=ef-passiontide-2-friday rank=class-1 colour=violet \
+ "2026-04-03 friday season=passiontide week=2 slug=ef-passiontide-2-friday rank=class-1 colour=violet subject=temporal \
comms=[] in=- out=[]"
(* ------------------------------------------------------------------ *)
@@ -419,14 +430,14 @@ let test_ember_ferias_commemorated_when_impeded () =
makes it PRIVILEGED, not merely eligible. *)
check ~msg:"1900-12-21: Advent Ember Friday, impeded, MUST be commemorated (RG24) and is privileged (RG109e)"
1900 12 21
- "1900-12-21 friday season=advent week=3 slug=thomas rank=class-2 colour=red \
+ "1900-12-21 friday season=advent week=3 slug=thomas rank=class-2 colour=red subject=saint \
comms=[ef-advent-ember-fri:privileged] in=- out=[]";
(* 1902-02-22: Lent Ember Saturday (RG91 entry 18, II class) impeded by
the Chair of St Peter (II class, universal, entry 16). Same RG24/
RG109(e) mandate. *)
check ~msg:"1902-02-22: Lent Ember Saturday, impeded, MUST be commemorated (RG24) and is privileged (RG109e)"
1902 2 22
- "1902-02-22 saturday season=lent week=1 slug=chair-of-st-peter rank=class-2 colour=white \
+ "1902-02-22 saturday season=lent week=1 slug=chair-of-st-peter rank=class-2 colour=white subject=saint \
comms=[ef-lent-ember-sat:privileged] in=- out=[]"
let test_iv_class_ferias_never_commemorated () =
@@ -443,7 +454,7 @@ let test_iv_class_ferias_never_commemorated () =
true
(omitted_has d1 "ef-time-after-pentecost-8-wednesday");
Alcotest.(check string) "2026-07-22: Mary Magdalene observed, no commemoration (RG26)"
- "2026-07-22 wednesday season=time-after-pentecost week=8 slug=mary-magdalene rank=class-3 colour=white \
+ "2026-07-22 wednesday season=time-after-pentecost week=8 slug=mary-magdalene rank=class-3 colour=white subject=saint \
comms=[] in=- out=[]"
(describe d1);
let d2 = fetch 2026 8 10 in
@@ -451,7 +462,7 @@ let test_iv_class_ferias_never_commemorated () =
true
(omitted_has d2 "ef-time-after-pentecost-11-monday");
Alcotest.(check string) "2026-08-10: St Lawrence observed, no commemoration (RG26)"
- "2026-08-10 monday season=time-after-pentecost week=11 slug=lawrence rank=class-2 colour=red comms=[] in=- \
+ "2026-08-10 monday season=time-after-pentecost week=11 slug=lawrence rank=class-2 colour=red subject=saint comms=[] in=- \
out=[]"
(describe d2)
@@ -476,7 +487,7 @@ let test_ii_class_sunday_admits_only_ii_class_commemoration () =
(omitted_has d2009 "romanus");
Alcotest.(check string) "2009-08-09: the Sunday observed, no commemoration at all (RG111(b) rank floor)"
"2009-08-09 sunday season=time-after-pentecost week=10 slug=ef-time-after-pentecost-sunday-10 rank=class-2 \
- colour=green comms=[] in=- out=[]"
+ colour=green subject=temporal comms=[] in=- out=[]"
(describe d2009);
let d2026 = fetch 2026 8 9 in
Alcotest.(check bool) "2026-08-09: romanus (Class3) is offered and excluded, proving RG111(b) actually fired"
@@ -484,7 +495,7 @@ let test_ii_class_sunday_admits_only_ii_class_commemoration () =
(omitted_has d2026 "romanus");
Alcotest.(check string) "2026-08-09: the Sunday observed, no commemoration at all (RG111(b) rank floor)"
"2026-08-09 sunday season=time-after-pentecost week=11 slug=ef-time-after-pentecost-sunday-11 rank=class-2 \
- colour=green comms=[] in=- out=[]"
+ colour=green subject=temporal comms=[] in=- out=[]"
(describe d2026)
(* ------------------------------------------------------------------ *)
@@ -549,9 +560,80 @@ let test_rg16a_lord_feast_no_sunday_commemoration_2028 () =
et privilegiis\" keeps the day a dominica II classis for RG111(b), so Sixtus (Class3) has no standing \
either, oracle-confirmed"
"2028-08-06 sunday season=time-after-pentecost week=9 slug=transfiguration-of-our-lord rank=class-2 \
- colour=white comms=[] in=- out=[]"
+ colour=white subject=lord comms=[] in=- out=[]"
(describe d)
+(* Holy Family (RG 17(b), scan-verified, docs/research/rules-register.md
+ §4/§6.0): "festum S. Familiae Iesu, Mariae, Ioseph, celebrandum dominica
+ prima post Epiphaniam" -- first Sunday after Epiphany, II class, EVERY
+ year, taking the Sunday's own place "cum omnibus iuribus et privilegiis"
+ (RG 17(b)'s own closing clause, the same formula RG 16(a) uses above).
+ Three shapes, each independently verified against the primary text
+ BEFORE being typed in here, per this file's own header rule:
+
+ 1. AN ORDINARY YEAR (2026, 11 January): no fixed-date competitor in the
+ 7-13 January window besides St Hyginus (11 Jan, Class3,
+ Commemoration_only per data/ef/sanctoral.sexp) -- excluded from the
+ day's single Sunday slot outright by RG 111(b)'s own rank floor ("de
+ festo II classis"), the SAME mechanism test_precedence_ef.ml's own
+ "II-class Sunday admits only a de-festo-II-classis commemoration" pin
+ already proves for a different date -- so comms=[] is not itself new
+ ground, only Holy Family's own slug/rank/colour/season are.
+ `date -d 2026-01-11 +%A` = Sunday, independently confirmed.
+ 2. THE 13-JANUARY COLLISION (2030, the latest possible date -- Epiphany
+ itself a Sunday, 6 January): the fixed Commemoration of the Baptism
+ of the Lord (13 Jan, Class2, subject Lord, data/ef/sanctoral.sexp)
+ would otherwise contest the day directly, not merely lose a
+ commemoration slot -- RG 91 entry 14's own "primum mobilia, deinde
+ fixa" (precedence_ef.ml's own [entry_14_movable_band]) is what keeps
+ Holy Family observed, and RG 112(a) -- directly confirmed by the Holy
+ Family Mass propers' own more specific 13-January rubric, both
+ photographic scans, word for word: "Si festum S. Familiae occurrerit
+ die 13 ianuarii, Missa dicitur de festo S. Familiae, sine
+ commemoratione Baptismatis D.N.I.C., et sine commemoratione
+ dominicae" -- is what keeps comms=[] rather than admitting the
+ Baptism as an ordinary Class2 commemoration. `date -d 2030-01-13
+ +%A` = Sunday, independently confirmed; 2030 is one of the seven
+ years in the 2005-2050 window this collision occurs (register §6.0's
+ own list, test_differential.ml's own C15).
+ 3. THE EARLIEST POSSIBLE DATE (2024, 7 January -- Epiphany a Saturday):
+ the OTHER end of RG 17(b)'s own 7-13 January range, proving the
+ window's own boundary, not only its middle and its latest instance.
+ No fixed-date competitor exists this early in the window either.
+ `date -d 2024-01-07 +%A` = Sunday, independently confirmed.
+
+ All three: season=christmastide (RG 72-73's own 1-13 January boundary,
+ unaffected by Holy Family), week=- (Christmastide carries no numbered
+ weeks at all, [T.week_origin]'s own [None] case -- unaffected by which
+ Sunday of it this is), slug=ef-time-after-epiphany-sunday-1 (unchanged
+ from the plain Sunday key this date always carried -- temporal_ef.ml's
+ own comment on why RG 17(b) does not need a new lectionary key), rank
+ =class-2, colour=white -- AND, fix round 1 (coordinator finding 4),
+ subject=lord: the ONE field that actually distinguishes Holy Family from
+ an ordinary, unnamed Sunday (every other field above was already
+ identical before temporal_ef.ml's own fix -- [describe] originally
+ omitted [subject], so two of these three pins would have stayed green
+ even with the feature fully reverted; see [describe]'s own comment). *)
+let test_holy_family_ordinary_year_2026 () =
+ check ~msg:"2026-01-11: Holy Family, ordinary year -- Hyginus (Class3, Commemoration_only) has no \
+ standing for the day's II-class-only Sunday slot"
+ 2026 1 11
+ "2026-01-11 sunday season=christmastide week=- slug=ef-time-after-epiphany-sunday-1 rank=class-2 \
+ colour=white subject=lord comms=[] in=- out=[]"
+
+let test_holy_family_excludes_baptism_2030 () =
+ check ~msg:"2030-01-13: Holy Family on the latest possible date -- RG91 e14 \"primum mobilia\" keeps it \
+ observed over the fixed Baptism, RG112(a) excludes the Baptism entirely, not merely demotes it"
+ 2030 1 13
+ "2030-01-13 sunday season=christmastide week=- slug=ef-time-after-epiphany-sunday-1 rank=class-2 \
+ colour=white subject=lord comms=[] in=- out=[]"
+
+let test_holy_family_earliest_possible_date_2024 () =
+ check ~msg:"2024-01-07: Holy Family on the earliest possible date (Epiphany a Saturday)"
+ 2024 1 7
+ "2024-01-07 sunday season=christmastide week=- slug=ef-time-after-epiphany-sunday-1 rank=class-2 \
+ colour=white subject=lord comms=[] in=- out=[]"
+
(* 2025-02-02, the other real instance of the SAME pattern -- not a
contrasting second direction, per this task's fix round 1 (CRITICAL
finding, item 1, reverted; see register §6.0 for the full account).
@@ -583,7 +665,7 @@ let test_purification_on_a_sunday_2025 () =
commemoration of the Sunday, oracle-confirmed"
2025 2 2
"2025-02-02 sunday season=time-after-epiphany week=4 slug=purification-of-the-blessed-virgin-mary \
- rank=class-2 colour=white comms=[] in=- out=[]"
+ rank=class-2 colour=white subject=lord comms=[] in=- out=[]"
(* ------------------------------------------------------------------ *)
(* RG 91 entry 24 ("Festa III classis, in calendario Ecclesiae universae
@@ -642,15 +724,15 @@ let test_iii_class_feast_wins_class4_feria () =
true
(omitted_has d1 "ef-easter-2-wednesday");
Alcotest.(check string) "2008-04-02: Francis of Paola (III class, RG91 entry 24) outranks the class-4 feria"
- "2008-04-02 wednesday season=paschaltide week=2 slug=francis-of-paola rank=class-3 colour=white comms=[] \
+ "2008-04-02 wednesday season=paschaltide week=2 slug=francis-of-paola rank=class-3 colour=white subject=saint comms=[] \
in=- out=[]"
(describe d1);
check ~msg:"2008-04-04: Isidore of Seville (III class) outranks the class-4 Paschaltide feria, same shape" 2008
4 4
- "2008-04-04 friday season=paschaltide week=2 slug=isidore-of-seville rank=class-3 colour=white comms=[] in=- \
+ "2008-04-04 friday season=paschaltide week=2 slug=isidore-of-seville rank=class-3 colour=white subject=saint comms=[] in=- \
out=[]";
check ~msg:"2008-04-05: Vincent Ferrer (III class) outranks the class-4 Paschaltide feria, same shape" 2008 4 5
- "2008-04-05 saturday season=paschaltide week=2 slug=vincent-ferrer rank=class-3 colour=white comms=[] in=- \
+ "2008-04-05 saturday season=paschaltide week=2 slug=vincent-ferrer rank=class-3 colour=white subject=saint comms=[] in=- \
out=[]";
let d4 = fetch 2038 3 6 in
Alcotest.(check bool)
@@ -660,15 +742,15 @@ let test_iii_class_feast_wins_class4_feria () =
Alcotest.(check string)
"2038-03-06: Sts Perpetua and Felicity (III class, RG91 entry 24) outrank the class-4 feria; RED per their \
own martyrs' colour, not the season's violet"
- "2038-03-06 saturday season=septuagesima week=2 slug=sts-felicitas-perpetua rank=class-3 colour=red comms=[] \
+ "2038-03-06 saturday season=septuagesima week=2 slug=sts-felicitas-perpetua rank=class-3 colour=red subject=saint comms=[] \
in=- out=[]"
(describe d4);
check ~msg:"2038-03-08: John of God (III class) outranks the class-4 Septuagesima feria, same shape" 2038 3 8
- "2038-03-08 monday season=septuagesima week=3 slug=john-of-god rank=class-3 colour=white comms=[] in=- \
+ "2038-03-08 monday season=septuagesima week=3 slug=john-of-god rank=class-3 colour=white subject=saint comms=[] in=- \
out=[]";
check ~msg:"2038-03-09: Frances of Rome (III class) outranks the class-4 Septuagesima feria, same shape" 2038 3
9
- "2038-03-09 tuesday season=septuagesima week=3 slug=frances-rome rank=class-3 colour=white comms=[] in=- \
+ "2038-03-09 tuesday season=septuagesima week=3 slug=frances-rome rank=class-3 colour=white subject=saint comms=[] in=- \
out=[]"
let suite =
@@ -704,6 +786,13 @@ let suite =
Alcotest.test_case
"RG16(a), the Purification (oracle-ruled): observed outright, the Sunday not commemorated (2025-02-02)"
`Quick test_purification_on_a_sunday_2025;
+ Alcotest.test_case "Holy Family (RG17(b)), ordinary year (2026-01-11)" `Quick
+ test_holy_family_ordinary_year_2026;
+ Alcotest.test_case
+ "Holy Family excludes the Baptism entirely on the latest possible date (RG91 e14 + RG112(a), 2030-01-13)"
+ `Quick test_holy_family_excludes_baptism_2030;
+ Alcotest.test_case "Holy Family, earliest possible date (2024-01-07)" `Quick
+ test_holy_family_earliest_possible_date_2024;
Alcotest.test_case
"ef-rebootstrap: RG91 entry24 (III-class feast) outranks entry28 (class-4 feria) -- the six dates \
deferred since 2038's own Easter-extreme pin (2008-04-02/04/05, 2038-03-06/08/09)"
diff --git a/test/test_oracle.ml b/test/test_oracle.ml
index a86fd76..35d4501 100644
--- a/test/test_oracle.ml
+++ b/test/test_oracle.ml
@@ -3,6 +3,20 @@
layer 4 of the design spec's five (colitur CLAUDE.md "Validation" section;
layer 3, the lectio differential, is test_differential.ml, already green).
+ *** WIP, 2026-08-13, branch ef-rg112-rg110: this file is EXPECTED RED
+ right now. [Observed_identity_mismatch]/[Observed_identity_unresolved]
+ (below) are new -- this layer used to compare the observed day's rank and
+ colour and stop there, never whether it is actually the RIGHT day (Holy
+ Family, 11 January 2026, is rank 2/white on both sides purely by
+ coincidence -- an ordinary unnamed Sunday and Holy Family share both).
+ Landing the strengthened comparator FIRST, red, before touching any
+ production code, is the point: it is the regression net the rest of this
+ task's fix commits are checked against, not a change bundled in alongside
+ the fix. The very next commits make it green again (data/ef/expected-
+ divergences-missalemeum.sexp's own M-series entries below, and this
+ file's own [layer_m_reason] widening) -- see that file and
+ docs/research/rules-register.md for the running account.
+
*** WHY THIS LAYER EXISTS, AND WHAT IT CAN DO THAT THE LECTIO DIFFERENTIAL
CANNOT (test_differential.ml's own header comment, limit 1) ***
@@ -280,6 +294,15 @@ type colitur_row = {
c_date : string;
c_rank : int;
c_colour : char;
+ c_observed_slug : string;
+ c_observed_name : string option;
+ (** colitur's own resolved English name for the OBSERVED celebration --
+ [None] for a TEMPORAL-origin day (an ordinary Sunday, a feria, a
+ movable named feast: {!Rite_ef.Temporal_ef}'s own [build] never
+ sets [names], the SAME gap this file's header already documents
+ for commemorations, see [Comm_identity_unresolved]), the same
+ [Names.find ... en] read [c_commemorations] below already uses,
+ applied to [LD.observed] instead of a commemoration candidate. *)
c_commemorations : (string * int * Cel.status * string option) list;
(** slug, rank, status, English name *)
}
@@ -321,6 +344,8 @@ let colitur_rows_2026_2027 () =
{ c_date = Date.to_iso8601 day.LD.date;
c_rank = rank_to_int cel.Cel.rank;
c_colour = colour_to_char cel.Cel.colour;
+ c_observed_slug = Slug.to_string cel.Cel.slug;
+ c_observed_name = Names.find cel.Cel.names en;
c_commemorations = commemorations
}
:: !rows
@@ -335,7 +360,15 @@ let colitur_rows_2026_2027 () =
(* header for the mapping and its exact limits. *)
(* ---------------------------------------------------------------------- *)
-type field = Rank | Colour_f | Comm_presence | Comm_count | Comm_identity_mismatch | Comm_identity_unresolved
+type field =
+ | Rank
+ | Colour_f
+ | Comm_presence
+ | Comm_count
+ | Comm_identity_mismatch
+ | Comm_identity_unresolved
+ | Observed_identity_mismatch
+ | Observed_identity_unresolved
let field_name = function
| Rank -> "rank"
@@ -344,6 +377,40 @@ let field_name = function
| Comm_count -> "commemoration-count"
| Comm_identity_mismatch -> "commemoration-identity-mismatch"
| Comm_identity_unresolved -> "commemoration-identity-unresolved"
+ | Observed_identity_mismatch -> "observed-identity-mismatch"
+ | Observed_identity_unresolved -> "observed-identity-unresolved"
+
+(* CORRECTING the gap this file's own header names (added under this task,
+ branch ef-rg112-rg110): the axes above all compare the day's OBSERVED
+ celebration's rank and colour ([Rank]/[Colour_f]) and the admitted
+ COMMEMORATIONS' identity ([Comm_identity_*]) -- nothing until now compared
+ the OBSERVED celebration's own identity, WHICH day is actually being kept,
+ only what class/colour it happens to carry. Holy Family (11 January 2026:
+ oracle title "The Holy Family: Jesus, Mary & Joseph", colitur's own
+ observed slug the plain "ef-time-after-epiphany-sunday-1", BOTH rank 2 and
+ colour white on both sides) is exactly the shape this axis exists to
+ catch -- rank and colour already agreed by coincidence (an ordinary,
+ unnamed II-class Sunday and Holy Family share both), so nothing above
+ ever saw a difference. Same mapping and the same honesty about its limit
+ as [identity_diff] just above: colitur identifies a day by SLUG,
+ missalemeum by an English TITLE string, and the bridge is
+ {!Celebration.t}.names, read directly off [LD.observed] by
+ [colitur_rows_2026_2027] into [c_observed_name] -- resolvable for a
+ SANCTORAL-origin observed day (a saint's feast winning the day outright),
+ NEVER for a TEMPORAL-origin one (an ordinary Sunday, a feria, a movable
+ named feast -- {!Rite_ef.Temporal_ef}'s own [build] never sets [names]).
+ [Observed_identity_unresolved] is that TEMPORAL-origin case: counted and
+ reported, never silently skipped, the same discipline
+ [Comm_identity_unresolved] already established and the brief demands
+ again here ("a day whose observed identity cannot be resolved must be a
+ counted, allow-listed outcome, never a silent skip"). Unlike commemoration
+ identity, this axis needs no presence/count gate first -- there is always
+ exactly one observed day on each side, no list-length ambiguity to
+ resolve before a title comparison is even meaningful. *)
+let observed_identity_diff (c : colitur_row) (o : oracle_row) =
+ match c.c_observed_name with
+ | None -> Some Observed_identity_unresolved
+ | Some name -> if String.equal name o.o_title then None else Some Observed_identity_mismatch
(* Identity (Task B): only even attempted once presence AND count already
agree (both non-empty, same length) -- see this file's own header,
@@ -378,7 +445,8 @@ let diff_fields (c : colitur_row) (o : oracle_row) =
(if c_has && o_has && List.length c.c_commemorations <> List.length o.o_commemorations then
Some Comm_count
else None);
- identity_diff c o
+ identity_diff c o;
+ observed_identity_diff c o
]
(* ---------------------------------------------------------------------- *)
@@ -658,25 +726,98 @@ let m16_dates = [ "2026-03-27" ]
entry needed at all -- removed, not re-adjudicated to a different
verdict, since there is no longer a divergence to name. *)
+(* M18 -- ef-rg112-rg110 task: the gap this file's own header now names under
+ "COMMEMORATION IDENTITY", restated for the OBSERVED axis. colitur's own
+ observed celebration carries an English name ({!Celebration.t}.names)
+ ONLY for a SANCTORAL-origin day (a saint's feast winning outright); a
+ TEMPORAL-origin one -- an ordinary Sunday, a feria, a movable named feast
+ including Holy Family itself -- never does ({!Rite_ef.Temporal_ef}'s own
+ [build] has no [en "..."] string anywhere in it, this file's own header
+ again). CORRECTED, fix round 1 (coordinator finding 8) -- the precise
+ breakdown: of this window's 730 days, 331 are RESOLVED (330 matching, 1
+ mismatched, [M13]); 399 are UNRESOLVED, split between this entry's own
+ 373 (the axis's sole disagreement) and 26 absorbed inside [M1]/[M2]/[M3]/
+ [M16]'s own widened subsets, where it fires ALONGSIDE their pre-existing
+ citation. The BLIND SPOT this axis exists to close, stated precisely: a
+ TEMPORAL-origin observed day silently replaced by a DIFFERENT
+ temporal-origin observed day of the SAME RANK AND COLOUR -- exactly the
+ shape that hid Holy Family from every layer before this task. This is
+ the OVERWHELMING majority of days in any calendar (saints' feasts are
+ common, but far from every day), so this is a single STRUCTURAL
+ predicate -- [Observed_identity_unresolved] alone, nothing else
+ disagreeing -- not a literal date list the way every other entry in this
+ file is: at this population size, a list would be exactly the
+ "range/pattern that could silently widen" this file's own header
+ explicitly avoids elsewhere, for the OPPOSITE reason a range is normally
+ risky here -- the predicate itself (colitur's own name is [None]) is the
+ precise, falsifiable evidence, the same shape [M2]'s own title-substring
+ predicate already uses instead of a date list, just keyed on
+ presence-of-a-name rather than a title string. NOT a rubric dispute and
+ NOT a data gap either engine is wrong about -- a LIMIT of this comparator
+ itself, honestly counted rather than silently passed, per the brief's own
+ explicit instruction ("a day whose observed identity cannot be resolved
+ must be a counted, allow-listed outcome, never a silent skip"), the SAME
+ discipline [M15] already established for an unresolvable COMMEMORATION,
+ applied here for the first time to the OBSERVED day. The count pin below
+ ([test_layer_m_counts_match_citations]) is what stands guard against this
+ population growing (or shrinking) silently -- it cannot say WHICH day
+ moved or why, only that the total did. Fixing this at the root (giving
+ every temporal-cycle candidate an English name) is a data/lectionary-
+ bootstrap task (Plan 4), not something this comparator can do for itself
+ -- register §6 tracks it, the same open item
+ [M15]'s own note already points to. *)
let layer_m_reason (c : colitur_row) (o : oracle_row) diffs =
if diffs = [] then None
- else if List.mem c.c_date m1_dates && subset diffs [ Rank; Colour_f; Comm_presence ] then Some "M1"
- else if contains_substring o.o_title ~needle:bvm_saturday_title && diffs = [ Colour_f ] then Some "M2"
- else if List.mem c.c_date m3_dates && diffs = [ Colour_f ] then Some "M3"
+ (* M1's own subset widened (this task): colitur's observed day on both
+ dates is TEMPORAL-origin (the Sunday itself, RG33's own point -- the
+ vigil should never have displaced it), so it now ALSO carries
+ [Observed_identity_unresolved] -- the same root cause the file's other
+ widened entries below share, restated for this one's own shape. *)
+ else if List.mem c.c_date m1_dates
+ && subset diffs [ Rank; Colour_f; Comm_presence; Observed_identity_unresolved ]
+ then Some "M1"
+ (* M2's own subset widened (this task): every BVM-Saturday date's observed
+ celebration is temporal-origin (the ordinary Saturday feria colitur
+ computes, not the distinct Office of the BVM missalemeum's title
+ names -- M2's own gap), so [Observed_identity_unresolved] now fires
+ alongside [Colour_f] where it used to fire alone, AND alone on its own
+ on every Paschaltide occurrence M2's own comment already says never
+ reached this predicate before (colour already agrees there; only the
+ new axis has anything left to say). *)
+ else if contains_substring o.o_title ~needle:bvm_saturday_title
+ && subset diffs [ Colour_f; Observed_identity_unresolved ]
+ then Some "M2"
+ (* M3's own subset widened (this task): the Rogation Monday feria colitur
+ 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 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"
else if List.mem c.c_date m12_dates && diffs = [ Comm_count ] then Some "M12"
- (* M13's own subset widened (Task B): the same underlying gap (the Seven
- Sorrows commemoration is never constructed at all) now ALSO surfaces
- as [Comm_identity_unresolved] on this date, since colitur's own
- admitted commemoration there is temporal-origin (no English name) --
- one root cause, one id, both axes it touches. *)
- else if List.mem c.c_date m13_dates && subset diffs [ Rank; Colour_f; Comm_identity_unresolved ] then
- Some "M13"
+ (* M13's own subset widened AGAIN (this task, following the SAME pattern
+ Task B's own widening comment below records): colitur's observed
+ celebration here is Joseph, SANCTORAL-origin, so its name IS
+ resolvable -- and disagrees with missalemeum's own title outright
+ ([Observed_identity_mismatch], not [_unresolved]), the SAME underlying
+ gap (the Seven Sorrows commemoration is never constructed, so colitur
+ has no candidate to observe OR commemorate in its place) now visible
+ on a THIRD axis. *)
+ else if List.mem c.c_date m13_dates
+ && subset diffs [ Rank; Colour_f; Comm_identity_unresolved; Observed_identity_mismatch ]
+ then Some "M13"
else if List.mem c.c_date m15_dates && diffs = [ Comm_identity_unresolved ] then Some "M15"
- else if List.mem c.c_date m16_dates && diffs = [ Comm_identity_mismatch ] then Some "M16"
+ (* M16's own subset widened (this task): colitur's observed celebration
+ here is the temporal Passiontide feria (John Damascene is only a
+ COMMEMORATION on this Feast-status-losing day -- band picks the
+ temporal candidate outright at Class3 rank, register's own account of
+ this date), so [Observed_identity_unresolved] now fires alongside the
+ pre-existing [Comm_identity_mismatch]. *)
+ else if List.mem c.c_date m16_dates
+ && subset diffs [ Comm_identity_mismatch; Observed_identity_unresolved ]
+ then Some "M16"
+ else if diffs = [ Observed_identity_unresolved ] then Some "M18"
else None
(* ---------------------------------------------------------------------- *)
diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml
index 2935e40..4ee2fd8 100644
--- a/test/test_precedence_ef.ml
+++ b/test/test_precedence_ef.ml
@@ -48,110 +48,162 @@ let easter = Comp.gregorian_easter 2026
let off n = D.add_days easter n
(* (description, date, candidate, expected RG 91 entry). *)
+(* CORRECTED, fix round 1 (coordinator finding 1): every expected [int]
+ below is now the real RG 91 entry number TIMES TEN (entry 1 -> 10, entry
+ 28 -> 280), matching [PE.band]'s own rescale -- see precedence_ef.ml's
+ own [entry_14_fixed_band] comment for why the bare 1..28 scale was a real
+ ordering bug, not a style choice. The description strings still say the
+ bare entry number ("14 Feast of the Lord...") -- that names the RG 91
+ ROW, not the band integer, and is unaffected by the scale. *)
let cases =
[ (* Entry 1 -- RG 91 entry 1 (§4): Nativity, Easter Sunday, Pentecost Sunday. *)
- ("1 Nativity", mk 2026 12 25, cand "ef-nativity", 1);
- ("1 Easter Sunday", off 0, cand "ef-easter-sunday", 1);
- ("1 Pentecost Sunday", off 49, cand "ef-pentecost", 1);
+ ("1 Nativity", mk 2026 12 25, cand "ef-nativity", 10);
+ ("1 Easter Sunday", off 0, cand "ef-easter-sunday", 10);
+ ("1 Pentecost Sunday", off 49, cand "ef-pentecost", 10);
(* Entry 2 -- RG 91 entry 2 (§4): Sacred Triduum. Thu-Sat of Holy Week,
NOT entry 7 (which stops at Wednesday -- see entry 7 below). *)
- ("2 Holy Thursday", off (-3), cand "ef-holy-thursday", 2);
- ("2 Good Friday", off (-2), cand "ef-good-friday", 2);
- ("2 Holy Saturday", off (-1), cand "ef-holy-saturday", 2);
+ ("2 Holy Thursday", off (-3), cand "ef-holy-thursday", 20);
+ ("2 Good Friday", off (-2), cand "ef-good-friday", 20);
+ ("2 Holy Saturday", off (-1), cand "ef-holy-saturday", 20);
(* Entry 3 -- RG 91 entry 3 (§4). *)
- ("3 Epiphany", mk 2026 1 6, cand "ef-epiphany", 3);
- ("3 Ascension", off 39, cand "ef-ascension", 3);
- ("3 Trinity", off 56, cand "ef-trinity", 3);
- ("3 Corpus Christi", off 60, cand "ef-corpus-christi", 3);
- ("3 Sacred Heart", off 68, cand "ef-sacred-heart", 3);
- ("3 Christ the King", T.christ_the_king 2026, cand "ef-christ-the-king", 3);
+ ("3 Epiphany", mk 2026 1 6, cand "ef-epiphany", 30);
+ ("3 Ascension", off 39, cand "ef-ascension", 30);
+ ("3 Trinity", off 56, cand "ef-trinity", 30);
+ ("3 Corpus Christi", off 60, cand "ef-corpus-christi", 30);
+ ("3 Sacred Heart", off 68, cand "ef-sacred-heart", 30);
+ ("3 Christ the King", T.christ_the_king 2026, cand "ef-christ-the-king", 30);
(* Entry 4 -- RG 91 entry 4 (§4). Sanctoral-origin: neither feast is part
of temporal_ef's movable cycle. *)
( "4 Immaculate Conception", mk 2026 12 8,
cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer
"ef-immaculate-conception",
- 4 );
- ("4 Assumption", mk 2026 8 15, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-assumption", 4);
+ 40 );
+ ("4 Assumption", mk 2026 8 15, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-assumption", 40);
(* Entry 5 -- RG 91 entry 5 (§4). *)
- ("5 Nativity Vigil", mk 2026 12 24, cand "ef-nativity-vigil", 5);
- ("5 Octave day (Circumcision)", mk 2026 1 1, cand "ef-circumcision", 5);
+ ("5 Nativity Vigil", mk 2026 12 24, cand "ef-nativity-vigil", 50);
+ ("5 Octave day (Circumcision)", mk 2026 1 1, cand "ef-circumcision", 50);
(* Entry 6 -- RG 91 entry 6 (§4). *)
- ("6 Advent Sunday", T.advent_start 2026, cand "ef-advent-sunday-1", 6);
- ("6 Lent Sunday", off (-42), cand "ef-lent-sunday-1", 6);
- ("6 Passion Sunday (I Passiontide)", off (-14), cand "ef-passion-sunday", 6);
- ("6 Palm Sunday (II Passiontide)", off (-7), cand "ef-palm-sunday", 6);
- ("6 Low Sunday", off 7, cand "ef-low-sunday", 6);
+ ("6 Advent Sunday", T.advent_start 2026, cand "ef-advent-sunday-1", 60);
+ ("6 Lent Sunday", off (-42), cand "ef-lent-sunday-1", 60);
+ ("6 Passion Sunday (I Passiontide)", off (-14), cand "ef-passion-sunday", 60);
+ ("6 Palm Sunday (II Passiontide)", off (-7), cand "ef-palm-sunday", 60);
+ ("6 Low Sunday", off 7, cand "ef-low-sunday", 60);
(* Entry 7 -- RG 91 entry 7 (§4): Ash Wednesday and Mon/Tue/Wed of Holy
Week ONLY -- Thu-Sat are entry 2 above, not this entry. *)
- ("7 Ash Wednesday", off (-46), cand "ef-ash-wednesday", 7);
- ("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 7);
- ("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 7);
- ("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 7);
+ ("7 Ash Wednesday", off (-46), cand "ef-ash-wednesday", 70);
+ ("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 70);
+ ("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 70);
+ ("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 70);
(* Entry 8 -- RG 91 entry 8 (§4). 2 Nov 2026 is a Monday (verified
independently below the table), so this row is the plain case. The
register's own qualifying case -- "yields to an occurring Sunday" --
gets its own row and its own end-to-end test after this table (2 Nov
2025 is a real Sunday). *)
- ("8 All Souls (non-Sunday)", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 8);
+ ("8 All Souls (non-Sunday)", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 80);
(* Entry 8's qualifier: "(yields to an occurring Sunday)". 2 Nov 2025 is
- a Sunday, so this must NOT be 8 -- it must lose to entry 15 (16 =
- entry 15's own value + 1, the exact value precedence_ef.ml documents
- and justifies at entry 8's branch). The end-to-end resolve-level
- proof that the Sunday actually wins the day is
+ a Sunday, so this must NOT be 80 -- it must lose to entry 15 (151 =
+ entry 15's own value, 150, + 1, the exact value precedence_ef.ml
+ documents and justifies at entry 8's branch). The end-to-end
+ resolve-level proof that the Sunday actually wins the day is
[test_all_souls_yields_to_sunday] below; this row pins the specific
integer [band] returns. *)
- ("8 All Souls (yields to a Sunday, 2 Nov 2025)", mk 2025 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 16);
+ ("8 All Souls (yields to a Sunday, 2 Nov 2025)", mk 2025 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 151);
(* Entry 9 -- RG 91 entry 9 (§4). *)
- ("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 9);
+ ("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 90);
(* Entry 10 -- RG 91 entry 10 (§4): both range boundaries, to guard the
off-by-one an inclusive Easter-offset window invites. *)
- ("10 Easter octave, day+1", off 1, cand "ef-easter-1-mon", 10);
- ("10 Easter octave, day+6", off 6, cand "ef-easter-1-sat", 10);
- ("10 Pentecost octave, day+50", off 50, cand "ef-pentecost-1-mon", 10);
- ("10 Pentecost octave, day+55", off 55, cand "ef-pentecost-1-sat", 10);
+ ("10 Easter octave, day+1", off 1, cand "ef-easter-1-mon", 100);
+ ("10 Easter octave, day+6", off 6, cand "ef-easter-1-sat", 100);
+ ("10 Pentecost octave, day+50", off 50, cand "ef-pentecost-1-mon", 100);
+ ("10 Pentecost octave, day+55", off 55, cand "ef-pentecost-1-sat", 100);
(* Entry 11 -- RG 91 entry 11 (§4). *)
( "11 Universal I-class feast", mk 2026 6 29,
cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-ss-peter-paul",
- 11 );
+ 110 );
(* Entry 12 -- RG 91 entry 12 (§4). The one non-base-layer case the brief
asks for explicitly: same date/rank/subject as 11, only the layer
differs, so this row isolates the layer test as the deciding factor. *)
( "12 Proper I-class feast (non-base layer)", mk 2026 6 29,
cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:"diocese-warsaw" "ef-local-patron",
- 12 );
+ 120 );
(* Entry 13 -- RG 91 entry 13 (§4). *)
( "13 Indult I-class feast", mk 2026 6 29,
cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:(PE.indult_prefix ^ "local-grant")
"ef-indult-feast-1",
- 13 );
+ 130 );
+ (* fix round 1 (coordinator finding 1), NEW: a synthetic I-CLASS
+ candidate landing INSIDE Holy Family's own 7-13 January window --
+ the exact counter-example that found the original [-14] sentinel's
+ inversion (Holy Family used to beat every I-class entry outright,
+ not merely the fixed Baptism it was built to beat). Must still
+ outrank the movable-14 entry below (110 < entry_14_movable_band),
+ proving the fix, not merely the absence of the old bug's OWN
+ witness (13 January). Not live against the shipped universal data
+ (register §6.2) -- this is exactly the kind of I-class PROPER/
+ INDULT feast an overlay could add, entries 11-13's own [not
+ is_temporal] tests notwithstanding. *)
+ ( "11 SYNTHETIC: a universal I-class feast inside Holy Family's own window \
+ (fix round 1, coordinator finding 1) must still outrank the movable half",
+ mk 2026 1 11,
+ cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer
+ "ef-synthetic-class1-probe",
+ 110 );
(* Entry 14 -- RG 91 entry 14 (§4), deliberately UNQUALIFIED (contrast
entry 16, which explicitly says "not of the Lord"). *)
( "14 Feast of the Lord, II class", mk 2026 7 1,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"ef-precious-blood",
- 14 );
+ PE.entry_14_fixed_band );
(* Entry 14, non-base layer: unlike 11-13/16/19/20/23/24, entry 14 draws
- no universal/proper/indult line at all, so this must STILL be 14, not
- 19 -- the exact restriction review finding 2 flagged and this row
- exists to keep from silently coming back. *)
+ no universal/proper/indult line at all, so this must STILL be
+ [PE.entry_14_fixed_band], not 19 -- the exact restriction review
+ finding 2 flagged and this row exists to keep from silently coming
+ back. *)
( "14 Feast of the Lord, II class (non-base layer)", mk 2026 7 2,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:"diocese-warsaw"
"ef-local-feast-of-the-lord",
- 14 );
+ PE.entry_14_fixed_band );
+ (* Entry 14, MOVABLE half -- RG 91 entry 14's own text (§4, scan-verified):
+ "Festa Domini II classis, PRIMUM MOBILIA, DEINDE FIXA" -- movable
+ FIRST, then fixed. Holy Family (RG 17(b), temporal_ef.ml's own
+ [holy_family_sunday]) is the one movable Class2 Lord feast this
+ codebase builds -- sourced from [Temporal_ef.temporal]'s own real
+ output ([of_temporal], the same review-finding-3 discipline entry
+ 18's own Ember row above uses), not a hand-typed slug, so a rename
+ or a dropped [~subject:Lord] on temporal_ef.ml's own side fails this
+ row too, not only test_temporal_ef.ml's. Must be a value STRICTLY
+ BETTER than the fixed half ([PE.entry_14_fixed_band] immediately
+ above, not merely different) -- the two are structurally disjoint by
+ [is_temporal] alone so they can never literally collide on any one
+ candidate, but {!compare_by}'s own kernel-level tie-break
+ (Slug.compare, deliberately NOT rubric-authoritative -- the exact
+ anti-pattern RG 113's own fix already corrected once, this file's
+ own header) is what would decide between them if they returned the
+ SAME band on the one date they can coincide (13 January, when it is
+ itself Holy Family's date) -- "commemoration-of-the-baptism-of-the-
+ lord" sorts before "ef-time-after-epiphany-sunday-1" alphabetically,
+ backwards from RG 91's own stated order. CORRECTED, fix round 1
+ (coordinator finding 1): [PE.entry_14_movable_band] used to be a
+ negative sentinel, which beat every I-class entry too, not merely
+ entry 14's own fixed half -- see the SYNTHETIC I-class probe row
+ above, and [PE.entry_14_movable_band]'s own comment in
+ precedence_ef.ml, for the fix and the counter-example that found it. *)
+ ( "14 Feast of the Lord, II class, MOVABLE half (Holy Family, RG 17(b))",
+ T.holy_family_sunday 2026, of_temporal (T.holy_family_sunday 2026), PE.entry_14_movable_band );
(* Entry 15 -- RG 91 entry 15 (§4): an ordinary Sunday not named at entry 6
-- Septuagesima is II class (RG 11-12 names only Advent/Lent/
Passiontide/Easter/Low/Pentecost as I class). *)
- ("15 II-class Sunday (Septuagesima)", off (-63), cand ~rank:V.Class2 "ef-septuagesima-sunday", 15);
+ ("15 II-class Sunday (Septuagesima)", off (-63), cand ~rank:V.Class2 "ef-septuagesima-sunday", 150);
(* Entry 16 -- RG 91 entry 16 (§4). *)
( "16 Universal II-class feast, not of the Lord", mk 2026 1 20,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:PE.universal_layer
"ef-some-saint",
- 16 );
+ 160 );
(* Entry 17 -- RG 91 entry 17 (§4): days WITHIN the Nativity octave (26-28
Dec are Stephen/John/Innocents -- sanctoral, not this entry; 1 Jan is
entry 5's Octave DAY, not this entry either). *)
- ("17 Nativity octave, 29 Dec", mk 2026 12 29, cand ~rank:V.Class2 "ef-nativity-octave-day-5", 17);
- ("17 Nativity octave, 31 Dec", mk 2026 12 31, cand ~rank:V.Class2 "ef-nativity-octave-day-7", 17);
+ ("17 Nativity octave, 29 Dec", mk 2026 12 29, cand ~rank:V.Class2 "ef-nativity-octave-day-5", 170);
+ ("17 Nativity octave, 31 Dec", mk 2026 12 31, cand ~rank:V.Class2 "ef-nativity-octave-day-7", 170);
(* Entry 18 -- RG 91 entry 18 (§4): Advent 17-23 Dec ferias AND the
Ember days of Advent/Lent/September share this one entry. The second
row is deliberately a Lent date (season Lent, NOT Advent) to prove the
@@ -159,21 +211,21 @@ let cases =
to fall in the Dec 17-23 window -- the exact trap the brief warns
about, worked the other way round: this Ember day must NOT be
mistaken for an ordinary entry-22 Lent feria either. *)
- ("18 Advent 17-23 Dec feria", mk 2026 12 21, cand ~rank:V.Class2 "ef-advent-4-mon", 18);
+ ("18 Advent 17-23 Dec feria", mk 2026 12 21, cand ~rank:V.Class2 "ef-advent-4-mon", 180);
(* Sourced from Temporal_ef.temporal's own output (see [of_temporal])
rather than a hand-typed "ef-lent-ember-wed" -- closes review finding
3's coupling concern for the Ember prefixes specifically. *)
- ("18 Lent Ember Wednesday (from Temporal_ef.temporal)", off (-39), of_temporal (off (-39)), 18);
+ ("18 Lent Ember Wednesday (from Temporal_ef.temporal)", off (-39), of_temporal (off (-39)), 180);
(* Entry 19 -- RG 91 entry 19 (§4). *)
( "19 Proper II-class feast", mk 2026 1 20,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:"diocese-warsaw"
"ef-local-saint-2",
- 19 );
+ 190 );
(* Entry 20 -- RG 91 entry 20 (§4). *)
( "20 Indult II-class feast", mk 2026 1 20,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint
~layer:(PE.indult_prefix ^ "local-grant-2") "ef-indult-feast-2",
- 20 );
+ 200 );
(* Entry 21 -- RG 91 entry 21 (§4, RG 28-34). Two rows: the Ascension
Vigil is the one II-class vigil temporal_ef already produces today
(temporal-origin); the Assumption Vigil stands in for the
@@ -183,22 +235,22 @@ let cases =
(* Sourced from Temporal_ef.temporal's own output (see [of_temporal])
rather than a hand-typed "ef-ascension-vigil" -- closes review finding
3's coupling concern for [vigil_suffix]. *)
- ("21 Ascension Vigil (from Temporal_ef.temporal)", off 38, of_temporal (off 38), 21);
+ ("21 Ascension Vigil (from Temporal_ef.temporal)", off 38, of_temporal (off 38), 210);
( "21 Assumption Vigil (sanctoral-origin)", mk 2026 8 14,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-assumption-vigil",
- 21 );
+ 210 );
(* Also review finding 3 / "worth doing": a UNIVERSAL-layer Class2 vigil
whose subject is the Lord must still be 21, not 14 -- pins entry 14's
[not is_vigil] guard even after finding 2 dropped its layer test. *)
( "21 Universal II-class vigil of the Lord", mk 2026 6 23,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"ef-precious-blood-vigil",
- 21 );
+ 210 );
(* Entry 22 -- RG 91 entry 22 (§4) (corrected: ends at Palm Sunday, not
Passion Sunday). Both a Lent and a Passiontide feria, clear of Ash
Wednesday, Holy Week and the Ember days. *)
- ("22 Lent feria", off (-41), cand ~rank:V.Class3 "ef-lent-1-mon", 22);
- ("22 Passiontide feria", off (-12), cand ~rank:V.Class3 "ef-passiontide-1-tue", 22);
+ ("22 Lent feria", off (-41), cand ~rank:V.Class3 "ef-lent-1-mon", 220);
+ ("22 Passiontide feria", off (-12), cand ~rank:V.Class3 "ef-passiontide-1-tue", 220);
(* Entry 23 -- RG 91 entry 23 (§4). NOTE the table's own order here is the
REVERSE of 11/12 and 14/16/19/20 above: entry 23 (particular
calendars) is numbered BELOW entry 24 (universal), so a proper
@@ -206,45 +258,45 @@ let cases =
register states it, not "corrected" to match the other classes. *)
( "23 Proper III-class feast (non-base layer)", mk 2026 6 30,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-saint-3",
- 23 );
+ 230 );
(* Entry 24 -- RG 91 entry 24 (§4). *)
( "24 Universal III-class feast", mk 2026 6 30,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3",
- 24 );
+ 240 );
(* Entry 25 -- RG 91 entry 25 (§4). *)
- ("25 Advent feria to 16 Dec", mk 2026 12 1, cand ~rank:V.Class3 "ef-advent-1-tue", 25);
+ ("25 Advent feria to 16 Dec", mk 2026 12 1, cand ~rank:V.Class3 "ef-advent-1-tue", 250);
(* Entry 26 -- RG 91 entry 26 (§4). *)
( "26 III-class vigil", mk 2026 8 9,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil",
- 26 );
+ 260 );
(* Also worth doing: a NON-universal-layer Class3 vigil must still be 26,
not 23 -- pins entry 23's [not is_vigil] guard. *)
( "26 III-class vigil (non-base layer)", mk 2026 8 10,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-patron-vigil",
- 26 );
+ 260 );
(* Task 11, issue (a): the sanctoral bootstrap (data/ef/sanctoral.sexp)
names its vigils with lectio's OWN "vigil-of-X" PREFIX convention, not
[PE.vigil_suffix]'s "-vigil" SUFFIX every row above uses -- exactly
the mismatch Task 7's review predicted. These two rows use the real
bootstrapped slugs verbatim (data/ef/sanctoral.sexp: 28 Jun, 9 Aug),
proving [band] recognises the prefix convention too: without it, both
- would misfile at 16/24 (an ordinary feast of the same rank) instead
- of 21/26. *)
+ would misfile at 160/240 (an ordinary feast of the same rank) instead
+ of 210/260. *)
( "21 II-class vigil via the sanctoral data's own \"vigil-of-X\" prefix",
mk 2026 6 28,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "vigil-of-sts-peter-paul",
- 21 );
+ 210 );
( "26 III-class vigil via the sanctoral data's own \"vigil-of-X\" prefix",
mk 2026 8 9,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "vigil-of-st-lawrence",
- 26 );
+ 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",
- 27 );
+ 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", 28);
+ ("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
is not the Nativity or Pentecost (entries 5/9, the only I-class
vigils the table names) and so has no entry to fall into. Proves the
@@ -662,7 +714,87 @@ let disposition_cases =
cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Lord ~layer:PE.universal_layer
"ef-synthetic-class3-lord-feast",
an_ordinary_sunday,
- "Commemorate(Privileged)" )
+ "Commemorate(Privileged)" );
+ (* PRIMARY authority -- CORRECTED, fix round 1 (coordinator finding 7):
+ RG 95's own second paragraph, present in ALL THREE documents
+ including the electronic transcription: "Si vero duo festa eiusdem
+ Divinae Personae... simul occurrunt, fit de festo, quod in tabella
+ praecedentiae superiorem obtinet locum et aliud omittitur" -- if two
+ feasts of the SAME DIVINE PERSON occur together, the one higher in
+ the table is kept, the OTHER OMITTED -- an occurrence-level rule
+ closer to this exact collision than RG 112(a) below.
+ CORROBORATION, RG 112(a) (docs/research/rules-register.md §4/§6.0,
+ primary text, verified against all three documents): "112. Ad
+ commemorationes et orationes quod attinet, haec insuper serventur:
+ a) Officium, Missa aut commemoratio de aliquo festo vel mysterio
+ unius Divinae Personae excludit commemorationem aut orationem de
+ alio festo vel mysterio eiusdem Divinae personae" -- the Office,
+ Mass or commemoration of some feast or MYSTERY of ONE Divine Person
+ excludes a commemoration or oration of ANOTHER feast or mystery of
+ the SAME Divine Person. Real witness, the ONE date these two
+ candidates can ever coincide: Holy Family (RG 17(b)) as winner, the
+ Commemoration of the Baptism of the Lord (13 January, data/ef/
+ sanctoral.sexp: Class2, subject Lord, status Feast) as loser, on the
+ seven years 13 January is itself Holy Family's own date (2008, 2013,
+ 2019, 2030, 2036, 2041, 2047).
+ CORROBORATION 2, the Holy Family Mass propers' own more specific
+ rubric, both photographic scans, word for word (real, and settles 13
+ January on its own regardless of RG 95/112(a)): "Si festum S.
+ Familiae occurrerit die 13 ianuarii, Missa dicitur de festo S.
+ Familiae, SINE COMMEMORATIONE BAPTISMATIS D.N.I.C., et sine
+ commemoratione dominicae" -- if the feast of the Holy Family occurs
+ on 13 January, the Mass is said of the Holy Family, WITHOUT
+ commemoration of the Baptism, and without commemoration of the
+ Sunday (RG 17(b)'s own general rule, restated).
+ CORRECTED, fix round 1 (coordinator finding 2): a previous version of
+ this comment claimed the electronic transcription's absence of this
+ specific rubric was "exactly the transcription's own documented
+ defect" -- WRONG, struck. That transcription (a 2006 web capture)
+ carries almost no Mass-propers text of any kind (5 Introit
+ occurrences in 26,322 lines against 61/402 in the two photographic
+ scans; its own "Proprium de Tempore" page is a table of contents
+ linking to PDFs the capture never pulled in) -- a coverage gap, not
+ a silent drop from content it otherwise has. RG 112 itself, unlike
+ this rubric, IS present in full in the transcription (all four
+ sub-clauses, verbatim) -- checked directly, not assumed. *)
+ ( "RG112(a): a feast/mystery of the Lord excludes a commemoration of \
+ ANOTHER feast/mystery of the Lord -- Omit, not Commemorate (Holy \
+ Family vs the Baptism of the Lord, 13 January)",
+ of_temporal (T.holy_family_sunday 2019),
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
+ "commemoration-of-the-baptism-of-the-lord",
+ "Omit" );
+ (* Conjunct 1/2, LOSER subject: an ORDINARY saint losing to the SAME
+ Lord-subject winner must still reach the ordinary commemorate flow,
+ not RG 112(a)'s exclusion -- RG 112(a) excludes another mystery of
+ the SAME Divine Person, not every commemoration a Lord feast happens
+ to outrank. Dropping the loser's own [subject = Lord] test would
+ wrongly Omit this ordinary saint's commemoration too. *)
+ ( "RG112(a) does NOT fire when only the WINNER is a mystery of the Lord \
+ -- an ordinary saint stays Commemorate(Ordinary)",
+ of_temporal (T.holy_family_sunday 2019),
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer
+ "ef-synthetic-ordinary-saint",
+ "Commemorate(Ordinary)" );
+ (* Conjunct 2/2, WINNER subject: a Class2 SAINT'S feast (not of the
+ Lord) impeding an ordinary temporal candidate must not exclude an
+ otherwise-due Lord commemoration either -- RG 112(a) is keyed on the
+ WINNER's own subject too, not just the loser's. Real slug/rank from
+ data/ef/sanctoral.sexp (25 January, Class3, "Conversion of St
+ Paul") standing in for a same-rank saint winner; only the [subject]
+ distinction is under test here, not a real-date collision (Holy
+ Family's own subject is what makes this branch reachable at all --
+ an ordinary sanctoral saint never wins against Holy Family in
+ practice, {!band}'s [entry_14_movable_band] sees to that, so this
+ row is SYNTHETIC in the same sense the RG16(a) block's own Class3
+ row above is: defensive, not a live witness). *)
+ ( "SYNTHETIC: RG112(a) does NOT fire when only the LOSER is a mystery \
+ of the Lord -- stays Commemorate(Ordinary)",
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer
+ "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)" )
]
(* Task 9: [privilege_of]'s RG 109 categories (§4, "Commemorations"),
@@ -1218,6 +1350,82 @@ let test_rg16a_lord_feast_suppresses_sunday_end_to_end () =
(List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted
|> List.sort compare)
+(* End-to-end proof of BOTH fixes this task adds together, {!P.resolve} in
+ full (band picks the winner, disposition and admit decide the loser's
+ fate), not just the two isolated functions [cases]/[disposition_cases]
+ already prove: 13 January 2019, the latest possible date Holy Family can
+ fall on (Epiphany itself a Sunday) and one of the seven years it
+ coincides with the fixed Commemoration of the Baptism of the Lord.
+ {!band}'s [entry_14_movable_band] (RG 91 entry 14, "primum mobilia") must
+ pick Holy Family as [observed], not the fixed Baptism candidate (which,
+ absent that fix, would win outright via {!compare_by}'s own alphabetical
+ tie-break at the shared literal 14 -- backwards from the primary text);
+ {!PE.disposition}'s new RG 112(a) branch must then Omit the Baptism
+ entirely, not merely demote it to a commemoration -- confirmed against
+ BOTH the general rubric (RG 112(a)) and the Holy Family Mass propers' own
+ more specific 13-January note (this file's own citation on the
+ RG112(a) [disposition_cases] rows above), and independently against
+ missalemeum's own JSON for this exact date (register §6.0): title "The
+ Holy Family: Jesus, Mary & Joseph", commemorations [], the Baptism
+ listed only under "displaced". *)
+let test_holy_family_excludes_baptism_end_to_end () =
+ let date = T.holy_family_sunday 2019 in
+ Alcotest.(check string) "sanity: this really is 13 January 2019" "2019-01-13" (D.to_iso8601 date);
+ let day_ctx = ctx date in
+ let holy_family = of_temporal date in
+ let baptism =
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
+ "commemoration-of-the-baptism-of-the-lord"
+ in
+ let resolution = P.resolve real_rules day_ctx ~temporal:holy_family ~sanctoral:[ baptism ] in
+ Alcotest.(check string) "Holy Family is observed, not the fixed Baptism (RG91 e14: movable first)"
+ "ef-time-after-epiphany-sunday-1" (S.to_string resolution.P.observed.P.cel.Cel.slug);
+ Alcotest.(check string) "II class" "class-2" (V.rank_to_string resolution.P.observed.P.cel.Cel.rank);
+ Alcotest.(check int)
+ "no commemorations at all -- RG112(a)'s own exclusion, not merely dropped by an admission count"
+ 0 (List.length resolution.P.commemorations);
+ Alcotest.(check (list string)) "the Baptism is OMITTED, not merely uncommemorated"
+ [ "commemoration-of-the-baptism-of-the-lord" ]
+ (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.omitted)
+
+(* Fix round 1 (coordinator finding 1), end-to-end proof of the
+ [entry_14_movable_band] fix itself: a genuine I-class candidate landing
+ inside Holy Family's own 7-13 January window (a synthetic proper/indult
+ feast, standing in for the diocesan-overlay shape {!band}'s own new
+ comment names as the live risk) must be OBSERVED and must NOT transfer
+ -- reproducing, at the [Precedence.resolve] level, the exact regression
+ the coordinator demonstrated against the pre-fix [-14] sentinel ("the
+ I-class feast displaced and transferred to the next day"). With the fix,
+ {!band}'s entry 11 (110) beats {!PE.entry_14_movable_band} (139)
+ outright, so the I-class candidate wins the day, and Holy Family itself
+ -- losing, is_temporal, its own slug carrying the "-sunday" marker --
+ falls to RG 109(a)'s ordinary privileged-Sunday-commemoration path
+ (disposition's final catch-all, [privilege_of]'s (a) branch), the same
+ treatment ANY genuine feast outranking an ordinary Sunday gets: neither
+ omitted nor transferred, a Privileged commemoration. *)
+let test_class1_feast_inside_holy_familys_window_end_to_end () =
+ let date = mk 2026 1 11 in
+ Alcotest.(check string) "sanity: this is Holy Family's own date in 2026" "2026-01-11" (D.to_iso8601 date);
+ let day_ctx = ctx date in
+ let holy_family = of_temporal date in
+ let synthetic_class1 =
+ cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-synthetic-class1-probe"
+ in
+ let resolution = P.resolve real_rules day_ctx ~temporal:holy_family ~sanctoral:[ synthetic_class1 ] in
+ Alcotest.(check string) "the I-class candidate is observed, NOT Holy Family (RG91 e11 beats e14-movable)"
+ "ef-synthetic-class1-probe" (S.to_string resolution.P.observed.P.cel.Cel.slug);
+ Alcotest.(check (list string)) "nothing transfers -- the pre-fix bug moved the I-class feast to the next day"
+ [] (List.map (fun c -> S.to_string c.P.cel.Cel.slug) resolution.P.deferred);
+ (match resolution.P.commemorations with
+ | [ (c, P.Privileged) ] ->
+ Alcotest.(check string)
+ "Holy Family itself is privileged-commemorated (RG109(a), a Sunday-shaped office \
+ outranked by a genuine feast), neither omitted nor transferred"
+ "ef-time-after-epiphany-sunday-1" (S.to_string c.P.cel.Cel.slug)
+ | other ->
+ Alcotest.failf "expected exactly one Privileged commemoration (Holy Family), got %d"
+ (List.length other))
+
(* Completes Task 7's carried fix (RG 91 entry 8, §4): on a real Sunday
landing on 2 November, All Souls does not merely lose (that was Task 7's
[band] fix, proved by [test_all_souls_yields_to_sunday] above) -- it must
@@ -1465,6 +1673,14 @@ let suite =
"RG16(a): a Feast of the Lord suppresses the Sunday's own commemoration entirely, \
end-to-end (2028-08-06)"
`Quick test_rg16a_lord_feast_suppresses_sunday_end_to_end;
+ Alcotest.test_case
+ "RG112(a) + RG91 e14 \"primum mobilia\": Holy Family excludes the Baptism of the Lord \
+ entirely, end-to-end (13 Jan 2019)"
+ `Quick test_holy_family_excludes_baptism_end_to_end;
+ Alcotest.test_case
+ "fix round 1, finding 1: a synthetic I-class candidate inside Holy Family's own window is \
+ observed and does not transfer (2026-01-11)"
+ `Quick test_class1_feast_inside_holy_familys_window_end_to_end;
Alcotest.test_case "transfer_target: general RG96 search walks past more than one blocked day"
`Quick test_transfer_target_general_multi_step_search;
Alcotest.test_case "transfer_target: Annunciation exception starts at Monday after Low Sunday"
diff --git a/test/test_temporal_ef.ml b/test/test_temporal_ef.ml
index 751abd2..23a817d 100644
--- a/test/test_temporal_ef.ml
+++ b/test/test_temporal_ef.ml
@@ -167,12 +167,110 @@ let test_resumed_sundays () =
module Cel = Colitur_kernel.Celebration
module Sl = Colitur_kernel.Slug
module Colr = Colitur_kernel.Colour
+module Sub = Colitur_kernel.Subject
let office dt = (T.temporal dt).Colitur_kernel.Temporal.office
let slug_of dt = Sl.to_string (office dt).Cel.slug
let rank_of dt = V.rank_to_string (office dt).Cel.rank
let colour_of dt = Colr.to_string (office dt).Cel.colour
+let subject_of dt = (office dt).Cel.subject
let temporal_week dt = (T.temporal dt).Colitur_kernel.Temporal.week
+let la = Colitur_kernel.Lang.of_string_exn "la"
+let en = Colitur_kernel.Lang.of_string_exn "en"
+let name_la_of dt = Colitur_kernel.Names.find (office dt).Cel.names la
+
+(* RG 17(b) (Caput III, "De Dominicis"), primary text (scan-verified,
+ docs/research/rules-register.md §4): "festum S. Familiae Iesu, Mariae,
+ Ioseph, celebrandum dominica prima post Epiphaniam" -- the Holy Family is
+ celebrated on the first Sunday after Epiphany, EVERY year (unlike RG
+ 17(a)'s Holy Name, whose narrower 2-5 January window can be empty and
+ carries an explicit "vel ea deficiente, die 2 ianuarii" fallback in the
+ calendarium itself -- 7-13 January can never be empty of a Sunday, so
+ Holy Family needs, and the calendarium carries, no such fallback).
+ [T.holy_family_sunday] is [named]'s own [week_origin Time_after_epiphany]
+ formula, restated for its own citation -- not re-derived a second time.
+
+ Deliberately NOT part of [T.named]: [named] is checked before
+ [T.sunday_slug]'s own Christmastide branch in [T.temporal]'s dispatch, but
+ this feast's slug, rank and colour are EXACTLY what that branch already
+ computes for the 7-13 January Sunday (register's own pre-existing note on
+ [sunday_slug]: "the key stays lectio's") -- the only field that actually
+ needed to change is [subject] (silently always [Temporal] before this),
+ so [T.temporal] applies it as a targeted override inside that branch
+ rather than duplicating slug/rank/colour a second time through [named]'s
+ 4-tuple. *)
+let test_holy_family () =
+ (* 2026: Epiphany (6 Jan) is a Tuesday, so Holy Family falls 11 January --
+ an ordinary instance, no collision with the Baptism (13 Jan). *)
+ Alcotest.(check string) "2026: slug is the ordinary I-post-Epiphany Sunday key"
+ "ef-time-after-epiphany-sunday-1" (slug_of (d 2026 1 11));
+ Alcotest.(check string) "2026: II class" "class-2" (rank_of (d 2026 1 11));
+ Alcotest.(check string) "2026: white" "white" (colour_of (d 2026 1 11));
+ Alcotest.(check bool) "2026: subject is Lord (RG 91 entry 14, \"Festa Domini II \
+ classis\" -- the mystery of the Holy Family touches Christ)"
+ true
+ (subject_of (d 2026 1 11) = Sub.Lord);
+ (* fix round 1 (coordinator finding 3): a primary-sourced LATIN name,
+ zero circularity (the calendarium's own January table and the Mass
+ propers' own heading, both photographic scans, word for word:
+ "Sanctae Familiae Iesu, Mariae, Ioseph"). No ENGLISH name -- deliberate
+ (temporal_ef.ml's own comment), and every OTHER temporal-cycle
+ candidate still carries no name of any kind. *)
+ Alcotest.(check (option string)) "2026: Latin name from the calendarium/propers, verbatim"
+ (Some "Sanctae Familiae Iesu, Mariae, Ioseph") (name_la_of (d 2026 1 11));
+ Alcotest.(check (option string)) "2026: no English name (deliberate -- would read the oracle it is \
+ compared against)"
+ None (Colitur_kernel.Names.find (office (d 2026 1 11)).Cel.names en);
+ Alcotest.(check (option string)) "2026: an ordinary neighbouring Sunday carries no Latin name either"
+ None (name_la_of (d 2026 1 18));
+ (* 2019: Epiphany itself is a Sunday, so the first Sunday AFTER it is the
+ LATEST possible date, 13 January -- the one date Holy Family collides
+ with the fixed Commemoration of the Baptism of the Lord (data/ef/
+ sanctoral.sexp: "commemoration-of-the-baptism-of-the-lord", also 13
+ Jan, II class, subject Lord). [T.temporal]'s own resolution of WHO wins
+ that day is {!Rite_ef.Precedence_ef}'s business (band/disposition, see
+ test_precedence_ef.ml), not this function's -- this only proves
+ [T.temporal] itself still identifies 13 January 2019 as Holy Family's
+ own date, unconditionally, regardless of what else might compete for
+ the day once precedence is applied. *)
+ Alcotest.(check bool) "2019: 6 January is a Sunday" true (D.weekday (d 2019 1 6) = D.Sun);
+ Alcotest.(check string) "2019: Holy Family falls on the latest possible date, 13 Jan"
+ "2019-01-13" (D.to_iso8601 (T.holy_family_sunday 2019));
+ Alcotest.(check string) "2019: still the same slug" "ef-time-after-epiphany-sunday-1"
+ (slug_of (T.holy_family_sunday 2019));
+ Alcotest.(check bool) "2019: still subject Lord" true (subject_of (T.holy_family_sunday 2019) = Sub.Lord);
+ (* Every possible weekday for 6 January exercised, proving the formula
+ never leaves the 7-13 window empty (register's own citation above) --
+ not merely the two years already checked above. *)
+ List.iter
+ (fun y ->
+ let hf = T.holy_family_sunday y in
+ Alcotest.(check bool) (Printf.sprintf "%d: Holy Family is a Sunday" y) true
+ (D.weekday hf = D.Sun);
+ Alcotest.(check bool) (Printf.sprintf "%d: Holy Family falls 7-13 January" y) true
+ (D.month hf = 1 && D.day hf >= 7 && D.day hf <= 13))
+ [ 2019; 2020; 2021; 2022; 2023; 2024; 2025; 2026; 2027 ];
+ (* Negative control: an ORDINARY Sunday after Epiphany (the 2nd) stays
+ [subject Temporal] -- the override is scoped to exactly one Sunday a
+ year, not every Time-after-Epiphany-shaped Sunday. *)
+ Alcotest.(check bool) "2026: the following Sunday (18 Jan, II after Epiphany) is NOT Lord"
+ true
+ (subject_of (d 2026 1 18) = Sub.Temporal)
+
+(* Three of [test_holy_family]'s own exhaustive-weekday sample, pinned to
+ their literal ISO dates and independently cross-checked against
+ `date -d <iso> +%A` (the same discipline test_golden.ml's own header
+ describes for its own weekday assertions) -- the earliest possible date
+ (Epiphany a Saturday), the latest (Epiphany a Sunday), and one ordinary
+ middle case, rather than trusting the structural "always 7-13" check
+ above on its own without ever anchoring it to real calendar dates. *)
+let test_holy_family_no_gap_year () =
+ Alcotest.(check string) "2024 (6 Jan a Saturday): earliest possible date, 7 Jan" "2024-01-07"
+ (D.to_iso8601 (T.holy_family_sunday 2024));
+ Alcotest.(check string) "2019 (6 Jan a Sunday): latest possible date, 13 Jan" "2019-01-13"
+ (D.to_iso8601 (T.holy_family_sunday 2019));
+ Alcotest.(check string) "2025 (6 Jan a Monday): 12 Jan" "2025-01-12"
+ (D.to_iso8601 (T.holy_family_sunday 2025))
(* Regression for a Task 14 Validate finding, reworked (register finding 4):
[named] no longer carries its own week at all -- [temporal] computes it
@@ -432,6 +530,59 @@ let test_anchors_erosion_is_caught () =
[ "ef-ascension"; "ef-nativity" ]
(missing_from_anchors ~named_slugs ~anchors:eroded_anchors)
+(* Holy Family's own anchor coverage, kept SEPARATE from the two tests above:
+ it is genuinely NOT one of [T.named]'s outputs (test_holy_family's own
+ comment explains why -- [T.temporal] applies it as a targeted override
+ inside [T.sunday_slug]'s branch instead), so [named_slugs_for_year]'s scan
+ cannot see it, and folding it into [test_anchors_cover_all_named_days]/
+ [test_anchors_erosion_is_caught] would misrepresent what those two are
+ actually proving ("everything [named] produces"). This proves the same
+ two properties directly against [T.holy_family_sunday] instead: the entry
+ is present, AND deleting it is caught -- {!Validate}'s own production
+ anchor check (validate.ml, register/spec §5.7) calls [temporal], not
+ [named], so it is unaffected by this distinction and already covers this
+ entry regardless; this is this file's own unit-level guard against the
+ same erosion.
+
+ CORRECTED, fix round 1 (coordinator finding 5): the original version of
+ this test built [eroded] by [List.filter]-ing the exact pair it then
+ asserted [List.mem ... eroded = false] against -- tautological (removing
+ an element from a list and then checking it is not there proves nothing
+ about a DETECTOR; it proves [List.filter] works). The genuine detector
+ the OTHER erosion test above actually exercises is [missing_from_anchors]
+ itself, called with an "expected" set sourced INDEPENDENTLY of
+ [T.anchors] ([named_slugs_for_year], via [T.named] -- a different
+ function entirely) -- the interesting claim is that the SAME slug is
+ confirmed by two independent computations, not merely that a filtered
+ list lacks what was filtered out of it. Reworked to match: the
+ "expected" side here is [T.sunday_slug expected_date] -- computed via
+ season/weekday logic, not read out of [T.anchors]'s own hand-typed
+ string -- so the presence check below is a genuine cross-check between
+ two independent sources, and the erosion check reuses the real
+ [missing_from_anchors] detector rather than re-deriving a bespoke,
+ self-referential one. *)
+let test_holy_family_anchor_present_and_erosion_is_caught () =
+ let y = 2026 in
+ let expected_date = T.holy_family_sunday y in
+ let expected_slug =
+ match T.sunday_slug expected_date with
+ | Some s -> s
+ | None -> Alcotest.fail "sanity: holy_family_sunday must itself be a Sunday"
+ in
+ Alcotest.(check (list string))
+ "the anchor entry exists: T.sunday_slug's own independent computation is not missing from T.anchors" []
+ (missing_from_anchors ~named_slugs:[ expected_slug ] ~anchors:(anchor_slugs y));
+ (* Erosion: with the entry struck out of [anchors], [missing_from_anchors]
+ -- the SAME real detector, not a bespoke re-check -- must now report it
+ missing, using [expected_slug]'s own independent source ([sunday_slug])
+ as the thing being checked FOR, not derived from the erosion itself. *)
+ let eroded_anchors = List.filter (fun s -> s <> expected_slug) (anchor_slugs y) in
+ Alcotest.(check (list string)) "the erosion is caught: missing_from_anchors reports it, and only it"
+ [ expected_slug ]
+ (missing_from_anchors ~named_slugs:[ expected_slug ] ~anchors:eroded_anchors);
+ Alcotest.(check string) "sanity: temporal itself is unaffected by the anchors-list erosion"
+ expected_slug (slug_of expected_date)
+
let test_totality () =
(* Every day of 2026 yields an office without raising. Not a slug
re-validation -- Slug.t is a private string validated on construction,
@@ -453,6 +604,8 @@ let suite_extra =
Alcotest.test_case "nativity octave" `Quick test_nativity_octave;
Alcotest.test_case "week numbers" `Quick test_week_numbers;
Alcotest.test_case "sunday slugs" `Quick test_sunday_slugs;
+ Alcotest.test_case "holy family (RG 17(b))" `Quick test_holy_family;
+ Alcotest.test_case "holy family: no gap year (RG 17(b))" `Quick test_holy_family_no_gap_year;
Alcotest.test_case "week/sunday_slug agree" `Quick test_week_sunday_slug_agree;
Alcotest.test_case "resumed sundays" `Quick test_resumed_sundays;
Alcotest.test_case "ferial slugs and ranks" `Quick test_ferial_slugs_and_ranks;
@@ -465,6 +618,8 @@ let suite_extra =
Alcotest.test_case "totality" `Quick test_totality;
Alcotest.test_case "anchors cover all named days" `Quick
test_anchors_cover_all_named_days;
+ Alcotest.test_case "holy family anchor present, erosion caught" `Quick
+ test_holy_family_anchor_present_and_erosion_is_caught;
Alcotest.test_case "anchors erosion is caught" `Quick test_anchors_erosion_is_caught ]
let suite =