(* Task 4 (2026-08-25-colitur-of-phases-3-5): the reading-cycle arithmetic and the temporal + sanctoral lectionary resolution chain, Rite_of.Lectionary_of. No Rite_of.context exists yet (Task 5's own deliverable), so the coverage test below builds its own APPROXIMATE day resolver directly against Layer.on_date + Temporal_of.temporal, rather than through Calendar.day/Colitur_kernel.Validate -- it does not run RG-style occurrence/precedence resolution (Precedence_of.rules is Task 5's own wiring point too), so on a date where more than one sanctoral entry would occur it simply takes the layer's own canonically-sorted first match. This is not a claim that the FULL calendar resolves this way; it is the minimum needed to exercise Lectionary_of.readings' own observed-vs-temporal branch honestly, over real (not synthetic) data. *) module L = Colitur_kernel.Layer module O = Colitur_kernel.Overlay module Cel = Colitur_kernel.Celebration module D = Colitur_kernel.Date module Slug = Colitur_kernel.Slug module Mass_formulary = Colitur_kernel.Mass_formulary module Lectionary = Colitur_kernel.Lectionary module Computus = Colitur_kernel.Computus module V = Rite_of.Vocab_of module T = Rite_of.Temporal_of module Lect_of = Rite_of.Lectionary_of 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 (* ---- fixtures -------------------------------------------------------- *) let lectionary_path = "../data/of/lectionary.sexp" let calendar_path = "../data/of/calendar-2002.sexp" let amendments_dir = "../data/of/amendments/" let amendment_files = [ "001-padre-pio.sexp"; "002-juan-diego-cuauhtlatoatzin.sexp"; "003-our-lady-of-guadalupe.sexp"; "004-john-xxiii-john-paul-ii.sexp"; "005-mary-magdalene-rank.sexp"; "006-mary-mother-of-the-church.sexp"; "007-paul-vi.sexp"; "008-our-lady-of-loreto.sexp"; "009-faustina-kowalska.sexp"; "010-narek-avila-hildegard.sexp"; "011-martha-mary-lazarus.sexp"; "012-teresa-of-calcutta.sexp"; "013-john-henry-newman.sexp" ] let real_lectionary () = match Lectionary.load lectionary_path with | Ok l -> l | Error e -> Alcotest.failf "%s: failed to load: %s" lectionary_path e let real_sanctoral_layer () = let base = match L.load V.rank_of_sexp calendar_path with | Ok l -> l | Error e -> Alcotest.failf "%s: failed to load: %s" calendar_path e in let overlays = List.map (fun name -> let path = amendments_dir ^ name in match O.load V.rank_of_sexp path with | Ok o -> o | Error e -> Alcotest.failf "%s: failed to load: %s" path e) amendment_files in let merged, diagnostics = O.merge base overlays in Alcotest.(check (list string)) "the committed OF overlays apply cleanly, no diagnostics" [] (List.map O.diagnostic_to_string diagnostics); merged (* Approximate "observed" celebration for [date]: the sanctoral layer's own (canonically-sorted) first match if any, else the day's own temporal office -- see this file's own top-of-file comment for why this is a deliberately simplified stand-in for real occurrence resolution. *) let observed_on index date temporal = match L.on_date index date with | e :: _ -> e.L.cel | [] -> temporal.Colitur_kernel.Temporal.office let readings_on lectionary index date = let temporal = T.temporal date in let observed = observed_on index date temporal in Lect_of.readings ~lectionary ~year_start:T.year_start ~observed ~temporal ~date ~temporal_at:T.temporal (* ---- cycle arithmetic (OLM 1981 Praenotanda n.66/n.69 -- see lectionary_of.mli's own citations, page-image verified) ---- *) let sunday_letter c = Lect_of.sunday_cycle_letter c let weekday_letter c = Lect_of.weekday_cycle_letter c (* Every case here is derivable from OLM n.66's own footnote 102, whose worked example ("annus 1980 est annus C, annus vero sequens, scilicet annus 1981, est annus A, annus vero 1982 est annus B, et annus 1983 est iterum annus C") is reproduced directly by testing dates EITHER SIDE of Advent 1980/1981/1982 -- the task brief's own "straddle Advent I inside a single civil year" requirement, exercised three times over, against the primary source's own numbers, not invented ones. *) let test_sunday_cycle_table () = let cases = [ (* Just before Advent 1980 (still the liturgical year opened Advent 1979, labelled 1980 -- OLM's own "1980 est annus C"), and Advent 1980 itself (opens the year labelled 1981 -- "1981, est annus A"): both civil dates fall in the SAME civil year, 1980, on either side of that year's own Advent I -- the boundary the brief names. *) (D.add_days (T.advent_start 1980) (-1), "c", "before Advent 1980 (civil year 1980, label 1980)"); (T.advent_start 1980, "a", "Advent Sunday 1980 itself (civil year 1980, label 1981)"); (D.add_days (T.advent_start 1980) 3, "a", "a few days into Advent 1980 (still label 1981)"); (* Advent 1981 -> 1982 (A -> B). *) (D.add_days (T.advent_start 1981) (-1), "a", "before Advent 1981 (label 1981)"); (T.advent_start 1981, "b", "Advent Sunday 1981 (label 1982)"); (* Advent 1982 -> 1983 (B -> C). *) (D.add_days (T.advent_start 1982) (-1), "b", "before Advent 1982 (label 1982)"); (T.advent_start 1982, "c", "Advent Sunday 1982 (label 1983)"); (* A live-relevant pair: Advent 2025 (opens the "2026" liturgical year, this task's own civil year) is label 2026, 2026 mod 3 = 1 -> A; the civil days immediately before it are still label 2025, 2025 mod 3 = 0 -> C. *) (D.add_days (T.advent_start 2025) (-1), "c", "before Advent 2025 (label 2025)"); (T.advent_start 2025, "a", "Advent Sunday 2025 (label 2026)") ] in List.iter (fun (date, expected, label) -> Alcotest.(check string) (label ^ " (" ^ iso date ^ ")") expected (sunday_letter (Lect_of.sunday_cycle ~year_start:T.year_start date))) cases (* OLM n.69 point 4: Year I odd label years, Year II even. Reuses the SAME Advent-1980/1981/1982 boundaries as the Sunday-cycle table above (label 1980 even -> II, 1981 odd -> I, 1982 even -> II, 1983 odd -> I), straddling Advent I within a single civil year exactly as the Sunday table does. *) let test_weekday_cycle_table () = let cases = [ (D.add_days (T.advent_start 1980) (-1), "ii", "before Advent 1980 (label 1980, even)"); (T.advent_start 1980, "i", "Advent Sunday 1980 (label 1981, odd)"); (D.add_days (T.advent_start 1981) (-1), "i", "before Advent 1981 (label 1981, odd)"); (T.advent_start 1981, "ii", "Advent Sunday 1981 (label 1982, even)"); (D.add_days (T.advent_start 1982) (-1), "ii", "before Advent 1982 (label 1982, even)"); (T.advent_start 1982, "i", "Advent Sunday 1982 (label 1983, odd)"); (D.add_days (T.advent_start 2025) (-1), "i", "before Advent 2025 (label 2025, odd)"); (T.advent_start 2025, "ii", "Advent Sunday 2025 (label 2026, even)") ] in List.iter (fun (date, expected, label) -> Alcotest.(check string) (label ^ " (" ^ iso date ^ ")") expected (weekday_letter (Lect_of.weekday_cycle ~year_start:T.year_start date))) cases (* The two cycles are independent computations, not two views of the same number -- Advent 1981 (label 1982) is Sunday-cycle B but weekday-cycle II, a label that is even for one modulus and not reducible to the other. *) let test_cycles_are_independent () = let date = T.advent_start 1981 in Alcotest.(check string) "Sunday cycle B" "b" (sunday_letter (Lect_of.sunday_cycle ~year_start:T.year_start date)); Alcotest.(check string) "weekday cycle II" "ii" (weekday_letter (Lect_of.weekday_cycle ~year_start:T.year_start date)) (* ---- SHA-256 pins ----------------------------------------------------- *) let sha256_of_file path = let tmp = Filename.temp_file "colitur_lectionary_of_sha256" ".txt" in let cmd = Printf.sprintf "sha256sum %s > %s" (Filename.quote path) (Filename.quote tmp) in (match Sys.command cmd with | 0 -> () | n -> Alcotest.failf "sha256sum exited %d" n); let ic = open_in tmp in let line = input_line ic in close_in ic; Sys.remove tmp; match String.split_on_char ' ' line with | hash :: _ -> hash | [] -> Alcotest.fail "sha256sum produced no output" (* Pinned 2026-08-26 (FIX 1, O-Antiphon/Christmas-season date-keyed review) against the shipped file itself, independently re-derived (`sha256sum data/of/lectionary.sexp`), the same "derive the new value, don't transcribe it from generator stdout" discipline test_calendar_of_data.ml's own [test_sha256] already follows. *) let test_sha256_pinned () = (* Fix wave I8 (final-review.md, 2026-08-25-colitur-of-phases-3-5): the hash changed again -- ONE genuine new entry this time ("of-nativity", the Christmas Day Mass hand-authored from OLM 1981; see test_coverage_2026's own citation), not header-only as the I2 change just above was. Re-derived with `sha256sum data/of/lectionary.sexp`, same discipline. *) Alcotest.(check string) "data/of/lectionary.sexp SHA-256" "e08207614472955974b504bfe6984ff44e882c4d8fadc778c4fa1ddce1adecf8" (sha256_of_file lectionary_path) (* lectio's OWN of-lectionary.ini SHA-256, pinned inside data/of/ lectionary.sexp's own provenance header (tools/bootstrap_lectionary_of .ml's own emitted "; SHA-256: ..." line for the SOURCE, not the file pinned above) -- checked here as a plain substring of the shipped file, not by re-reading the sibling lectio repository (which need not be present wherever this test runs; the point of pinning it in the header is that this repo alone is then enough to know which snapshot produced it). *) let test_source_sha256_pinned_in_header () = let ic = open_in lectionary_path in let n = in_channel_length ic in let content = really_input_string ic n in close_in ic; let needle = "SHA-256: f0b527aef58b93e6ccdfbeafe52b340ed2a9d7d72805bf5083e3da04b080123c" in let contains haystack needle = let hl = String.length haystack and nl = String.length needle in let rec go i = i + nl <= hl && (String.sub haystack i nl = needle || go (i + 1)) in go 0 in Alcotest.(check bool) "lectio's of-lectionary.ini SHA-256 is pinned in the provenance header" true (contains content needle) (* A missing lectio snapshot must fail LOUDLY -- Task 1's own extractor shipped exactly this bug (parse_lectio_ini swallowing FileNotFoundError, silently producing a DIFFERENT file with a different SHA-256 and zero `en` names) and needed a review round to fix; this task's own brief names it explicitly as a trap not to repeat. Exercised here as a real subprocess run, not merely by reading the generator's source: invokes the built tools/bootstrap_lectionary_of.exe against a path that does not exist and asserts BOTH a non-zero exit AND that no output file was written (a generator that failed loudly but left a partial/stale destination file behind would be only half fixed). *) let test_missing_source_fails_loudly () = let exe = "../tools/bootstrap_lectionary_of.exe" in let bogus_src = Filename.temp_file "colitur_lectionary_of_missing" ".ini" in Sys.remove bogus_src; (* guaranteed not to exist, still a plausible-looking path *) let dst = Filename.temp_file "colitur_lectionary_of_dst" ".sexp" in Sys.remove dst; let log = Filename.temp_file "colitur_lectionary_of_stderr" ".txt" in let cmd = Printf.sprintf "%s %s %s >%s 2>&1" (Filename.quote exe) (Filename.quote bogus_src) (Filename.quote dst) (Filename.quote log) in let status = Sys.command cmd in let ic = open_in log in let n = in_channel_length ic in let stderr_text = really_input_string ic n in close_in ic; Sys.remove log; Alcotest.(check bool) "non-zero exit on a missing source file" true (status <> 0); Alcotest.(check bool) "no output file written" false (Sys.file_exists dst); Alcotest.(check bool) "error message names the missing path and explains why this matters" true (let contains haystack needle = let hl = String.length haystack and nl = String.length needle in let rec go i = i + nl <= hl && (String.sub haystack i nl = needle || go (i + 1)) in go 0 in contains stderr_text "no such file" && contains stderr_text bogus_src) (* ---- basic load ------------------------------------------------------- *) let test_load () = let l = real_lectionary () in (* 771, not 770: fix wave I8 (final-review.md) added ONE hand-authored entry, "of-nativity" (the Christmas Day Mass, absent from lectio's own source entirely -- see this file's own [test_coverage_2026] and tools/bootstrap_lectionary_of.ml's own [hand_authored] table for the citation and full argument). *) Alcotest.(check int) "771 entries" 771 (List.length (Lectionary.entries l)) (* ---- coverage: a full sample civil year, both directions, PINNED ------ *) (* Civil year 2026 -- the same year this task's data file's own coverage report uses. Iterates every day, resolving readings the same approximate way [readings_on] does (see this file's own top-of-file comment), and asserts BOTH the total resolved count AND the exact, named set of unresolved dates -- never a silent zero (task brief's own Step 4 requirement). *) let all_days_of_2026 = let days_in_month = [| 31; 28; 31; 30; 31; 30; 31; 31; 30; 31; 30; 31 |] in List.concat (List.init 12 (fun mi -> let m = mi + 1 in List.init days_in_month.(mi) (fun di -> d 2026 m (di + 1)))) let test_coverage_2026 () = let lectionary = real_lectionary () in let layer = real_sanctoral_layer () in let index = L.index layer ~easter:Computus.gregorian_easter ~years:[ 2025; 2026 ] in let unresolved = List.filter_map (fun date -> match readings_on lectionary index date with | None, [] -> Some (iso date) | Some _, (_ :: _) -> None | (Some _, []) | (None, _ :: _) -> Alcotest.failf "%s: Mass_formulary.t and citations disagree on whether readings resolved" (iso date)) all_days_of_2026 in Alcotest.(check int) "365 days in 2026" 365 (List.length all_days_of_2026); (* CLOSED, fix wave I8 (final-review.md, 2026-08-25-colitur-of-phases-3-5): lectio's 988 keys never included a Christmas DAY Mass (only the Vigil) -- tools/bootstrap_lectionary_of.ml's own [hand_authored] table now injects it directly (Isa 52:7-10/John 1:1-18, OLM 1981's own "16 Ad Missam in die", docs/research/of/olm-1981-ocr.txt:4513-4520), so the set that used to be pinned as {25 December} is now pinned as EMPTY -- still a NAMED set, not a bare count collapsing to zero by coincidence: a future regression reopening this (or any other) gap still fails loudly by naming the date, not merely by a count mismatch. *) Alcotest.(check (list string)) "the unresolved-day set is empty" [] unresolved; Alcotest.(check int) "365 of 365 days resolve readings" 365 (365 - List.length unresolved) (* Same measurement, restated as "how many days resolve" rather than "how many don't" -- redundant with the count above by construction, kept because Step 4's own brief asks for a pinned resolved-count assertion specifically, not only its complement. *) let test_pinned_resolved_count () = let lectionary = real_lectionary () in let layer = real_sanctoral_layer () in let index = L.index layer ~easter:Computus.gregorian_easter ~years:[ 2025; 2026 ] in let resolved = List.length (List.filter (fun date -> match readings_on lectionary index date with Some _, _ :: _ -> true | _ -> false) all_days_of_2026) in Alcotest.(check int) "365 days resolve readings in 2026 (fix wave I8: was 364)" 365 resolved (* ---- resolution chain: unit-level spot checks -------------------------- *) (* Step 3: an ordinary Ordinary Time weekday with no sanctoral entry resolves via its own temporal slug, cycle-selected. 16 June 2026 is a Tuesday of Ordinary Time, in the SECOND (post-Pentecost) block, with no calendar-2002.sexp/amendments entry (checked against the real merged layer, not assumed, and clear of the three nearby movable solemnities -- Corpus Christi 4 June, Sacred Heart 12 June, Immaculate Heart 13 June). The expected slug is read off Temporal_of.temporal itself, not hand-computed: the second block's own week arithmetic (temporal_of.ml's own [ordinary_time_week]) is counted BACKWARDS from Christ the King, not forwards from the Baptism -- re-deriving that by hand here would just duplicate the one calculation this test exists to exercise, not check it independently. *) let test_step3_temporal_ferial () = let lectionary = real_lectionary () in let layer = real_sanctoral_layer () in let index = L.index layer ~easter:Computus.gregorian_easter ~years:[ 2025; 2026 ] in let date = d 2026 6 16 in Alcotest.(check bool) "16 June 2026: no sanctoral entry" true (L.on_date index date = []); let expected_slug = (T.temporal date).Colitur_kernel.Temporal.office.Cel.slug in match readings_on lectionary index date with | Some fm, (_ :: _ as cs) -> Alcotest.(check bool) "tagged Own_slug" true (fm.Mass_formulary.via = Mass_formulary.Own_slug); Alcotest.(check (option string)) "said the temporal slug" (Some (Slug.to_string expected_slug)) (Option.map Slug.to_string fm.Mass_formulary.said); Alcotest.(check int) "two citations (First + Gospel)" 2 (List.length cs) | _ -> Alcotest.fail "16 June 2026 should resolve readings via step 3" (* Step 2: a sanctoral saint who won the day resolves via his own slug, not the temporal ferial's. 30 November 2026 is a Monday of Advent week 1 (which would otherwise be "of-advent-1-monday") but is also St Andrew the Apostle's own fixed date. *) let test_step2_sanctoral_wins () = let lectionary = real_lectionary () in let layer = real_sanctoral_layer () in let index = L.index layer ~easter:Computus.gregorian_easter ~years:[ 2025; 2026 ] in let date = d 2026 11 30 in (match L.on_date index date with | [ e ] -> Alcotest.(check string) "St Andrew's own entry" "andrew-the-apostle" (Slug.to_string e.L.cel.Cel.slug) | es -> Alcotest.failf "expected exactly one sanctoral entry on 30 Nov 2026, got %d" (List.length es)); match readings_on lectionary index date with | Some fm, (_ :: _ as cs) -> Alcotest.(check bool) "tagged Proper" true (fm.Mass_formulary.via = Mass_formulary.Proper); Alcotest.(check (option string)) "said Andrew's own slug" (Some "andrew-the-apostle") (Option.map Slug.to_string fm.Mass_formulary.said); (* "of-advent-1-monday" is FLAT, not cycle-lettered (OLM n.69 point 3: Advent's own ferias do not change year to year -- confirmed by this generator's own collapse, tools/bootstrap_lectionary_of.ml's [collapse_weekday]), so there is exactly one temporal citation set to compare against, not two. *) Alcotest.(check bool) "NOT the temporal ferial's Advent citations" true (cs <> Option.get (Lectionary.find lectionary (Slug.of_string_exn "of-advent-1-monday"))) | _ -> Alcotest.fail "30 November 2026 should resolve readings via step 2" (* ---- date-keyed windows (OLM n. 69.3) -- FIX 1, 2026-08-26 review ----- *) (* {!Lect_of.date_keyed_slug} itself, direct unit coverage of its own boundaries: the O-Antiphon window (17-24 December), the Christmas-season window (29-31 December, then 2-5 and 7 January), 6 January's own deliberate exclusion (Temporal_of's own [named] fixes Epiphany there unconditionally, so that date can never reach this function's caller as a ferial), and the Sunday guard (a Sunday inside either window is a NAMED office -- Advent 4, Holy Family, the Second Sunday after Christmas -- never one of these ferial dates). *) let test_date_keyed_slug_boundaries () = let some_str dt = Option.map Slug.to_string (Lect_of.date_keyed_slug dt) in Alcotest.(check (option string)) "16 December: outside the window" None (some_str (d 2026 12 16)); Alcotest.(check (option string)) "17 December: in the window" (Some "of-advent-dec-17") (some_str (d 2026 12 17)); Alcotest.(check (option string)) "24 December: in the window" (Some "of-advent-dec-24") (some_str (d 2029 12 24)); Alcotest.(check (option string)) "25 December: outside the window" None (some_str (d 2026 12 25)); Alcotest.(check (option string)) "28 December: outside the window" None (some_str (d 2026 12 28)); Alcotest.(check (option string)) "29 December: in the window" (Some "of-christmas-dec-29") (some_str (d 2026 12 29)); Alcotest.(check (option string)) "31 December: in the window" (Some "of-christmas-dec-31") (some_str (d 2029 12 31)); Alcotest.(check (option string)) "1 January: outside the window" None (some_str (d 2026 1 1)); Alcotest.(check (option string)) "2 January: in the window" (Some "of-christmas-jan-2") (some_str (d 2026 1 2)); Alcotest.(check (option string)) "5 January: in the window" (Some "of-christmas-jan-5") (some_str (d 2026 1 5)); Alcotest.(check (option string)) "6 January: deliberately excluded, Epiphany is fixed there unconditionally" None (some_str (d 2026 1 6)); Alcotest.(check (option string)) "7 January: in the window" (Some "of-christmas-jan-7") (some_str (d 2026 1 7)); Alcotest.(check (option string)) "8 January: outside the window" None (some_str (d 2026 1 8)); Alcotest.(check bool) "2028-12-24 is a Sunday, sanity check" true (D.weekday (d 2028 12 24) = D.Sun); Alcotest.(check (option string)) "24 December on a Sunday (2028, Advent 4): guarded off" None (some_str (d 2028 12 24)); Alcotest.(check bool) "2025-01-05 is a Sunday, sanity check" true (D.weekday (d 2025 1 5) = D.Sun); Alcotest.(check (option string)) "5 January on a Sunday (2025): guarded off" None (some_str (d 2025 1 5)) (* The fix's own headline claim, through the REAL resolution chain, not just the slug function in isolation: the same civil date resolves the SAME citation across two years with a different weekday alignment, where it used to drift. Verified by hand against the pre-fix binary before this fix landed: 2024-12-17 served "Zephaniah 3:1-2,9-13/ Matth 21:28-32" (that year's own "of-advent-3-tuesday" content) while 2029-12-17 served "Numbers 24:2-7,15-17a/Matth 21:23-27" ("of-advent-3- monday") -- two DIFFERENT citations for the identical calendar date. 2024 (Tuesday) and 2029 (Monday) are two different, non-Sunday weekdays, so neither the sanctoral layer nor the Sunday guard is in play -- both years reach {!Lect_of.readings}'s own step 3 through the plain temporal-ferial branch, exactly like {!test_step3_temporal_ferial} above. *) let test_o_antiphon_no_drift () = let check_year y expected_weekday_slug = let lectionary = real_lectionary () in let layer = real_sanctoral_layer () in let index = L.index layer ~easter:Computus.gregorian_easter ~years:[ y - 1; y ] in let date = d y 12 17 in let expected_slug = (T.temporal date).Colitur_kernel.Temporal.office.Cel.slug in Alcotest.(check string) (Printf.sprintf "%d-12-17: expected temporal slug" y) expected_weekday_slug (Slug.to_string expected_slug); match readings_on lectionary index date with | Some fm, cs -> Alcotest.(check bool) (Printf.sprintf "%d: tagged Own_slug" y) true (fm.Mass_formulary.via = Mass_formulary.Own_slug); Alcotest.(check (option string)) (Printf.sprintf "%d: said the temporal slug" y) (Some (Slug.to_string expected_slug)) (Option.map Slug.to_string fm.Mass_formulary.said); Alcotest.(check (list string)) (Printf.sprintf "%d: 17 December is Gen 49:2,8-10 / Matthew 1:1-17" y) [ "Genesis 49:2,8-10"; "Matthew 1:1-17" ] (List.map (fun (c : Colitur_kernel.Citation.t) -> c.reference) cs) | None, _ -> Alcotest.failf "%d-12-17 should resolve readings" y in check_year 2024 "of-advent-3-tuesday"; check_year 2029 "of-advent-3-monday" (* Same claim, the Christmas-season window, through a REALISTIC step 2 -> step 3 path: 2 January carries the fixed memorial of Saints Basil and Gregory Nazianzen (calendar-2002.sexp), which has no dedicated lectionary entry of its own, so both years fall through to the day's own ferial -- exactly the shape 2024-01-02's own CLI-verified pre-fix drift took (that year served "1 John 3:22-4:6/Matt 4:12-17,23-25", 2029's weekday-keyed content, before this fix). 2024 (Tuesday) and 2025 (Thursday) are again two different non-Sunday weekdays. *) let test_christmas_date_keyed_no_drift () = let check_year y = let lectionary = real_lectionary () in let layer = real_sanctoral_layer () in let index = L.index layer ~easter:Computus.gregorian_easter ~years:[ y - 1; y ] in let date = d y 1 2 in (match L.on_date index date with | [ e ] -> Alcotest.(check string) (Printf.sprintf "%d-01-02: Basil/Gregory's own entry" y) "saints-basil-the-great-and-gregory-nazianzen-bishops-and-doctors" (Slug.to_string e.L.cel.Cel.slug) | es -> Alcotest.failf "%d-01-02: expected exactly one sanctoral entry, got %d" y (List.length es)); match readings_on lectionary index date with | Some fm, cs -> Alcotest.(check bool) (Printf.sprintf "%d: tagged Own_slug (fell through to the ferial)" y) true (fm.Mass_formulary.via = Mass_formulary.Own_slug); Alcotest.(check (list string)) (Printf.sprintf "%d: 2 January is 1 John 2:22-28 / John 1:19-28" y) [ "1 John 2:22-28"; "John 1:19-28" ] (List.map (fun (c : Colitur_kernel.Citation.t) -> c.reference) cs) | None, _ -> Alcotest.failf "%d-01-02 should resolve readings" y in check_year 2024; check_year 2025 let suite = [ Alcotest.test_case "Sunday cycle table (OLM n.66, straddles Advent I)" `Quick test_sunday_cycle_table; Alcotest.test_case "weekday cycle table (OLM n.69.4, straddles Advent I)" `Quick test_weekday_cycle_table; Alcotest.test_case "the two cycles are independent" `Quick test_cycles_are_independent; Alcotest.test_case "data/of/lectionary.sexp SHA-256 pinned" `Quick test_sha256_pinned; Alcotest.test_case "lectio source SHA-256 pinned in the header" `Quick test_source_sha256_pinned_in_header; Alcotest.test_case "a missing lectio source fails loudly, not silently" `Quick test_missing_source_fails_loudly; Alcotest.test_case "loads, 771 entries" `Quick test_load; Alcotest.test_case "2026 coverage: nothing unresolved (fix wave I8)" `Quick test_coverage_2026; Alcotest.test_case "2026 coverage: 365 days resolve" `Quick test_pinned_resolved_count; Alcotest.test_case "step 3: temporal ferial fallback" `Quick test_step3_temporal_ferial; Alcotest.test_case "step 2: sanctoral proper wins over the ferial" `Quick test_step2_sanctoral_wins; Alcotest.test_case "date_keyed_slug boundaries (OLM n. 69.3)" `Quick test_date_keyed_slug_boundaries; Alcotest.test_case "O-Antiphon date-keyed reading does not drift (17 December)" `Quick test_o_antiphon_no_drift; Alcotest.test_case "Christmas-season date-keyed reading does not drift (2 January)" `Quick test_christmas_date_keyed_no_drift ]