aboutsummaryrefslogtreecommitdiff
path: root/test/test_temporal_of.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 23:45:16 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 23:45:16 +0200
commit9ebb06983b7a26db5564302253f2551dfbcf834e (patch)
treef73de5c193a13239258d062ca5f4caf64ba64dbe /test/test_temporal_of.ml
parent2f6d25aff88b4c76c61e36889f1c017b3cc8faee (diff)
downloadcolitur-9ebb06983b7a26db5564302253f2551dfbcf834e.tar.gz
colitur-9ebb06983b7a26db5564302253f2551dfbcf834e.zip
fix(of): dispatch Holy Family's Normae n.35(a) fallback (W2)
temporal_of.ml's temporal only ever reached holy_family's own correct 26-31 December / 30 December fallback logic through sunday_slug, which returns None immediately for a non-Sunday date. In a year 25 December is itself a Sunday (26-31 December then holding no Sunday of its own), the feast vanished entirely and fell through to an ordinary Christmas ferial slug -- confirmed on 2022, 2033, 2039, 2044, 2050 (and roughly 1583..9999 domain-wide, one year in seven). temporal's dispatch now consults holy_family directly, independent of weekday, between the sunday_slug and christmas_feria_slug branches, carrying the identical identity the Sunday case already builds (subject Lord, holy_family_names, rank Festum). holy_family and anchors were already correct and needed no change. Added a direct example-based test for the fallback across five affected years plus a control for the unaffected Sunday case, and a domain-wide property (Holy Family is observed in every liturgical year 1583-9999) folded into both the 200-sample QCheck run and the exhaustive sweep -- the property that would have caught this directly. Un-pinned the now-fixed behaviour everywhere it was recorded as known-wrong: test_rite_of.ml's own 1583 pin (rewritten to assert the fix), test_validate_of.ml's own independent exhaustive-sweep counter and landmark-year filter (found via make check, not the initial survey), and data/of/expected-divergences-litcal.sexp's L2/L3 allow-list entries against the litcal oracle (closed and removed, with a dedicated regression test replacing the allow-list's own silence).
Diffstat (limited to 'test/test_temporal_of.ml')
-rw-r--r--test/test_temporal_of.ml114
1 files changed, 82 insertions, 32 deletions
diff --git a/test/test_temporal_of.ml b/test/test_temporal_of.ml
index 2522e44..d8b1f4a 100644
--- a/test/test_temporal_of.ml
+++ b/test/test_temporal_of.ml
@@ -13,6 +13,10 @@ module D = Colitur_kernel.Date
let d y m dd = match D.make ~year:y ~month:m ~day:dd with Ok t -> t | Error e -> Alcotest.failf "%s" e
let iso = D.to_iso8601
+(* Moved up from the "Sunday and ferial slugs" section below so the W2
+ fallback tests (which need it) can use it without a forward reference. *)
+let slug_of dt = match T.temporal dt with t -> Colitur_kernel.Slug.to_string t.Colitur_kernel.Temporal.office.Colitur_kernel.Celebration.slug
+
(* ---- vocabulary ---- *)
let test_vocab_roundtrips () =
@@ -77,6 +81,49 @@ let test_holy_family () =
true;
Alcotest.(check string) "2022 fallback" "2022-12-30" (iso (T.holy_family 2022))
+(* CLAUDE.md's own W2: {!T.temporal} used to reach {!T.holy_family}'s own
+ Normae n. 35(a) fallback (30 December) ONLY through {!T.sunday_slug},
+ which returns [None] immediately for any non-Sunday date -- so in a year
+ 26-31 December held no Sunday (equivalently, 25 December, Christmas Day,
+ is itself a Sunday), the feast vanished entirely and 30 December fell
+ through to an ordinary Christmas ferial slug instead, even though
+ {!T.holy_family}/{!T.anchors} both already computed the right date. Fixed:
+ {!T.temporal} now consults {!T.holy_family} directly in that dispatch arm,
+ independent of weekday, carrying the identical identity (subject Lord,
+ {!T.holy_family_names} via [named_slug]'s own equivalent, rank Festum) the
+ Sunday case already builds. Five real affected years, per CLAUDE.md's own
+ W2 diagnosis (25 December a Sunday): 2022, 2033, 2039, 2044, 2050. *)
+let test_holy_family_fallback_temporal () =
+ List.iter
+ (fun y ->
+ let hf = T.holy_family y in
+ Alcotest.(check bool) (Printf.sprintf "%d: fallback really lands on 30 December" y) true
+ (D.month hf = 12 && D.day hf = 30);
+ Alcotest.(check bool) (Printf.sprintf "%d: and that day is not itself a Sunday" y) true
+ (D.weekday hf <> D.Sun);
+ Alcotest.(check string) (Printf.sprintf "%d: temporal resolves the fallback to of-holy-family" y)
+ "of-holy-family" (slug_of hf);
+ let t = T.temporal hf in
+ Alcotest.(check string) (Printf.sprintf "%d: subject is Lord" y) "lord"
+ (Colitur_kernel.Subject.to_string t.Colitur_kernel.Temporal.office.Colitur_kernel.Celebration.subject);
+ Alcotest.(check string) (Printf.sprintf "%d: rank is Festum" y) "festum"
+ (V.rank_to_string t.Colitur_kernel.Temporal.office.Colitur_kernel.Celebration.rank);
+ Alcotest.(check string) (Printf.sprintf "%d: colour is white (season default)" y) "white"
+ (Colitur_kernel.Colour.to_string t.Colitur_kernel.Temporal.office.Colitur_kernel.Celebration.colour))
+ [ 2022; 2033; 2039; 2044; 2050 ]
+
+(* Unaffected years -- 26-31 December DOES hold a Sunday, so {!T.sunday_slug}
+ already reaches the office and this fix changes nothing there. *)
+let test_holy_family_sunday_unaffected () =
+ List.iter
+ (fun y ->
+ let hf = T.holy_family y in
+ Alcotest.(check bool) (Printf.sprintf "%d: Holy Family really is a Sunday here" y) true
+ (D.weekday hf = D.Sun);
+ Alcotest.(check string) (Printf.sprintf "%d: temporal still resolves to of-holy-family" y) "of-holy-family"
+ (slug_of hf))
+ [ 2026; 2027; 2028; 2032 ]
+
let test_second_sunday_of_christmas () =
Alcotest.(check (option string)) "2026" (Some "2026-01-04") (Option.map iso (T.second_sunday_of_christmas 2026));
(* 2024: Baptism is already 7 January (6 Jan a Saturday), so no Sunday
@@ -211,7 +258,6 @@ let test_ordinary_time_resumption () =
(* ---- Sunday and ferial slugs ---- *)
let sunday_slug_of dt = match T.sunday_slug dt with Some s -> s | None -> "<none>"
-let slug_of dt = match T.temporal dt with t -> Colitur_kernel.Slug.to_string t.Colitur_kernel.Temporal.office.Colitur_kernel.Celebration.slug
let test_sunday_slugs () =
Alcotest.(check string) "Advent I 2026" "of-advent-sunday-1" (sunday_slug_of (T.advent_start 2026));
@@ -387,6 +433,22 @@ let prop_liturgical_years_partition_civil_time y =
in
check sorted
+(* CLAUDE.md's own W2: the property that would have caught the fallback gap
+ directly, rather than needing a dedicated example-based test to notice it
+ -- Holy Family must be OBSERVED somewhere in every single liturgical
+ year, never silently absent, whichever of the two Normae n. 35(a)
+ branches (a Sunday 26-31 December, or the 30 December fallback) fires
+ that year. [walk_year y] spans Advent of civil year [y] through the eve
+ of Advent [y + 1], which always contains December of civil year [y] in
+ full -- the same civil year {!T.holy_family} itself is keyed to -- so
+ this is a genuine same-year check, not an off-by-one across the Advent
+ boundary. *)
+let prop_holy_family_exists y =
+ List.exists
+ (fun (_, t) ->
+ Colitur_kernel.Slug.to_string t.Colitur_kernel.Temporal.office.Colitur_kernel.Celebration.slug = "of-holy-family")
+ (walk_year y)
+
let year_gen = QCheck.int_range 1584 9997 (* leaves room for [y - 1] and [y + 1] *)
let make_prop name f = QCheck.Test.make ~count:200 ~name year_gen f
@@ -399,7 +461,8 @@ let prop_tests =
make_prop "OF: temporal's weekday always agrees with Date.weekday" prop_weekday_agrees;
make_prop "OF: temporal is deterministic" prop_determinism;
make_prop "OF: consecutive liturgical years partition civil time with no gap or overlap"
- prop_liturgical_years_partition_civil_time ]
+ prop_liturgical_years_partition_civil_time;
+ make_prop "OF: Holy Family is observed every liturgical year (CLAUDE.md W2)" prop_holy_family_exists ]
(* ---- the committed exhaustive sweep, mirroring test_validate.ml's own
COLITUR_EXHAUSTIVE_SWEEP convention: every year 1583..9998, not a
@@ -416,19 +479,18 @@ let test_exhaustive_domain_sweep () =
(fixed just above, {!test_anchors_agree_with_temporal})? Checked for
EVERY entry [anchors y] returns, across the WHOLE exhaustive domain,
not merely 24 December, folded into this loop rather than a separate
- one so it shares its own env-var gate. Answer: no NEW drift -- the
- only mismatch found is the ALREADY-KNOWN "of-holy-family" gap
- (Normae n.35(a)'s own 30-December fallback, whenever 26-31 December
- holds no Sunday): there, unlike the Vigil bug, {!T.anchors} (via
- {!T.holy_family}) computes the RIGHT date and it is {!T.temporal}
- that fails to reach it (its Holy-Family test lives inside
- [sunday_slug]'s own Sunday-only dispatch arm) -- a different shape,
- already found, pinned and explicitly deferred by test_rite_of.ml's
- own [test_holy_family_fallback_1583_known_wrong_ferial]/
- [is_known_holy_family_fallback_gap] (see that file's own extensive
- citation), collected below rather than failing the sweep on it a
- second time in a second place. *)
- let holy_family_fallback_years = ref [] in
+ one so it shares its own env-var gate.
+
+ UPDATED (W2 fix): this loop used to carve out and separately collect
+ an "ALREADY-KNOWN of-holy-family gap" here -- {!T.anchors} (via
+ {!T.holy_family}) computed the right 30-December fallback date but
+ {!T.temporal} failed to reach it (its Holy-Family test lived only
+ inside [sunday_slug]'s own Sunday-only dispatch arm), pinned rather
+ than fixed by test_rite_of.ml's own (now also fixed)
+ [test_holy_family_fallback_1583_known_wrong_ferial]. {!T.temporal}
+ now consults {!T.holy_family} directly, independent of weekday, so
+ that carve-out is gone: EVERY entry [anchors y] returns must now
+ agree with [temporal] with no exception, across the whole domain. *)
for y = 1584 to 9997 do
let days = walk_year y in
if compressed_seasons days <> expected_season_runs then
@@ -438,28 +500,14 @@ let test_exhaustive_domain_sweep () =
if not (prop_advent_four_sundays y) then Alcotest.failf "%d: Advent does not have exactly four Sundays" y;
if not (prop_slug_uniqueness y) then Alcotest.failf "%d: a slug repeats within the liturgical year" y;
if not (prop_weekday_agrees y) then Alcotest.failf "%d: a weekday disagrees with Date.weekday" y;
+ if not (prop_holy_family_exists y) then Alcotest.failf "%d: Holy Family is not observed anywhere this year" y;
List.iter
(fun (expected_slug, date) ->
let actual = slug_of date in
if actual <> expected_slug then
- if expected_slug = "of-holy-family" then holy_family_fallback_years := y :: !holy_family_fallback_years
- else
- Alcotest.failf
- "%d: anchors' %S (%s) disagrees with temporal (%S) -- an UNEXPECTED drift, not the known \
- Holy Family fallback shape"
- y expected_slug (iso date) actual)
+ Alcotest.failf "%d: anchors' %S (%s) disagrees with temporal (%S)" y expected_slug (iso date) actual)
(T.anchors y)
- done;
- (* The known shape is real, not vacuous, and really is the Normae
- n.35(a) trigger, for every year collected above, not merely
- plausible-sounding. *)
- Alcotest.(check bool) "the Holy Family fallback shape is real and not empty" true
- (!holy_family_fallback_years <> []);
- List.iter
- (fun y ->
- Alcotest.(check bool) (Printf.sprintf "%d is really a Christmas-Day-is-Sunday year" y) true
- (D.weekday (d y 12 25) = D.Sun))
- !holy_family_fallback_years
+ done
end
(* 1583 and 9999 themselves sit at the domain edges, where the [y-1]/[y+1]
@@ -483,6 +531,8 @@ let suite =
Alcotest.test_case "seasons" `Quick test_seasons;
Alcotest.test_case "baptism of the lord" `Quick test_baptism_of_the_lord;
Alcotest.test_case "holy family" `Quick test_holy_family;
+ Alcotest.test_case "holy family fallback reaches temporal (W2 fix)" `Quick test_holy_family_fallback_temporal;
+ Alcotest.test_case "holy family Sunday case unaffected by the fix" `Quick test_holy_family_sunday_unaffected;
Alcotest.test_case "second sunday of christmas" `Quick test_second_sunday_of_christmas;
Alcotest.test_case "christ the king" `Quick test_christ_the_king;
Alcotest.test_case "named feasts" `Quick test_named_feasts;