diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-13 12:44:15 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-13 12:44:15 +0200 |
| commit | dc00d1f730b23fa99cd15ff142ecba515a9fe0b7 (patch) | |
| tree | e40f7f003008b33104723a6d8856168bbdea7633 /test | |
| parent | 59fbda87d565ad9b98c2493e9703390cd988609d (diff) | |
| parent | e45ec1ccce68d9edb9655004c21861d6bfb33a9d (diff) | |
| download | colitur-dc00d1f730b23fa99cd15ff142ecba515a9fe0b7.tar.gz colitur-dc00d1f730b23fa99cd15ff142ecba515a9fe0b7.zip | |
Merge branch 'ef-holyname-rg110': the Holy Name of Jesus, RG 110
Builds the Most Holy Name of Jesus with the fallback RG 17(a) and the
calendarium both specify -- 'secus die 2 ianuarii', 'vel, ea deficiente,
die 2 ianuarii' -- which was unimplemented, so in 3619 of the domain's
8417 years a II-class feast did not exist at all. Also retags it as a
feast of the Lord per RG 91 entry 14.
Builds RG 110's inseparable Peter/Paul commemoration ('in numero
orationum computando, pro unica habeantur') across the three calendarium
pairs, and adds the 30 June companion, which was missing from lectio's
source and from missalemeum's output alike -- corroborated in both
photographic scans plus RG 124(c), and added through adjustments.sexp so
the bootstrap's provenance stands.
14627 days change, in four shapes, none unclassified. The register's
earlier 852-day estimate for RG 110 reconciles exactly as the Lent Ember
subset of one shape: 288 + 284 + 280.
RG 110(c)'s ordering was implemented backwards at first and corrected:
'huic orationi additur altera' attaches the companion to the trigger's
own oration, so it follows it, where (a) and (b) attach to the day's and
so precede everything. The 'ante omnes alias commemorationes' half
remains unimplemented and is unreachable by construction -- shape (c)
needs the trigger to lose while still being admitted, which only a
I-class day or a II-class Sunday allows, and both RG 111 caps are one.
Records a fifth way the validation layers can be green while wrong:
nothing anywhere compares commemoration order.
Diffstat (limited to 'test')
| -rw-r--r-- | test/test_differential.ml | 21 | ||||
| -rw-r--r-- | test/test_golden.ml | 121 | ||||
| -rw-r--r-- | test/test_oracle.ml | 105 | ||||
| -rw-r--r-- | test/test_precedence_ef.ml | 235 | ||||
| -rw-r--r-- | test/test_temporal_ef.ml | 156 |
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 = |
