summaryrefslogtreecommitdiff
path: root/test/test_differential.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-22 16:40:07 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-22 16:40:24 +0200
commit60e87914718dd2c8e69bb89d8cc48d6bef9bfc74 (patch)
tree814f451ac97e61d8ab83a2e24877b1b5223734db /test/test_differential.ml
parent219c0ca6884a634e83293c62b27cca990fa25854 (diff)
downloadcolitur-60e87914718dd2c8e69bb89d8cc48d6bef9bfc74.tar.gz
colitur-60e87914718dd2c8e69bb89d8cc48d6bef9bfc74.zip
feat(ef): implement the calendarium's bissextile February shift
The Missale Romanum's calendarium footnote for February (LT.txt:5011-5014, scan-corroborated scan2.txt:3050-3058): in a leap year the sixth kalends of March (24 February) is doubled rather than a 29th day appended, so St Matthias moves 24->25 February and St Gabriel of Our Lady of Sorrows 27->28, with 24 February itself carrying no fixed office that year. data/ef/expected-divergences-lms.sexp entry L3 tracked this as an open gap. Implemented as the rubric's own general mechanism (every fixed entry from 24 through 28 February shifts one civil day later in a leap year), not as "move these two named saints": the two readings are indistinguishable on shipped data (nothing else is fixed in that window) and diverge only for a future --overlay entry in the same range, which the mechanism reading covers correctly and a two-saints special case would not. This project has already been bitten once (RG 16(a)) by a rule implemented against shipped data's coincidental shape rather than the rubric itself. Kernel stays rite-agnostic: Rite.t gains a fixed_key field (Date.t -> (int * int) option), the same seam easter already establishes, read only by Layer.on_date's FIXED half -- the movable half (Easter_offset/Nth_weekday) is untouched. Default is the identity mapping, an optional argument on on_date, so every existing caller and every rite that supplies nothing is byte-identical to before this field existed. Date.is_leap is exposed in date.mli (already existed in date.ml) so the rite reads the kernel's own single-sourced leap-year definition. The EF implementation lives in Rite_ef.Temporal_ef.bissextile_fixed_key, fully cited, wired into Rite_ef.context. Blast radius, measured over the full 1583-9999 domain (Calendar.year run twice per civil year, real fixed_key vs identity, every day diffed): all 2,041 leap years in the domain show a visible change for each saint; 6,983 individual liturgical days change total, zero unclassified, across four shapes (Matthias vacates 24 Feb in 1,803 years / occupies 25 Feb in 1,811; Gabriel vacates 27 Feb in 1,699 / occupies 28 Feb in 1,670). L3's own prior estimate (1,650 years, observed-outright only) is confirmed close on the same definition (1,677 measured); the broader observed-or- commemorated count is higher, not lower, showing the narrow estimate undercounted visible impact rather than overcounting it. The lectio differential (2005-2050 fixture) gains a new cited entry, C40 in data/ef/expected-divergences.sexp: lectio implements no such shift, so every leap year in its window now diverges on the two shifted days (21 rows, not the naive 44 -- the comparator never compares commemorations, so a side where the shifted saint has zero comparator-visible footprint on both engines produces no row). The LMS Ordo's 2023-2024 fixture -- the window L3 was originally found in -- now matches on the Creed comparison; its formulary-override bucket count is corrected 182->181 (24 February leaves the counted Proper population, becoming Own_slug-sourced). The missalemeum oracle fixtures (2026-2027, 2038, 2035) cover no leap year and are unaffected. L3 is closed: converted from an active allow-list record to a prose closure paragraph (L1's own precedent), citation preserved verbatim, fix and measured blast radius recorded. The id-list assertion narrows to [L4] alone, and the now-dead "2024-02-24" -> "L3" date mapping is removed. Six golden values pinned across four new test cases: Matthias in a leap year (both the vacated and occupied sides) and a common-year control; Gabriel likewise, deliberately choosing a leap year where he is admitted only as an ordinary commemoration rather than winning outright, a different shape from Matthias's. Mutation-tested: forcing fixed_key back to identity reddens exactly five test cases -- the differential's Layer C count-pin (C40 drops to 0 actual vs 21 expected), the LMS Ordo's Creed and formulary-bucket checks, and both new golden pins -- confirming the tests actually exercise the fix. Reverted; dune test, the exhaustive sweep, and make check are all green.
Diffstat (limited to 'test/test_differential.ml')
-rw-r--r--test/test_differential.ml32
1 files changed, 32 insertions, 0 deletions
diff --git a/test/test_differential.ml b/test/test_differential.ml
index 8c1dd8d..2f63df6 100644
--- a/test/test_differential.ml
+++ b/test/test_differential.ml
@@ -1096,6 +1096,36 @@ let rg33_vigil_slugs =
[ "vigil-of-st-lawrence"; "vigil-of-the-nativity-of-st-john-the-baptist";
"vigil-of-sts-peter-paul" ]
+(* C40 -- the calendarium's own bissextile (leap-year) footnote (February,
+ docs/research/LT.txt:5011-5014; see rite_ef/temporal_ef.ml's
+ [bissextile_fixed_key] for the full citation and mechanism), implemented
+ 2026-08-22. lectio builds no such shift at all (confirmed directly
+ against the fixture: Matthias sits at 24 February and Gabriel of Our Lady
+ of Sorrows at 27 February in EVERY leap year the 2005-2050 fixture
+ covers, 11 of them), so every leap year now diverges on the two shifted
+ civil days: colitur vacates 24/27 February (Matthias/Gabriel move away)
+ and occupies 25/28 February instead, while lectio keeps the pre-shift
+ assignment on both.
+
+ Four shapes, one mechanism -- OLD position vacated (colitur no longer
+ matches lectio's still-there Matthias/Gabriel) and NEW position occupied
+ (colitur now shows a saint lectio's own unmoved date does not) -- each
+ checked by NAME on both sides, the same identity discipline C39 and
+ C6/C14 already apply, not merely a diff-set shape: a coincidental
+ same-shape diff on an unrelated 24/25/27/28 February slug would not match
+ any of the four literal-slug conjuncts below. *)
+let is_bissextile_shift_row (l : row) (c : row) ~month ~day =
+ month = 2
+ && Date.is_leap (year_of_date c.date)
+ && ((day = 24 && String.equal l.slug "matthias" && not (String.equal c.slug "matthias"))
+ || (day = 25 && String.equal c.slug "matthias" && not (String.equal l.slug "matthias"))
+ || (day = 27
+ && String.equal l.slug "gabriel-of-our-lady-of-sorrows"
+ && not (String.equal c.slug "gabriel-of-our-lady-of-sorrows"))
+ || (day = 28
+ && String.equal c.slug "gabriel-of-our-lady-of-sorrows"
+ && not (String.equal l.slug "gabriel-of-our-lady-of-sorrows")))
+
let layer_c_reason (l : row) (c : row) diffs =
let m = month_of_date l.date and d = day_of_date l.date in
if diffs = [] then None
@@ -1158,6 +1188,8 @@ let layer_c_reason (l : row) (c : row) diffs =
&& (not (List.mem c.slug rg33_vigil_slugs))
&& subset diffs [ Slug_f; Rank; Colour_f; First_f; Gospel_f ]
then Some "C39"
+ else if is_bissextile_shift_row l c ~month:m ~day:d && subset diffs [ Slug_f; Rank; Colour_f; First_f; Gospel_f ]
+ then Some "C40"
(* C25 -- CLOSED, BVM Saturday Mass (2026-08-17), predicate removed: 0 rows.
Its days are unoccupied IV-class Saturdays, so they carry RG 78's office
and now RG 309(a)'s Mass for it, which answers before step 3's walkback