summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/test_differential.ml21
-rw-r--r--test/test_golden.ml121
-rw-r--r--test/test_oracle.ml105
-rw-r--r--test/test_precedence_ef.ml235
-rw-r--r--test/test_temporal_ef.ml156
5 files changed, 610 insertions, 28 deletions
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..0f90ee7 100644
--- a/test/test_golden.ml
+++ b/test/test_golden.ml
@@ -434,11 +434,21 @@ let test_ember_ferias_commemorated_when_impeded () =
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)"
+ RG109(e) mandate. ALSO, ef-holyname-rg110 task: RG 110's own
+ inseparable Peter/Paul commemoration -- "in Officio ... S. Petri semper
+ fit commemoratio S. Pauli" -- fires unconditionally whenever
+ [chair-of-st-peter] is observed, uncapped by RG 111's own admission
+ count, so `+paul` now rides alongside the Ember feria's own
+ commemoration rather than being displaced by it (precedence_ef.ml's own
+ [rg110_additions]). This pin's own [ef-lent-ember-sat:privileged] half
+ is UNCHANGED from before this task -- only [paul:ordinary] is new,
+ confirming RG 110 adds a genuinely SEPARATE, uncapped slot rather than
+ competing for the Ember feria's own privileged one. *)
+ check ~msg:"1902-02-22: Lent Ember Saturday, impeded, MUST be commemorated (RG24) and is privileged (RG109e) -- \
+ AND RG110's Paul companion rides alongside it, uncapped"
1902 2 22
"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=[]"
+ comms=[ef-lent-ember-sat:privileged,paul:ordinary] in=- out=[]"
let test_iv_class_ferias_never_commemorated () =
(* 2026-07-22: an ordinary Time-after-Pentecost feria (Class4, RG91 entry
@@ -634,6 +644,98 @@ 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=[]"
+
+(* RG 110 (docs/research/rules-register.md §4, Caput XIV), fix round 1
+ (coordinator findings F1/F2): two golden pins neither existed before this
+ round -- F2 named 30 June's own total absence of golden coverage
+ directly ("no golden pin on any 30 June date"), and F1 named the
+ shape-(c) collision as a real day this suite otherwise never exercises
+ end-to-end against real data (both new [admit_cases] rows are synthetic
+ candidates, not `Calendar.day` over the real committed layer).
+
+ *** BOTH PINS ARE BLIND TO COMMEMORATION ORDER -- STATED HONESTLY, NOT
+ CLAIMED AS PROOF OF F1's OWN FIX ***
+
+ [describe]'s own `comms` field (this file's header, above) sorts the
+ commemoration list alphabetically before rendering
+ (`List.sort compare`) -- deliberately, so a golden pin's own STRING
+ comparison is not accidentally order-sensitive where nothing textual
+ requires it to be. That means this file, like the differential and the
+ oracle's own [identity_diff] (test_oracle.ml), CANNOT see whether
+ [rg110_additions] emits `chair-of-st-peter, paul` or `paul,
+ chair-of-st-peter` -- both sort to the same string. The ONLY place in
+ this codebase's test suite that asserts commemoration ORDER at all is
+ `test_precedence_ef.ml`'s own [admit_cases] table (`Alcotest.(check
+ (list string))`, not a sorted string) -- see precedence_ef.ml's own
+ [rg110_additions] comment for the citation this uses to fix the order,
+ and CLAUDE.md's "know what each layer cannot see" section for this as a
+ permanent, structural limit, not a gap this task's own pins happened not
+ to close. These two pins prove PRESENCE and IDENTITY of both
+ commemorations on a real, `Calendar.day`-resolved date -- genuine
+ ground no other pin in this file currently covers -- not order. *)
+let test_rg110_june_30_2026 () =
+ check ~msg:"2026-06-30: RG 110's 30-June direction -- In Commemoratione Sancti Pauli Apostoli observed, \
+ +commemoration-of-st-peter (data/ef/adjustments.sexp's own `Add` directive) the sole \
+ commemoration -- missalemeum shows neither (M19, verdict colitur)"
+ 2026 6 30
+ "2026-06-30 tuesday season=time-after-pentecost week=5 slug=in-commemoratione-sancti-pauli-apostoli \
+ rank=class-3 colour=red subject=saint comms=[commemoration-of-st-peter:ordinary] in=- out=[]"
+
+let test_rg110_shape_c_2004 () =
+ check ~msg:"2004-02-22: RG 110 shape (c) -- Chair of St Peter itself loses the day to an ordinary \
+ (Septuagesima, not I-class) II-class Sunday and is admitted only as a commemoration under \
+ RG 111(b)'s own rank floor; Paul rides along too, uncapped (`date -d 2004-02-22 +%A` = \
+ Sunday, independently confirmed)"
+ 2004 2 22
+ "2004-02-22 sunday season=septuagesima week=3 slug=ef-septuagesima-sunday-3 rank=class-2 colour=violet \
+ subject=temporal comms=[chair-of-st-peter:ordinary,paul:ordinary] 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 +895,19 @@ 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
+ "RG110: 30 June, In Commemoratione S. Pauli Apostoli + commemoration-of-st-peter (2026-06-30)" `Quick
+ test_rg110_june_30_2026;
+ Alcotest.test_case
+ "RG110 shape (c): Chair of St Peter admitted only as a commemoration of an ordinary Sunday, Paul \
+ rides along (2004-02-22)"
+ `Quick test_rg110_shape_c_2004;
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 35d4501..58ceb26 100644
--- a/test/test_oracle.ml
+++ b/test/test_oracle.ml
@@ -599,23 +599,23 @@ let m10_dates = [ "2026-11-30"; "2027-11-30"; "2026-12-08"; "2027-12-08" ]
own full note and the task report. *)
let m11_dates = [ "2026-12-26"; "2026-12-28"; "2027-12-27"; "2027-12-28" ]
-(* M12 -- RG 110, primary text: "In Officio et Missa S. Petri semper fit
- commemoratio S. Pauli, et vicissim" -- in the Office and Mass of EITHER
- Peter or Paul, a commemoration of the OTHER is ALWAYS made (the
- "inseparable commemoration"), uncapped by RG 111's ordinary admission
- count ("in numero orationum computando, pro unica habeantur" -- counted
- as one for counting purposes, i.e. it rides alongside whatever RG 111
- already admits). Unimplemented in this codebase -- confirmed by the one
- row in this window where it is visible: 22 February 2027 (Chair of
- St Peter) shows only the privileged Lent feria commemorated in colitur,
- while missalemeum shows that AND "St. Paul". A genuine, primary-cited
- feature gap -- verdict missalemeum, register §6 open item. Real but
- NARROW: every other Peter/Paul office in this window (29 June, both
- years; 1 August M4-adjacent dates were checked and found clean) either
- had no room for the extra commemoration to become visible against this
- comparator's axes, or fell on a day this predicate does not otherwise
- touch -- see the task report for the specific check. *)
-let m12_dates = [ "2027-02-22" ]
+(* M12 -- CLOSED (ef-holyname-rg110 task): RG 110 is now built
+ (precedence_ef.ml's own [rg110_additions]/[rg110_companion_slug]), and
+ this window's one visible instance -- 22 February 2027 -- no longer
+ diverges from missalemeum on [Comm_count] at all: colitur now shows the
+ privileged Lent feria's own commemoration AND "St. Paul", same COUNT
+ (two) as missalemeum. Removed rather than kept as a dead 0-row citation,
+ the SAME shape M17's own removal already established (this file's own
+ header): the count pin now enforces that this exact divergence cannot
+ silently reappear. NOT closed with zero remaining difference, though --
+ see M15's own widened date list immediately below: once count agrees,
+ IDENTITY comparison is reached for the first time on this date, and
+ trips a SEPARATE, pre-existing, unrelated comparator limit (one of
+ colitur's two admitted commemorations, [ef-lent-2-monday], is
+ TEMPORAL-origin and carries no English name) -- a newly-exposed
+ instance of M15's own gap, not a new one, folded into M15's date list
+ rather than kept here under RG 110's own citation, which no longer
+ explains ANY part of this row's remaining diff. *)
(* M13 -- OPEN, NOT adjudicated (the brief's own explicit permission,
"say so as an open item rather than absorbing it"). 19 March 2027: St
@@ -647,13 +647,13 @@ let m13_dates = [ "2027-03-19" ]
either engine is wrong about: a genuine LIMIT of this comparator, honestly
counted rather than silently passed (the brief's own explicit
instruction: "a day whose commemoration cannot be matched must be
- visibly counted and reported, not quietly passed"). On each of these 19
+ visibly counted and reported, not quietly passed"). On 19 of these 20
rows, colitur's own admitted commemoration is TEMPORAL-origin (an
impeded Advent/Lent feria) -- Rite_ef.Temporal_ef's own [build]
(temporal_ef.ml) never sets a celebration's [names] field, so there is no
English string on colitur's side to compare against missalemeum's title
text at all (this file's own header, "THE MAPPING, AND EXACTLY WHAT IT
- CANNOT RESOLVE"). Every one of these 19 oracle titles reads as a plain
+ CANNOT RESOLVE"). Every one of those 19 oracle titles reads as a plain
prose description of the SAME temporal ferial slot colitur independently
computes for that date (e.g. 2 Dec 2026: oracle "Feria IV after I Sunday
of Advent" against colitur's own [ef-advent-1-wednesday]; 4 Dec 2027:
@@ -664,12 +664,25 @@ let m13_dates = [ "2027-03-19" ]
unresolved. Fixing this at the ROOT (giving every temporal-cycle
candidate an English name) is a data/lectionary-bootstrap task, not
something this comparator can do for itself -- register §6, "RG 113
- tie-break" section's own neighbour, records it as an open item. *)
+ tie-break" section's own neighbour, records it as an open item.
+
+ ADDED, ef-holyname-rg110 task: 2027-02-22 -- the closed M12's own date,
+ above. NOT the same shape as the other 19 (colitur admits TWO
+ commemorations there, not one -- [paul], sanctoral-origin and
+ resolvable, and [ef-lent-2-monday], temporal-origin and not): the
+ comparator's own [identity_diff] bails out to [Comm_identity_unresolved]
+ the moment ANY admitted commemoration is unresolvable, regardless of how
+ many others resolve cleanly (test_oracle.ml's own [identity_diff],
+ "[List.exists] ... name = None"), so this row reaches the identical
+ diagnosis for the identical structural reason, once RG 110 stopped
+ [Comm_count] from masking it. Genuinely newly-EXPOSED by this task's own
+ fix, not newly-CAUSED by it: the underlying gap (no English name for a
+ temporal-cycle candidate) is exactly M15's own pre-existing one. *)
let m15_dates =
[ "2026-02-24"; "2026-03-19"; "2026-03-25"; "2026-12-02"; "2026-12-03"; "2026-12-04";
- "2026-12-07"; "2026-12-11"; "2026-12-21"; "2027-02-24"; "2027-12-02"; "2027-12-03";
- "2027-12-04"; "2027-12-06"; "2027-12-07"; "2027-12-11"; "2027-12-13"; "2027-12-16";
- "2027-12-21" ]
+ "2026-12-07"; "2026-12-11"; "2026-12-21"; "2027-02-22"; "2027-02-24"; "2027-12-02";
+ "2027-12-03"; "2027-12-04"; "2027-12-06"; "2027-12-07"; "2027-12-11"; "2027-12-13";
+ "2027-12-16"; "2027-12-21" ]
(* M16 -- Task B: a genuine IDENTITY mismatch, both sides otherwise agreeing
on rank/colour/count/presence. 27 March 2026 (Friday of Passion Week):
@@ -766,6 +779,49 @@ let m16_dates = [ "2026-03-27" ]
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. *)
+(* M19 -- ef-holyname-rg110 task: RG 110's OTHER direction, 30 June (this
+ window's two instances -- both years -- of
+ [in-commemoratione-sancti-pauli-apostoli], data/ef/adjustments.sexp's
+ own `Add commemoration-of-st-peter` directive; see that file's own
+ citation for the full scan quote). Unlike 22 February above,
+ missalemeum's own row for both dates shows an EMPTY commemorations array
+ ("St. Peter" absent, and not even listed as "displaced" -- nothing
+ competes for the slot on either side, so there is no candidate for
+ missalemeum's own engine to have rejected, only one it never
+ constructed): missalemeum has the SAME gap this task's own scan found in
+ lectio's source data (`~/git/projects/lectio/internal/caldata/
+ tridentine-calendar.ini` has no 06-30 companion entry either) -- a
+ genuine feature gap, verdict colitur, the SAME shape as M1/M3/M8/M10
+ above (each already "missalemeum does not implement X"), not a
+ citation-count coincidence: RG 110's own text is unconditional ("in
+ Officio et Missa S. Petri semper fit commemoratio S. Pauli, ET
+ VICISSIM"), and the calendarium's own June table states the SAME
+ pattern already built for the other two pairs, word for word.
+
+ IDENTITY-GATED, fix round 1 (coordinator finding F2): the predicate
+ below now also requires [c.c_commemorations] to be EXACTLY the single
+ entry [commemoration-of-st-peter] -- not merely that some [Comm_presence]
+ diff exists on these two dates. Proved necessary, not decorative: the
+ reviewer added a SECOND, fabricated `Add` directive on 30 June (a
+ `bogus-fabricated-companion` slug) to a scratch copy of
+ data/ef/adjustments.sexp and found the whole suite stayed green with the
+ pre-fix, presence-only predicate -- the exact C6/C14 failure mode (a
+ Layer-C-style predicate that pins a date and a diff SHAPE but not WHICH
+ candidate). This matters more here than for any other entry in this
+ file: `commemoration-of-st-peter` is corroborated by NEITHER oracle (it
+ rests on a scan reading alone, data/ef/adjustments.sexp's own citation),
+ so this allow-list predicate is the only place in the whole suite that
+ could assert what colitur actually emits here -- and, pre-fix, asserted
+ nothing about it. Mirrors C16's own identity guard
+ (`String.equal c.slug "ef-holy-name"`, test_differential.ml) at the
+ analogous decision point in this file. *)
+let m19_dates = [ "2026-06-30"; "2027-06-30" ]
+
+let m19_commemoration_matches (c : colitur_row) =
+ match c.c_commemorations with
+ | [ (slug, _, _, _) ] -> String.equal slug "commemoration-of-st-peter"
+ | _ -> false
+
let layer_m_reason (c : colitur_row) (o : oracle_row) diffs =
if diffs = [] then None
(* M1's own subset widened (this task): colitur's observed day on both
@@ -795,7 +851,6 @@ let layer_m_reason (c : colitur_row) (o : oracle_row) diffs =
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 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
@@ -818,6 +873,8 @@ let layer_m_reason (c : colitur_row) (o : oracle_row) diffs =
&& subset diffs [ Comm_identity_mismatch; Observed_identity_unresolved ]
then Some "M16"
else if diffs = [ Observed_identity_unresolved ] then Some "M18"
+ else if List.mem c.c_date m19_dates && diffs = [ Comm_presence ] && m19_commemoration_matches c then
+ Some "M19"
else None
(* ---------------------------------------------------------------------- *)
diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml
index 4ee2fd8..ed9a933 100644
--- a/test/test_precedence_ef.ml
+++ b/test/test_precedence_ef.ml
@@ -1016,6 +1016,43 @@ let sixtus =
UNIVERSALLY), so the two values coinciding is the faithful shape, not a
simplification that dodges the new parameter. The last two rows are
where they genuinely differ -- see their own comment. *)
+(* RG 110 (§4, Caput XIV): candidates built with the REAL trigger/companion
+ slugs {!PE.rg110_companion_slug} matches on -- unlike every other
+ [admit_cases] candidate above, which is deliberately built with a
+ SYNTHETIC slug {!PE.band}'s 28 branches (and this file's own RG 110
+ lookup) cannot recognise, these three rows need [PE.admit] to recognise
+ the slug itself, so the real ones are used directly. [~status:
+ Cel.Commemoration_only] on the companions matches the real data shape
+ (data/ef/sanctoral.sexp's own [peter]/[paul];
+ data/ef/adjustments.sexp's own [commemoration-of-st-peter]) even though
+ [admit] itself never reads [status] -- only [Precedence.resolve]'s own
+ partition does, upstream of [admit] -- kept for a reader's sake, not a
+ test requirement. *)
+let chair_of_st_peter_synth = cand ~rank:V.Class2 "chair-of-st-peter"
+let paul_companion_synth = cand ~rank:V.Class3 ~status:Cel.Commemoration_only "paul"
+
+(* An unrelated third commemoration, sorting AFTER both members of the RG110
+ pair, so that "spliced immediately after its trigger" and "appended at the
+ end" give different answers. Added by the fix-round re-review (G1): with
+ only the pair present the two are indistinguishable, and the re-review
+ proved an append-at-end variant passed the whole suite green. Class3
+ Commemoration_only so it cannot win the day; its slug is deliberately not
+ one {!PE.rg110_companion_slug} recognises. *)
+let unrelated_comm_synth =
+ cand ~rank:V.Class3 ~status:Cel.Commemoration_only "zzz-unrelated-commemoration"
+
+(* Any real Lent/Ember feria's own shape (a Class3, PRIVILEGED, temporal-
+ origin candidate) -- {!PE.rg110_companion_slug} does not match this slug,
+ so it is a neutral bystander in [admit]'s own normal (pre-RG110) contest,
+ exactly like [privileged_lo] elsewhere in this table, just kept
+ separately named so this block's own rows are self-contained and do not
+ depend on a binding declared for an unrelated purpose above. Given the
+ WORSE (higher) precedence int than [paul_companion_synth] would need to
+ win the day's one "other II class" slot on its own dignity ALONE -- the
+ whole point of the row below is that RG 110 does not care who wins that
+ contest; it adds Paul regardless. *)
+let lent_feria_synth = (cand ~origin:P.Temporal ~rank:V.Class3 "ef-lent-2-monday-synth", P.Privileged, 180)
+
let admit_cases =
[ (* RG 111 (§4): "I class: none save one privileged." *)
( "I-class day, only an ordinary commemoration due -> none admitted",
@@ -1109,7 +1146,98 @@ let admit_cases =
admitted",
lord_winner, lord_winner,
[ po sixtus P.Ordinary ],
- [ "pope-sixtus-ii-felicissimus-and-agapitus-martyrs" ] )
+ [ "pope-sixtus-ii-felicissimus-and-agapitus-martyrs" ] );
+ (* RG 110, shape (a)/(b): [chair-of-st-peter] observed, a competing
+ PRIVILEGED Lent feria also due for the day's single "other II class"
+ slot (RG 111(c) itself has no privilege-override clause -- unlike the
+ Sunday branch above -- so pure precedence order decides that slot on
+ its own, and [lent_feria_synth]'s own 180 beats
+ [paul_companion_synth]'s 999). Without RG 110, this is exactly
+ precedence_ef.ml's own measured defect (3,533 of 8,417 domain-wide
+ years, this task's own report): Paul loses the slot outright and is
+ never admitted at all. With RG 110, Paul is added ADDITIONALLY,
+ PREPENDED ahead of the feria -- RG 110(a)/(b)'s own text ("oratio
+ alterius Apostoli additur ... orationi diei", the companion's oration
+ added to the DAY's own oration) governs here, not (c)'s "ante omnes
+ alias commemorationes" (CORRECTED, fix round 1, coordinator finding
+ F1: this comment previously cited (c) for both shapes uniformly --
+ see [rg110_additions]'s own comment in precedence_ef.ml for why the
+ two shapes need, and get, DIFFERENT orderings, not the same one for
+ different reasons) -- both admitted, not one displacing the other. *)
+ ( "RG110 (a)/(b): chair-of-st-peter observed, Paul rides alongside a \
+ competing privileged Lent feria, uncapped and prepended",
+ chair_of_st_peter_synth, chair_of_st_peter_synth,
+ [ (paul_companion_synth, P.Ordinary, 999); lent_feria_synth ],
+ [ "paul"; "ef-lent-2-monday-synth" ] );
+ (* RG 110, shape (c): [chair-of-st-peter] itself LOSES the day outright
+ (observed is some unrelated II-class Sunday) but is admitted, on its
+ OWN Class2 dignity, as RG 111(b)'s single ordinary commemoration --
+ exactly the domain-wide 593-year pattern this task's own report
+ measures. RG 110(c)'s own text, word for word ("quoties vero oratio
+ unius Apostoli addenda est ad modum commemorationis, HUIC ORATIONI
+ ADDITUR ALTERA immediate, ante omnes alias commemorationes"): the
+ companion (Paul, Class3, no standing of its OWN for this Sunday's
+ rank-floored slot) rides along anyway, uncapped -- ADDED TO THE
+ TRIGGER'S own oration ("huic orationi additur altera"), i.e.
+ FOLLOWING it, not preceding it. CORRECTED, fix round 1 (coordinator
+ finding F1): this row originally asserted `["paul";
+ "chair-of-st-peter"]` -- the companion BEFORE the trigger -- citing
+ the SAME "ante omnes alias commemorationes" clause that actually
+ governs the PAIR's position relative to OTHER, unrelated
+ commemorations, not the pair's own internal order. "Huic orationi"
+ (dative, "to THIS oration") refers back to "oratio unius Apostoli",
+ the trigger's own oration -- so the companion is added TO it,
+ following, not leading. Reproduced by mutation: reverting
+ [rg110_additions]'s shape-(c) branch back to a uniform prepend
+ reddens exactly this row's own expected order, nothing else. *)
+ ( "RG110 (c): chair-of-st-peter admitted merely AS a commemoration \
+ (loses the day to an ordinary II-class Sunday), Paul rides along \
+ AFTER it (RG110(c): \"huic orationi additur altera\")",
+ cand ~rank:V.Class2 "ef-some-other-sunday", cand ~rank:V.Class2 "ef-some-other-sunday",
+ (* A third, unrelated candidate is OFFERED here deliberately, and is
+ correctly NOT admitted -- which is itself the point.
+
+ The fix-round re-review asked for a third commemoration so that
+ "spliced IMMEDIATELY after its own trigger" (RG110(c)'s own word)
+ could be told apart from "appended at the end of the list": with a
+ single-member result the two coincide, and it proved an
+ append-at-end variant passed the whole suite green.
+
+ Attempting it establishes something better than the test it asked
+ for: on this day-shape a third commemoration is RUBRICALLY
+ IMPOSSIBLE, so the distinction is unobservable BY CONSTRUCTION, not
+ merely unobserved. Shape (c) requires the trigger to LOSE the day
+ while still being admitted -- and a Class2 trigger can only lose to
+ a I-class day (RG 111(a): one privileged commemoration) or a
+ II-class Sunday (RG 111(b): one, and "scilicet de festo II
+ classis"). Both caps are ONE. Anything a third candidate could be
+ is either outranked by the trigger or excluded by the rank floor;
+ here [zzz-unrelated-commemoration] is Class3 and RG 111(b) drops
+ it, leaving the pair alone.
+ That is why the re-review's own domain sweep found shape (c) never
+ co-occurring with a third commemoration in 8 417 years: not an
+ accident of the data, a consequence of the rubric.
+
+ So RG110(c)'s "ante omnes alias commemorationes" half has no
+ reachable witness in the EF's real rubrical space, and the
+ adjacency this row asserts is exercised only against the pair
+ itself. The append-at-end variant therefore remains
+ indistinguishable from the splice on any input the engine can
+ actually construct -- recorded in the register rather than papered
+ over with a synthetic row that proves a shape no rubric permits. *)
+ [ po chair_of_st_peter_synth P.Ordinary;
+ (paul_companion_synth, P.Ordinary, 999);
+ po unrelated_comm_synth P.Ordinary ],
+ [ "chair-of-st-peter"; "paul" ] );
+ (* Defensive: a trigger slug is [observed], but its companion is simply
+ not present among [comms] at all (a data gap, or -- as this exact
+ shape was for 30 June before this task's own [Add
+ commemoration-of-st-peter] directive -- an upstream omission).
+ {!PE.rg110_find_companion} must return [None] gracefully, not raise;
+ the day's own (empty, here) normal result is unaffected. *)
+ ( "RG110 defensive: chair-of-st-peter observed, no companion present in \
+ comms at all -> no crash, nothing added",
+ chair_of_st_peter_synth, chair_of_st_peter_synth, [], [] )
]
(* Order independence (brief: "the admitted set must not depend on input
@@ -1426,6 +1554,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 +1906,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 =