summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/ef/expected-divergences.sexp5
-rw-r--r--lib/rites/rite_ef/temporal_ef.ml122
-rw-r--r--lib/rites/rite_ef/temporal_ef.mli11
-rw-r--r--test/test_differential.ml21
-rw-r--r--test/test_golden.ml53
-rw-r--r--test/test_precedence_ef.ml105
-rw-r--r--test/test_temporal_ef.ml156
7 files changed, 471 insertions, 2 deletions
diff --git a/data/ef/expected-divergences.sexp b/data/ef/expected-divergences.sexp
index 283c3bc..261e900 100644
--- a/data/ef/expected-divergences.sexp
+++ b/data/ef/expected-divergences.sexp
@@ -67,3 +67,8 @@
(verdict colitur)
(note "ef-rg112-rg110 task: SPLIT OUT of C1's own blanket 6-13 January window, not a new kind of divergence -- the same discipline the now-closed C13 already established for this exact date (C1's own note above), applied again because building Holy Family reopened the identical shape C13 used to cover, for a different underlying reason. Before this task, colitur had no Holy Family office: on these seven dates (13 January when it is itself a Sunday -- 2008, 2013, 2019, 2030, 2036, 2041, 2047), colitur's generic Sunday fallback lost outright to the fixed Commemoration of the Baptism of the Lord (already subject Lord in the rebootstrapped data, RG16(a)'s existing mechanism), producing the SAME slug lectio's own tridentine-calendar.ini shows there (`commemoration-of-the-baptism-of-the-lord`) -- only Season differed, inside C1's own shape. Holy Family (RG 17(b)) now correctly outranks the fixed Baptism (RG 91 entry 14's own \"primum mobilia, deinde fixa\" -- {!Rite_ef.Precedence_ef.entry_14_movable_band}) and excludes it as a commemoration entirely (RG 112(a), directly confirmed by the Holy Family Mass propers' own more specific 13-January rubric, both photographic scans: \"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\"), so colitur's own slug on these 7 dates changes to `ef-time-after-epiphany-sunday-1` (Holy Family's own -- unchanged from the plain Sunday key it always carried, temporal_ef.ml's own comment on why) -- a genuine SLUG divergence against lectio, which has no Holy Family at all and still shows the fixed Baptism observed. Gated on the literal 7-date list AND colitur's own slug (the same identity guard C1/C6/C8/C14 already apply, fix round 1's own finding 1). Derived from the OCaml comparator's own failure output directly (test_layer_c_counts_match_citations), not by hand-counting first and cross-checking after: 7.")
(expected_rows 7))
+ ((id C16)
+ (citation "RG 17(a) (\"festum Ss.mi Nominis Iesu, celebrandum dominica quae occurrit a die 2 ad 5 ianuarii, SECUS DIE 2 IANUARII\") + the calendarium's own January table, both photographic scans, word for word: \"Dominica inter octavam Nativitatis Domini et Epiphaniam, vel, EA DEFICIENTE, die 2 ianuarii: Sanctissimi Nominis Iesu, II classis\"")
+ (verdict colitur)
+ (note "ef-holyname-rg110 task: RG 17(a)'s own fallback, previously unbuilt (register §6.2's own open item -- colitur emitted no Holy Name office at all in a year with no Sunday 2-5 January, not merely an unnamed generic Sunday the way Holy Family's own pre-fix gap was). 2 January in a year with no Sunday in that window now carries the feast (`ef-holy-name`, class-2, subject Lord) instead of the ordinary Christmastide ferial fallback (`ef-christmas-1-<weekday>`/`ef-christmas-0-<weekday>` in lectio's own naming, class-4) -- a genuine SLUG AND RANK divergence against lectio, which has no fallback for this window at all (`~/git/projects/lectio/internal/caldata/tridentine-calendar.ini` carries no 01-02 entry; checked directly, not assumed) and shows the plain ferial slug on every one of these dates regardless. Gated on the literal date list AND colitur's own slug (the same identity guard C1/C6/C8/C14/C15 already apply, fix round 1's own finding 1). 20 of the 46 years in the fixture's own 2005-2050 span have no Sunday 2-5 January -- independently re-derived against `date -d <year>-01-0{2,3,4,5} +%u` for every year in range (not the register's domain-wide 3,619-of-8,417 figure divided down; a fresh count over this file's own narrower window), then cross-checked against the OCaml comparator's own failure output before being set here: 20.")
+ (expected_rows 20))
diff --git a/lib/rites/rite_ef/temporal_ef.ml b/lib/rites/rite_ef/temporal_ef.ml
index 1ce9de9..721790d 100644
--- a/lib/rites/rite_ef/temporal_ef.ml
+++ b/lib/rites/rite_ef/temporal_ef.ml
@@ -104,6 +104,56 @@ let christ_the_king y = sunday_on_or_before (mk y 10 31)
date against it without reaching into [week_origin]'s implementation. *)
let holy_family_sunday y = Date.add_days (sunday_on_or_before (mk y 1 6)) 7
+(* RG 17(a) (Caput III, "De Dominicis"; full text on [holy_family_sunday]'s
+ own citation above): "festum Ss.mi Nominis Iesu, celebrandum dominica quae
+ occurrit a die 2 ad 5 ianuarii (secus die 2 ianuarii)" -- the feast of the
+ Most Holy Name of Jesus, celebrated on the Sunday falling 2-5 January,
+ OTHERWISE [on] 2 January [itself]. [None] here means no such Sunday exists
+ that year -- [holy_name_fallback_date]'s own fallback branch is for
+ exactly that case; see its own citation for why the window CAN be empty
+ (unlike (b)'s 7-13 January window, which never is).
+
+ Formula: the Sunday on or before 5 January is in [2,5] iff it is not
+ before 2 January -- the window is only 4 days wide, so there is at most
+ one Sunday in it either way. *)
+let holy_name_sunday y =
+ let s = sunday_on_or_before (mk y 1 5) in
+ if on_or_after s (mk y 1 2) then Some s else None
+
+(* RG 17(a)'s own "secus die 2 ianuarii" clause, and the calendarium's
+ January table, BOTH photographic scans, word for word (docs/research/
+ rules-register.md; also confirmed in the Mass propers' own heading for
+ this feast, both scans, Caput "Sanctissimi Nominis Iesu": "DOMINICA a die
+ 2 ad diem 5 ianuarii occurrenti, VEL, SI HAEC DEFECERIT, DIE 2 IANUARII" --
+ the SUNDAY occurring 2-5 January, OR, IF THIS IS LACKING, [on] 2 January):
+ *"Dominica inter octavam Nativitatis Domini et Epiphaniam, vel, EA
+ DEFICIENTE, die 2 ianuarii: Sanctissimi Nominis Iesu, II classis"* -- the
+ Sunday between the Octave of the Nativity and Epiphany, or, THAT FAILING,
+ 2 January. Unlike [holy_family_sunday]'s 7-13 January window (which can
+ never be empty of a Sunday), this 2-5 January window CAN be, and is in
+ 3,619 of the 8,417 years in [1583, 9999] (a plain weekday check,
+ independently cross-checked against `date -d`) -- [temporal]'s own
+ dispatch applies this fallback date, tagged exactly like the Sunday shape
+ (subject Lord, the same Latin name), only when [holy_name_sunday y] is
+ [None] for that date's own civil year. *)
+let holy_name_fallback_date y = mk y 1 2
+
+(* The ONE Latin title both of RG 17(a)'s two shapes (the Sunday and the 2
+ January fallback) carry -- ONE feast, RG 91's own "primum mobilia, deinde
+ fixa" language notwithstanding (see [temporal]'s own fallback-branch
+ comment for why both shapes band identically at entry 14's MOVABLE half,
+ not split across the two halves by year). Bound once so the two [temporal]
+ branches that use it cannot silently drift apart, the same discipline
+ [holy_family_sunday] already gets from being named instead of re-derived.
+ Both photographic scans, word for word, twice over (the calendarium's own
+ January table AND the Mass propers' own running header/heading, this
+ file's own citations on [holy_name_sunday]/[holy_name_fallback_date]
+ above): "Sanctissimi Nominis Iesu". No English name, deliberately -- the
+ SAME "would read the oracle it is compared against" reasoning
+ [holy_family_sunday]'s own [names] comment below gives for Holy Family. *)
+let holy_name_names =
+ Colitur_kernel.Names.of_list [ (Colitur_kernel.Lang.of_string_exn "la", "Sanctissimi Nominis Iesu") ]
+
let same a b = Date.compare a b = 0
(* Named temporal days: the I-class feasts of the Lord (RG 91 entries 1 and 3),
@@ -533,15 +583,68 @@ let temporal d =
item on Holy Name of Jesus, RG 17(a)) -- this is a
targeted addition for the one day this task built, not a
claim that the gap is closed generally. *)
+ (* RG 17(a) (this file's own [holy_name_sunday], full citation
+ there): the SECOND Sunday a year this branch must not
+ leave [subject = Temporal] -- added alongside Holy
+ Family's own check above, not replacing it (the two
+ windows, 2-5 and 7-13 January, never overlap, so at most
+ one of the two conditions is ever true for a given [d]).
+ Guarded on [holy_name_sunday y = Some d] rather than only
+ "is [d] in [2,5] January": every OTHER Sunday in that
+ range must stay unnamed, and in a year where a DIFFERENT
+ day of the window is the one true Sunday, [d] itself is
+ never reached by [sunday_slug] as a Sunday to begin with
+ (weekday alone already excludes it) -- but writing the
+ check this way, against the SAME independently-computed
+ date [holy_name_sunday] returns rather than a bare month/
+ day range, is what test_temporal_ef.ml's own anchor test
+ below cross-checks against. *)
let subject, names =
if same d (holy_family_sunday y) then
( Colitur_kernel.Subject.Lord,
Colitur_kernel.Names.of_list
[ (Colitur_kernel.Lang.of_string_exn "la", "Sanctae Familiae Iesu, Mariae, Ioseph") ] )
+ else if holy_name_sunday y = Some d then (Colitur_kernel.Subject.Lord, holy_name_names)
else (Colitur_kernel.Subject.Temporal, Colitur_kernel.Names.empty)
in
build ~subject ~names ~season:s ~slug ~colour ~rank ~week:(week d) ()
- | None -> (
+ | None ->
+ (* RG 17(a)'s own fallback, "secus die 2 ianuarii" (full
+ citation on [holy_name_fallback_date] above): ONLY when
+ [holy_name_sunday y] is [None] for [d]'s own civil year --
+ i.e. 2-5 January genuinely has no Sunday that year -- does
+ 2 January itself carry the feast; every other year, 2
+ January is an ordinary Christmastide feria, exactly what
+ [christmastide_feria_slug] below already computes for it.
+ Checked here, ahead of that generic fallback, the same way
+ [named]'s own fixed dates are checked ahead of everything
+ ferial -- NOT folded into [named] itself, because unlike
+ every one of [named]'s ~20 entries this one is
+ CONDITIONAL on a per-year fact ([holy_name_sunday y] =
+ [None]) that [named]'s bare [Date.t -> ... option] shape
+ has no way to express without threading [subject] through
+ every one of its other branches too -- the same reason
+ Holy Family, immediately above, is not one of [named]'s
+ outputs either (that branch's own comment).
+
+ Season Christmastide (RG 72-73: 2 January is always within
+ it); rank/colour/subject/names identical to the Sunday
+ shape immediately above -- ONE feast, {!holy_name_names}'s
+ own comment on why both shapes share it. Band-classified
+ at entry 14's MOVABLE half regardless of which shape fired
+ this year (Precedence_ef.band; both shapes are built
+ through this same [temporal] function, hence [origin =
+ Temporal] either way) -- RG 91's "primum mobilia, deinde
+ fixa" split is a property of the TABLE ENTRY a feast
+ occupies, and RG 17(a) names ONE feast with a fallback
+ clause, not two differently-classified feasts that happen
+ to share a Mass formulary; the Mass propers' own heading
+ (docs/research/rules-register.md) states both shapes under
+ the identical title for exactly this reason. *)
+ if same d (holy_name_fallback_date y) && holy_name_sunday y = None then
+ build ~subject:Colitur_kernel.Subject.Lord ~names:holy_name_names ~season:Christmastide
+ ~slug:"ef-holy-name" ~colour:Colour.White ~rank:Class2 ~week:(week d) ()
+ else (
match christmastide_feria_slug d with
| Some slug ->
build ~season:s ~slug ~colour:(season_colour s) ~rank:(ferial_rank d s)
@@ -608,7 +711,19 @@ let temporal d =
4-tuple), but it is exactly the same kind of independently-computed
anchor this list exists to guard -- an accidental drift in
[holy_family_sunday]'s own [+7] would silently move Holy Family without
- this guard catching it, same as any other entry here. *)
+ this guard catching it, same as any other entry here.
+
+ Holy Name of Jesus (RG 17(a)) is the SAME shape, restated per-year rather
+ than as a single fixed pair: exactly ONE of its two slugs exists for any
+ given [y] ([holy_name_sunday y] is [Some] xor [None]), so exactly one
+ entry -- "ef-holy-name-sunday" at that Sunday, or "ef-holy-name" at
+ [holy_name_fallback_date y] -- is appended below, matching whichever
+ shape [temporal] itself will actually build that year. An erosion of
+ EITHER branch is still caught: {!Validate.run} calls [anchors] for both
+ the civil year a date falls in and the one before it (a liturgical year
+ straddles two), so across any two consecutive years both shapes are
+ exercised regardless of which one civil year [y] itself happens to
+ land on. *)
let anchors y =
let easter = Computus.gregorian_easter y in
let off n = Date.add_days easter n in
@@ -633,6 +748,9 @@ let anchors y =
("ef-corpus-christi", off 60);
("ef-sacred-heart", off 68);
("ef-christ-the-king", christ_the_king y) ]
+ @ (match holy_name_sunday y with
+ | Some d -> [ ("ef-holy-name-sunday", d) ]
+ | None -> [ ("ef-holy-name", holy_name_fallback_date y) ])
(* Compile-time check that this module satisfies the kernel's rite contract. *)
module _ : Colitur_kernel.Temporal.RITE = struct
diff --git a/lib/rites/rite_ef/temporal_ef.mli b/lib/rites/rite_ef/temporal_ef.mli
index 68fe64e..5e85111 100644
--- a/lib/rites/rite_ef/temporal_ef.mli
+++ b/lib/rites/rite_ef/temporal_ef.mli
@@ -21,6 +21,17 @@ val christ_the_king : int -> Date.t
fallback for it), so this needs none. *)
val holy_family_sunday : int -> Date.t
+(** RG 17(a): the Sunday falling 2-5 January in civil year [y], the Most Holy
+ Name of Jesus's own primary date -- [None] when no such Sunday exists
+ that year (3,619 of the 8,417 years in [1583, 9999]), unlike
+ {!holy_family_sunday}'s window, which never is. *)
+val holy_name_sunday : int -> Date.t option
+
+(** RG 17(a)'s own "secus die 2 ianuarii" fallback: 2 January in civil year
+ [y]. Only actually carries the feast when {!holy_name_sunday} [y] is
+ [None] for that year -- {!temporal} is what applies that condition. *)
+val holy_name_fallback_date : int -> Date.t
+
(** The named temporal days: I-class feasts of the Lord, vigils, and days within
the Octave of the Nativity. Returns (season, slug, colour, rank). Carries
no week of its own -- {!temporal} computes it uniformly via {!week} for
diff --git a/test/test_differential.ml b/test/test_differential.ml
index 94adb2c..bea1bf4 100644
--- a/test/test_differential.ml
+++ b/test/test_differential.ml
@@ -500,6 +500,22 @@ let joseph_annunciation_collision_dates_2005_2050 = [ "2008-04-01"; "2035-04-03"
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" ]
+(* C16 -- ef-holyname-rg110 task: RG 17(a)'s own fallback (2 January, "secus
+ die 2 ianuarii"), for every civil year 2005-2050 with no Sunday 2-5
+ January -- independently re-derived against `date -d <year>-01-0{2..5}
+ +%u`, not transcribed from the register's domain-wide figure (see
+ data/ef/expected-divergences.sexp's own C16 note for the full citation
+ and the derivation). lectio has no equivalent fallback at all (checked
+ directly against tridentine-calendar.ini, which carries no 01-02 entry),
+ so it shows the plain Christmastide ferial slug on every one of these
+ dates, class-4 -- colitur's own `ef-holy-name`, class-2, is a genuine
+ [Slug_f; Rank] divergence, not a naming synonym Layer A could absorb. *)
+let holy_name_fallback_dates_2005_2050 =
+ [ "2006-01-02"; "2007-01-02"; "2008-01-02"; "2012-01-02"; "2013-01-02"; "2017-01-02";
+ "2018-01-02"; "2019-01-02"; "2023-01-02"; "2024-01-02"; "2029-01-02"; "2030-01-02";
+ "2034-01-02"; "2035-01-02"; "2036-01-02"; "2040-01-02"; "2041-01-02"; "2045-01-02";
+ "2046-01-02"; "2047-01-02" ]
+
(* [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). *)
@@ -550,6 +566,11 @@ let layer_c_reason (l : row) (c : row) diffs =
&& subset diffs [ Season; Slug_f ]
&& String.equal c.slug "ef-time-after-epiphany-sunday-1"
then Some "C15"
+ else if
+ List.mem l.date holy_name_fallback_dates_2005_2050
+ && subset diffs [ Slug_f; Rank ]
+ && String.equal c.slug "ef-holy-name"
+ then Some "C16"
else None
(* ---------------------------------------------------------------------- *)
diff --git a/test/test_golden.ml b/test/test_golden.ml
index 1724375..c5aa65a 100644
--- a/test/test_golden.ml
+++ b/test/test_golden.ml
@@ -634,6 +634,53 @@ let test_holy_family_earliest_possible_date_2024 () =
"2024-01-07 sunday season=christmastide week=- slug=ef-time-after-epiphany-sunday-1 rank=class-2 \
colour=white subject=lord comms=[] in=- out=[]"
+(* Most Holy Name of Jesus (RG 17(a), scan-verified, docs/research/rules-
+ register.md §6.2/§4): "festum Ss.mi Nominis Iesu, celebrandum dominica
+ quae occurrit a die 2 ad 5 ianuarii (secus die 2 ianuarii)" -- unlike Holy
+ Family (RG 17(b), immediately above), this window CAN be, and routinely
+ is, empty of a Sunday (3,619 of 8,417 years domain-wide) -- the
+ calendarium's own fallback, "vel, ea deficiente, die 2 ianuarii", is what
+ the SECOND pin below exercises.
+
+ *** THIS FILE'S OWN VACUITY WARNING, APPLIED DIRECTLY ***
+
+ [test_holy_name_sunday_shape_2026] pins a year where the Sunday shape
+ fires -- slug/rank/colour/season/comms here are ALL already what
+ [T.sunday_slug]'s Christmastide branch computed before this task's fix
+ (the identical accident Holy Family's own [describe] note above already
+ explains for its shape): an unnamed Sunday and Holy Name of Jesus share
+ every one of those fields on this date. Reverting the fix leaves this
+ pin GREEN except for [subject] -- the ONE field it actually tests, same
+ discipline as Holy Family's own "coordinator finding 4" fix. This pin
+ earns its place only by asserting [subject] explicitly, not by pinning
+ the whole line uncritically.
+
+ [test_holy_name_fallback_2029] is the pin that actually has teeth against
+ the LARGER of the two defects this task closed: before the fix, colitur
+ emitted NO Holy Name office at all in a fallback year -- not merely an
+ unnamed generic Sunday (Holy Family's own pre-fix shape), but the
+ ordinary Christmastide ferial slug (`ef-christmas-1-tuesday`, class-4)
+ the task brief's own example names. Reverting the fallback half of the
+ fix reddens this pin on EVERY field (slug, rank, subject), not only one --
+ the shape this file's header calls "the whole line moved", the opposite
+ of the Sunday pin's own narrow, single-field teeth. *)
+let test_holy_name_sunday_shape_2026 () =
+ check ~msg:"2026-01-04: Holy Name of Jesus, Sunday shape -- oracle-corroborated (missalemeum's own \
+ 2026-01-04 row: rank 2, white, title \"Holy Name of Jesus\")"
+ 2026 1 4
+ "2026-01-04 sunday season=christmastide week=- slug=ef-holy-name-sunday rank=class-2 colour=white \
+ subject=lord comms=[] in=- out=[]"
+
+let test_holy_name_fallback_2029 () =
+ check ~msg:"2029-01-02: Holy Name of Jesus, FALLBACK shape (RG 17(a)'s own \"secus die 2 ianuarii\", the \
+ calendarium's \"vel, ea deficiente, die 2 ianuarii\") -- no Sunday falls 2-5 January 2029 \
+ (`date -d 2029-01-0{2,3,4,5} +%u` = 2,3,4,5, independently confirmed), so 2 January itself \
+ carries the feast; before this task's fix colitur emitted `ef-christmas-1-tuesday class-4` \
+ here instead -- a real, II-class office simply missing, not merely misnamed"
+ 2029 1 2
+ "2029-01-02 tuesday season=christmastide week=- slug=ef-holy-name 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).
@@ -793,6 +840,12 @@ let suite =
`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 "Holy Name of Jesus (RG17(a)), Sunday shape, subject asserted (2026-01-04)" `Quick
+ test_holy_name_sunday_shape_2026;
+ Alcotest.test_case
+ "Holy Name of Jesus, fallback shape (RG17(a) \"secus die 2 ianuarii\", 2029-01-02 -- no Sunday in \
+ the window)"
+ `Quick test_holy_name_fallback_2029;
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_precedence_ef.ml b/test/test_precedence_ef.ml
index 4ee2fd8..d873e34 100644
--- a/test/test_precedence_ef.ml
+++ b/test/test_precedence_ef.ml
@@ -1426,6 +1426,103 @@ let test_class1_feast_inside_holy_familys_window_end_to_end () =
Alcotest.failf "expected exactly one Privileged commemoration (Holy Family), got %d"
(List.length other))
+(* Most Holy Name of Jesus (RG 17(a)) -- the SAME two proofs immediately
+ above, restated for Holy Name instead of Holy Family. UNLIKE Holy Family,
+ there is no LIVE collision to exercise here (this file's own [cases]/
+ [disposition_cases] rows never construct one, because
+ data/ef/sanctoral.sexp has nothing fixed-date 2-5 January besides
+ [telesphorus-pope-and-martyr] (5 Jan, Commemoration_only, so it can never
+ contest [observed] at all -- {!P.resolve}'s own partition holds it out of
+ the band contest entirely) -- register §6.2's own account: "no live
+ collision exists for it in this codebase's current data". These two
+ tests are the SYNTHETIC proof the task brief asked for instead: that
+ {!band}'s [entry_14_movable_band]/RG 112(a) mechanism, ALREADY proved
+ above for Holy Family, governs Holy Name identically, since neither
+ {!band} nor {!disposition} key on anything Holy-Family-specific (only on
+ [subject]/[rank]/[origin], which [temporal_ef.ml]'s fix now sets
+ identically for both feasts) -- not a live-data regression test, a
+ mechanism-generalises-correctly one, the same distinction this file's own
+ header draws between a table row and an end-to-end proof.
+
+ Run against BOTH of RG 17(a)'s own shapes -- the Sunday (2026-01-04) and
+ the 2 January fallback (2029-01-02, {!T.holy_name_fallback_date}) --
+ because [temporal_ef.ml]'s own fallback-branch comment claims both band
+ identically at {!PE.entry_14_movable_band} (ONE feast, not two
+ differently-classified ones); a table proving the SAME two outcomes hold
+ for both dates is direct evidence for that specific claim, not merely
+ for the Sunday shape colitur already had a name for before this task. *)
+let holy_name_probe_dates =
+ [ ("Sunday shape (2026-01-04)", mk 2026 1 4); ("fallback shape (2029-01-02)", T.holy_name_fallback_date 2029) ]
+
+let test_holy_name_excludes_synthetic_lord_end_to_end () =
+ List.iter
+ (fun (label, date) ->
+ let holy_name = of_temporal date in
+ Alcotest.(check bool) (label ^ ": sanity -- temporal_ef really did tag this Lord") true
+ (holy_name.P.cel.Cel.subject = Sub.Lord);
+ let day_ctx = ctx date in
+ let synthetic_lord =
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
+ "ef-synthetic-fixed-lord-probe"
+ in
+ let resolution = P.resolve real_rules day_ctx ~temporal:holy_name ~sanctoral:[ synthetic_lord ] in
+ Alcotest.(check string) (label ^ ": Holy Name is observed (RG91 e14: movable first)")
+ (S.to_string holy_name.P.cel.Cel.slug) (S.to_string resolution.P.observed.P.cel.Cel.slug);
+ Alcotest.(check int) (label ^ ": no commemorations at all -- RG112(a)'s own exclusion") 0
+ (List.length resolution.P.commemorations);
+ Alcotest.(check (list string)) (label ^ ": the synthetic Lord candidate is OMITTED, not commemorated")
+ [ "ef-synthetic-fixed-lord-probe" ]
+ (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.omitted))
+ holy_name_probe_dates
+
+(* A REAL asymmetry this test found between the two shapes, kept explicit
+ rather than papered over by forcing one uniform expectation on both --
+ RG 109(a)'s privilege is textually "of a SUNDAY" (docs/research/rules-
+ register.md §4), read here via {!PE.is_sunday_slug}'s own substring test:
+ the Sunday shape's slug ("ef-holy-name-sunday") carries the marker and
+ the fallback shape's ("ef-holy-name") genuinely does not, because 2
+ January genuinely is not a Sunday. So a losing Holy-Name-Sunday is
+ PRIVILEGED (RG109(a)) and survives RG111(a)'s "I class: none save one
+ privileged" cap when it loses to a higher day; a losing Holy-Name-
+ fallback has no RG109(a)-(f) category to claim (not a Sunday, not I
+ class, none of (c)-(f) either) and so is only ORDINARY -- which
+ RG111(a)'s own cap admits NONE of on a I-class day. Both outcomes are
+ individually correct readings of RG 109/111's own closed lists, not a
+ defect in either shape -- first observed as a test failure while writing
+ this pair (the original version of this test asserted one Privileged
+ commemoration for BOTH shapes uniformly and reddened here), traced to
+ this citation rather than forced to pass. *)
+let test_class1_feast_inside_holy_name_window_end_to_end () =
+ List.iter
+ (fun (label, date, expect_privileged_commemoration) ->
+ let holy_name = of_temporal date in
+ let day_ctx = ctx date in
+ let synthetic_class1 =
+ cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-synthetic-class1-probe-hn"
+ in
+ let resolution = P.resolve real_rules day_ctx ~temporal:holy_name ~sanctoral:[ synthetic_class1 ] in
+ Alcotest.(check string) (label ^ ": the I-class candidate is observed, NOT Holy Name (RG91 e11 beats e14-movable)")
+ "ef-synthetic-class1-probe-hn" (S.to_string resolution.P.observed.P.cel.Cel.slug);
+ Alcotest.(check (list string)) (label ^ ": nothing transfers") []
+ (List.map (fun c -> S.to_string c.P.cel.Cel.slug) resolution.P.deferred);
+ if expect_privileged_commemoration then
+ match resolution.P.commemorations with
+ | [ (c, P.Privileged) ] ->
+ Alcotest.(check string)
+ (label ^ ": Holy Name itself is privileged-commemorated (RG109(a)), neither omitted nor transferred")
+ (S.to_string holy_name.P.cel.Cel.slug) (S.to_string c.P.cel.Cel.slug)
+ | other ->
+ Alcotest.failf "%s: expected exactly one Privileged commemoration (Holy Name), got %d" label
+ (List.length other)
+ else
+ Alcotest.(check int)
+ (label ^ ": RG111(a) admits none -- Holy Name's own commemoration here is Ordinary \
+ (no RG109(a)-(f) category applies to a non-Sunday loser), and a I-class day admits \
+ only a Privileged one")
+ 0 (List.length resolution.P.commemorations))
+ [ ("Sunday shape (2026-01-04)", mk 2026 1 4, true);
+ ("fallback shape (2029-01-02)", T.holy_name_fallback_date 2029, false) ]
+
(* 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
@@ -1681,6 +1778,14 @@ let suite =
"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
+ "RG112(a) + RG91 e14 \"primum mobilia\": Holy Name of Jesus excludes a synthetic fixed Lord \
+ candidate entirely, both shapes (Sunday + fallback)"
+ `Quick test_holy_name_excludes_synthetic_lord_end_to_end;
+ Alcotest.test_case
+ "a synthetic I-class candidate inside Holy Name's own window is observed and does not \
+ transfer, both shapes (Sunday + fallback)"
+ `Quick test_class1_feast_inside_holy_name_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 23a817d..f341533 100644
--- a/test/test_temporal_ef.ml
+++ b/test/test_temporal_ef.ml
@@ -272,6 +272,105 @@ let test_holy_family_no_gap_year () =
Alcotest.(check string) "2025 (6 Jan a Monday): 12 Jan" "2025-01-12"
(D.to_iso8601 (T.holy_family_sunday 2025))
+(* RG 17(a) (Caput III, "De Dominicis"; full text on [T.holy_name_sunday]'s
+ own citation), Sunday shape: "festum Ss.mi Nominis Iesu, celebrandum
+ dominica quae occurrit a die 2 ad 5 ianuarii". Deliberately NOT part of
+ [T.named], same reason as Holy Family (that test's own comment): the
+ slug/rank/colour this branch produces for a plain Sunday 2-5 January
+ already match what [T.sunday_slug]'s Christmastide branch always
+ computed; only [subject]/[names] change. *)
+let test_holy_name_sunday_shape () =
+ (* 2026: 4 January is the Sunday (independently cross-checked against
+ `date -d 2026-01-04 +%u` = 7). Oracle-corroborated: missalemeum's own
+ 2026-01-04 row is rank 2, white, title "Holy Name of Jesus" (the task
+ brief's own example). *)
+ Alcotest.(check string) "2026: slug is the ordinary Holy-Name-window Sunday key" "ef-holy-name-sunday"
+ (slug_of (d 2026 1 4));
+ Alcotest.(check string) "2026: II class" "class-2" (rank_of (d 2026 1 4));
+ Alcotest.(check string) "2026: white" "white" (colour_of (d 2026 1 4));
+ Alcotest.(check bool) "2026: subject is Lord (RG 91 entry 14, \"Festa Domini II classis\")" true
+ (subject_of (d 2026 1 4) = Sub.Lord);
+ Alcotest.(check (option string)) "2026: Latin name from the calendarium/propers, verbatim"
+ (Some "Sanctissimi Nominis Iesu") (name_la_of (d 2026 1 4));
+ 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 4)).Cel.names en);
+ (* Negative controls: every OTHER day in the 2-5 January window that year
+ stays [subject Temporal] -- the override is scoped to exactly the one
+ Sunday [T.holy_name_sunday] names, not the whole window. *)
+ List.iter
+ (fun dd ->
+ Alcotest.(check bool) (Printf.sprintf "2026-01-%02d: NOT Lord (not the Sunday)" dd) true
+ (subject_of (d 2026 1 dd) = Sub.Temporal))
+ [ 2; 3; 5 ];
+ (* 2027: 3 January is the Sunday (`date -d 2027-01-03 +%u` = 7) -- the
+ oracle window's own second instance of the Sunday shape. missalemeum's
+ 2027-01-03 row independently corroborates: rank 2, white, title "Holy
+ Name of Jesus". *)
+ Alcotest.(check bool) "2027: 3 January is a Sunday" true (D.weekday (d 2027 1 3) = D.Sun);
+ Alcotest.(check string) "2027: same slug" "ef-holy-name-sunday" (slug_of (d 2027 1 3));
+ Alcotest.(check bool) "2027: still subject Lord" true (subject_of (d 2027 1 3) = Sub.Lord);
+ (* Cross-checked against [T.holy_name_sunday] itself, independent of the
+ literal dates above. *)
+ Alcotest.(check (option string)) "2026: T.holy_name_sunday agrees" (Some "2026-01-04")
+ (Option.map D.to_iso8601 (T.holy_name_sunday 2026));
+ Alcotest.(check (option string)) "2027: T.holy_name_sunday agrees" (Some "2027-01-03")
+ (Option.map D.to_iso8601 (T.holy_name_sunday 2027))
+
+(* RG 17(a)'s own fallback clause, "(secus die 2 ianuarii)" -- the calendarium
+ confirms it in its own words: "vel, ea deficiente, die 2 ianuarii:
+ Sanctissimi Nominis Iesu, II classis". Unlike Holy Family's 7-13 January
+ window, this narrower 2-5 January window CAN be empty of a Sunday --
+ 3,619 of the 8,417 years in [1583, 9999] -- and colitur emitted NO Holy
+ Name office at all on any of them before this fix (register §6.2's own
+ account of the gap this closes). *)
+let test_holy_name_fallback_shape () =
+ (* 2029: no Sunday falls 2-5 January (independently cross-checked: `date -d
+ 2029-01-0{2,3,4,5} +%u` are 2,3,4,5 -- Tuesday through Friday, none a
+ 7). The task brief's own reported example: "2029-01-02
+ ef-christmas-1-tuesday class-4 white (no Sunday in window: feast
+ MISSING)" -- that is the pre-fix behaviour this test pins the fix
+ against. *)
+ Alcotest.(check (option string)) "2029: no Sunday in the window" None
+ (Option.map D.to_iso8601 (T.holy_name_sunday 2029));
+ Alcotest.(check string) "2029: slug is the fallback key" "ef-holy-name" (slug_of (d 2029 1 2));
+ Alcotest.(check string) "2029: II class" "class-2" (rank_of (d 2029 1 2));
+ Alcotest.(check string) "2029: white" "white" (colour_of (d 2029 1 2));
+ Alcotest.(check bool) "2029: subject is Lord, same as the Sunday shape" true
+ (subject_of (d 2029 1 2) = Sub.Lord);
+ Alcotest.(check (option string)) "2029: SAME Latin name as the Sunday shape (one feast, two dates)"
+ (Some "Sanctissimi Nominis Iesu") (name_la_of (d 2029 1 2));
+ (* Negative controls: 3, 4 and 5 January stay ordinary Christmastide
+ ferias in a fallback year -- the fallback names 2 January specifically,
+ not "some day near the start of the window". *)
+ List.iter
+ (fun dd ->
+ Alcotest.(check bool) (Printf.sprintf "2029-01-%02d: NOT the feast (ordinary feria)" dd) true
+ (subject_of (d 2029 1 dd) = Sub.Temporal
+ && Sl.to_string (office (d 2029 1 dd)).Cel.slug <> "ef-holy-name"))
+ [ 3; 4; 5 ];
+ (* A second, independently cross-checked fallback year, at the OTHER end
+ of the week (2008: `date -d 2008-01-02 +%A` = Wednesday), so this is
+ not a single-year accident. *)
+ Alcotest.(check (option string)) "2008: no Sunday in the window either" None
+ (Option.map D.to_iso8601 (T.holy_name_sunday 2008));
+ Alcotest.(check string) "2008: same fallback slug" "ef-holy-name" (slug_of (d 2008 1 2))
+
+(* [T.holy_name_sunday]'s own [Some]/[None] split, cross-checked against
+ `date -d` independently for a sample spanning both shapes -- not merely
+ the two years the shape tests above already pin literally, so the
+ boundary itself (not only two hand-picked instances) is exercised. *)
+let test_holy_name_sunday_or_none () =
+ List.iter
+ (fun (y, expected) ->
+ Alcotest.(check (option string))
+ (Printf.sprintf "%d: holy_name_sunday" y)
+ expected
+ (Option.map D.to_iso8601 (T.holy_name_sunday y)))
+ [ (2026, Some "2026-01-04"); (2027, Some "2027-01-03"); (2009, Some "2009-01-04");
+ (2010, Some "2010-01-03"); (2020, Some "2020-01-05"); (2025, Some "2025-01-05");
+ (2006, None); (2007, None); (2008, None); (2018, None); (2029, None) ]
+
(* Regression for a Task 14 Validate finding, reworked (register finding 4):
[named] no longer carries its own week at all -- [temporal] computes it
uniformly via [week] for every day, named or not -- so "a named day inside
@@ -583,6 +682,56 @@ let test_holy_family_anchor_present_and_erosion_is_caught () =
Alcotest.(check string) "sanity: temporal itself is unaffected by the anchors-list erosion"
expected_slug (slug_of expected_date)
+(* Holy Name of Jesus (RG 17(a)), SUNDAY shape -- same structure as Holy
+ Family's own test immediately above, same reasoning (also not one of
+ [T.named]'s outputs). 2026 is a Sunday-shape year (4 January). *)
+let test_holy_name_sunday_anchor_present_and_erosion_is_caught () =
+ let y = 2026 in
+ let expected_date =
+ match T.holy_name_sunday y with
+ | Some d -> d
+ | None -> Alcotest.fail "sanity: 2026 must have a Sunday in the Holy Name window"
+ in
+ let expected_slug =
+ match T.sunday_slug expected_date with
+ | Some s -> s
+ | None -> Alcotest.fail "sanity: holy_name_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));
+ 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)
+
+(* Holy Name of Jesus, FALLBACK shape -- the SAME two properties (presence,
+ erosion caught), for a year with no Sunday in the window. Unlike the
+ Sunday shape above, there is no SECOND independent function (the way
+ [T.sunday_slug] is independent of [T.anchors]) that names "ef-holy-name"
+ for a non-Sunday date -- [expected_slug] is the literal this task chose,
+ cross-checked instead against [T.temporal]'s own output (the function
+ under test) via the final sanity line, same as every other branch of
+ this cross-check, honestly not a second independent source. 2029 has no
+ Sunday 2-5 January (test_holy_name_fallback_shape's own citation). *)
+let test_holy_name_fallback_anchor_present_and_erosion_is_caught () =
+ let y = 2029 in
+ (match T.holy_name_sunday y with
+ | None -> ()
+ | Some _ -> Alcotest.fail "sanity: 2029 must have no Sunday in the Holy Name window");
+ let expected_date = T.holy_name_fallback_date y in
+ let expected_slug = "ef-holy-name" in
+ Alcotest.(check (list string)) "the anchor entry exists for the fallback shape" []
+ (missing_from_anchors ~named_slugs:[ expected_slug ] ~anchors:(anchor_slugs y));
+ 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,
@@ -606,6 +755,9 @@ let suite_extra =
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 "holy name: Sunday shape (RG 17(a))" `Quick test_holy_name_sunday_shape;
+ Alcotest.test_case "holy name: fallback shape (RG 17(a))" `Quick test_holy_name_fallback_shape;
+ Alcotest.test_case "holy name: Sunday-or-none cross-check" `Quick test_holy_name_sunday_or_none;
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;
@@ -620,6 +772,10 @@ let suite_extra =
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 "holy name (Sunday shape) anchor present, erosion caught" `Quick
+ test_holy_name_sunday_anchor_present_and_erosion_is_caught;
+ Alcotest.test_case "holy name (fallback shape) anchor present, erosion caught" `Quick
+ test_holy_name_fallback_anchor_present_and_erosion_is_caught;
Alcotest.test_case "anchors erosion is caught" `Quick test_anchors_erosion_is_caught ]
let suite =