From 98d698b261c75a287ba3992744032babfb6ad763 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 25 Aug 2026 23:25:56 +0200 Subject: fix(of): add the Sacred Heart of Jesus and Immaculate Heart of Mary Both are printed in the 2002 calendar table, immediately after 30 June, as prose headings rather than (kalends, day) rows -- the shape parse_rows' generic movable-heading skip branch was built for, which is why the first pass silently dropped them. They belong in the transcription, not in Temporal_of: adding them completes the table this file transcribes rather than amending the temporal cycle. Produced as Date_spec.Easter_offset entries (68 and 69 respectively), the same variant already used for EF's Rogation Wednesday and needing no kernel change. Pentecost is Easter+49, so the Friday and Saturday after the Second Sunday after Pentecost are Easter+68/+69 -- verified against three real Easter dates (2026, 2027, 2035), not merely computed on paper. The extractor now recognises these two headings specifically among the seven "Dominica/Feria/Sabbato ... :" headings the table contains; a full audit of the other five confirms each is already covered by Temporal_of and correctly stays excluded here. 206 -> 208 entries. --- test/test_calendar_of_data.ml | 76 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 8 deletions(-) (limited to 'test/test_calendar_of_data.ml') diff --git a/test/test_calendar_of_data.ml b/test/test_calendar_of_data.ml index 01a757e..d4f8a8b 100644 --- a/test/test_calendar_of_data.ml +++ b/test/test_calendar_of_data.ml @@ -5,9 +5,20 @@ '(slug', grep -oP rank/status/colour/subject | sort | uniq -c -- see .superpowers/sdd/2026-08-25-colitur-of-phases-3-5/task-1-report.md), plus named spot-checks read straight off the Missal's own printed page so a - passing count can't hide the wrong 206 entries having been transcribed + passing count can't hide the wrong entries having been transcribed (the same "live hazard" test_sanctoral_ef.ml's own header names). + Fix round 1 (2026-08-25): 206 -> 208. Two movable universal celebrations + -- the Sacred Heart of Jesus and the Immaculate Heart of Mary -- are + printed in the SAME 2002 table (as prose headings, not (kalends, day) + rows) but were missed by the first pass; both are now produced as + Date_spec.Easter_offset entries (68/69) by + tools/extract_of_calendar.py's own MOVABLE_ENTRY_HEADINGS, the same + mechanism EF's Rogation Wednesday already uses. See + test_movable_entries below, which verifies the Easter+68/Easter+69 + arithmetic against three REAL Easter dates rather than trusting the + offset on its own say-so. + SOURCE DISCIPLINE, asserted structurally here, not merely stated in the tool's own header: every entry's rank is one of the four Vocab_of.rank grades NEVER Feria (that rank is temporal-only, see vocab_of.mli), and @@ -58,7 +69,7 @@ let sha256_of_file path = don't transcribe it from output" pinning discipline. *) let test_sha256 () = Alcotest.(check string) "data/of/calendar-2002.sexp SHA-256" - "b2bf3fa9d27f5bc6e47225c477c158f629eaab6c280e0b69fa04c8a55db9f612" (sha256_of_file path) + "1b4f2117133dea1c70989708d94e6f292f3ce23fce6221391191d6fcf6042d19" (sha256_of_file path) (* Counts independently re-derived from the shipped file (`grep -c '(slug'`; `grep -oP '(?<=\(rank )[A-Za-z_]+' | sort | uniq -c`). @@ -68,19 +79,19 @@ let test_sha256 () = asserting the count moved. *) let test_load_and_counts () = let l = load () in - Alcotest.(check int) "206 entries" 206 (List.length l.L.entries); + Alcotest.(check int) "208 entries" 208 (List.length l.L.entries); let count pred = List.length (List.filter pred l.L.entries) in - Alcotest.(check int) "8 Sollemnitas" 8 (count (fun e -> e.L.cel.Cel.rank = V.Sollemnitas)); + Alcotest.(check int) "9 Sollemnitas" 9 (count (fun e -> e.L.cel.Cel.rank = V.Sollemnitas)); Alcotest.(check int) "23 Festum" 23 (count (fun e -> e.L.cel.Cel.rank = V.Festum)); - Alcotest.(check int) "67 Memoria_obligatoria" 67 + Alcotest.(check int) "68 Memoria_obligatoria" 68 (count (fun e -> e.L.cel.Cel.rank = V.Memoria_obligatoria)); Alcotest.(check int) "108 Memoria_ad_libitum" 108 (count (fun e -> e.L.cel.Cel.rank = V.Memoria_ad_libitum)); Alcotest.(check int) "0 Feria (temporal-only rank, never a calendar entry)" 0 (count (fun e -> e.L.cel.Cel.rank = V.Feria)); - Alcotest.(check int) "206 Feast (OF admits no Commemoration_only status)" 206 + Alcotest.(check int) "208 Feast (OF admits no Commemoration_only status)" 208 (count (fun e -> e.L.cel.Cel.status = Cel.Feast)); - Alcotest.(check int) "every entry tagged Precedence_of.universal_layer" 206 + Alcotest.(check int) "every entry tagged Precedence_of.universal_layer" 208 (count (fun e -> e.L.cel.Cel.layer = PO.universal_layer)) let test_slugs_unique () = @@ -183,6 +194,52 @@ let test_excludes_temporal_of_dates () = | Some (_, _, _, rank) -> Alcotest.(check bool) "25 Dec: Sollemnitas" true (rank = V.Sollemnitas) | None -> Alcotest.fail "25 Dec: Temporal_of.named returned None" +(* Fix round 1: the two movable entries (Sacred Heart of Jesus, Immaculate + Heart of Mary) resolve to the right weekday AND date in three real + years, computed from Colitur_kernel.Computus.gregorian_easter, not + hand-typed dates -- so a wrong offset in the data would fail this even + if it "looked" plausible. Also independently confirms the coordinator's + own +68/+69 derivation (Pentecost = Easter+49, so the Friday/Saturday + after the Second Sunday after Pentecost are Easter+68/+69) rather than + trusting it. *) +let test_movable_entries () = + let l = load () in + let heart = find l "sacred-heart-of-jesus" in + Alcotest.(check bool) "Sacred Heart: date spec is Easter_offset 68" true + (heart.L.date = DS.Easter_offset 68); + Alcotest.(check bool) "Sacred Heart: Sollemnitas" true + (heart.L.cel.Cel.rank = V.Sollemnitas); + Alcotest.(check bool) "Sacred Heart: white" true (heart.L.cel.Cel.colour = Col.White); + Alcotest.(check bool) "Sacred Heart: subject Lord" true (heart.L.cel.Cel.subject = Sub.Lord); + let immaculate = find l "immaculate-heart-of-mary" in + Alcotest.(check bool) "Immaculate Heart: date spec is Easter_offset 69" true + (immaculate.L.date = DS.Easter_offset 69); + Alcotest.(check bool) "Immaculate Heart: Memoria_obligatoria" true + (immaculate.L.cel.Cel.rank = V.Memoria_obligatoria); + Alcotest.(check bool) "Immaculate Heart: white" true (immaculate.L.cel.Cel.colour = Col.White); + Alcotest.(check bool) "Immaculate Heart: subject Bvm" true (immaculate.L.cel.Cel.subject = Sub.Bvm); + List.iter + (fun year -> + let easter = Colitur_kernel.Computus.gregorian_easter year in + let resolve spec = + match DS.resolve spec ~year ~easter with + | Some d -> d + | None -> Alcotest.failf "year %d: date spec did not resolve" year + in + let sacred_heart_date = resolve heart.L.date in + let immaculate_heart_date = resolve immaculate.L.date in + Alcotest.(check bool) + (Printf.sprintf "%d: Sacred Heart (Easter+68) falls on a Friday" year) + true (D.weekday sacred_heart_date = D.Fri); + Alcotest.(check bool) + (Printf.sprintf "%d: Immaculate Heart (Easter+69) falls on a Saturday" year) + true (D.weekday immaculate_heart_date = D.Sat); + (* Immaculate Heart is the day right after Sacred Heart, every year. *) + Alcotest.(check bool) + (Printf.sprintf "%d: Immaculate Heart is the day after Sacred Heart" year) + true (immaculate_heart_date = D.add_days sacred_heart_date 1)) + [ 2026; 2027; 2035 ] + let suite = ( "Calendar_of (data/of/calendar-2002.sexp)", [ Alcotest.test_case "SHA-256 pinned" `Quick test_sha256; @@ -204,4 +261,7 @@ let suite = test_spot_check_angela_merici; Alcotest.test_case "1/6 Jan and 25 Dec deliberately excluded -- Temporal_of.named covers them" `Quick - test_excludes_temporal_of_dates ] ) + test_excludes_temporal_of_dates; + Alcotest.test_case + "fix round 1: Sacred Heart of Jesus (Easter+68) and Immaculate Heart of Mary (Easter+69)" `Quick + test_movable_entries ] ) -- cgit v1.3