open Colitur_kernel let slug = Slug.of_string_exn let cit part reference = { Citation.part; reference } let entry s cs = (slug s, cs) let ok_exn = function Ok x -> x | Error e -> Alcotest.fail e (* Stdlib-only substring search -- Astring is not among the frozen deps. *) let contains_substring ~needle haystack = let hn = String.length needle and hh = String.length haystack in let rec go i = i + hn <= hh && (String.sub haystack i hn = needle || go (i + 1)) in hn = 0 || go 0 let test_find_present () = let l = ok_exn (Lectionary.of_entries [ entry "ef-advent-sunday-1" [ cit Citation.First "Rom 13:11-14"; cit Citation.Gospel "Luke 21:25-33" ] ]) in match Lectionary.find l (slug "ef-advent-sunday-1") with | Some [ a; b ] -> Alcotest.(check string) "epistle" "Rom 13:11-14" a.Citation.reference; Alcotest.(check string) "gospel" "Luke 21:25-33" b.Citation.reference | Some _ -> Alcotest.fail "expected exactly two citations" | None -> Alcotest.fail "slug not found" let test_find_absent () = let l = ok_exn (Lectionary.of_entries []) in Alcotest.(check bool) "absent" true (Lectionary.find l (slug "ef-advent-sunday-1") = None) let test_duplicate_slug_rejected () = let e = entry "ef-advent-sunday-1" [ cit Citation.First "Rom 13:11-14" ] in match Lectionary.of_entries [ e; e ] with | Ok _ -> Alcotest.fail "duplicate slug must be rejected" | Error msg -> (* Strengthened beyond the brief's "non-empty and contains 'e'" check: the interface doc promises the error names the slug, so assert the offending slug string actually appears in the message. *) Alcotest.(check bool) "names the slug" true (contains_substring ~needle:"ef-advent-sunday-1" msg) let test_sexp_round_trip () = let l = ok_exn (Lectionary.of_entries [ entry "ef-lent-1-monday" [ cit Citation.First "Ezech 34:11-16"; cit Citation.Gospel "Matt 25:31-46" ] ]) in let l' = Lectionary.t_of_sexp (Lectionary.sexp_of_t l) in Alcotest.(check bool) "round trips" true (l = l') let with_temp_file contents f = let path = Filename.temp_file "lectionary_test" ".sexp" in let oc = open_out path in output_string oc contents; close_out oc; Fun.protect ~finally:(fun () -> Sys.remove path) (fun () -> f path) let load_is_error contents what = with_temp_file contents (fun path -> match Lectionary.load path with | Error _ -> () | Ok _ -> Alcotest.fail (what ^ ": expected Error, got Ok")) let test_load_never_raises () = load_is_error "((ef-advent-sunday-1 (" "unterminated list"; load_is_error "((ef-advent-sunday-1 ((part First) (reference \"x" "unterminated string"; load_is_error "" "empty file"; load_is_error "() ()" "more than one sexp"; load_is_error "not-a-list" "wrong shape"; load_is_error "((\"NOT A SLUG\" ()))" "invalid slug"; match Lectionary.load "/nonexistent/path/lectionary.sexp" with | Error _ -> () | Ok _ -> Alcotest.fail "missing file: expected Error, got Ok" let test_ef_data_file_loads () = match Lectionary.load "../data/ef/lectionary.sexp" with | Error e -> Alcotest.fail e | Ok l -> (* 119 lectio ini sections, translated/widened/renamed into colitur's own Temporal_ef vocabulary, PLUS hand-authored/derived entries (tools/bootstrap_lectionary.ml's own [colitur_keys]/ [vigil_entries]/[holy_week_entries]/[passion_tuesday_entry]/ [nativity_octave_entries]/[holy_name_entries]/ [epiphanytide_opening_entries]/[movable_feast_entries] comments have the full account, task 8 (branch ef-lectionary, fix rounds 1-2) then Task 9 (same branch, layer-4 oracle) below): "ef-passiontide-0-tuesday" is EXCLUDED (task 8 fix round 2, Important -- it is Holy Tuesday's data, not Passion Tuesday's, -1); the other five Passiontide sections rename 1:1 (no count change); the three Lent Ember sections rename 1:1 (no count change); "ef-advent-ember-sat"/"ef-september-ember-sat" are EXCLUDED, task 8 fix round 3 (Important -- Thomas the Apostle's/ Matthew's Mass verbatim, not the real Ember Saturday, -2); 119 - 1 - 2 = 116 real ini-translated entries. PLUS: one hand-authored entry sourced from lectio's SANCTORAL calendar, not this lectionary ini ([ef-nativity-vigil], +1); SIX hand-authored directly from the Missal, all of Holy Week ([holy_week_entries], +6); ONE hand-authored directly from the Missal, Passion Tuesday's own real citation ([passion_tuesday_entry], +1); TWO hand-authored directly from the Missal, replacing the two excluded Ember Saturdays above ([ember_saturday_corrections], +2); three hand-authored directly from the Missal, the fixed Nativity-Octave days ([nativity_octave_entries], +3). Task 9 (branch ef-lectionary, layer-4 oracle -- CORRECTED, [ef-holy-name-sunday]/[ef-holy-name] no longer widened/derived from "ef-christmas-sunday-0", see [colitur_keys]'s own CORRECTED note): TWO hand-authored directly from the Missal ([holy_name_entries], +2); FIFTEEN hand-authored directly from the Missal, three 5-weekday families ([epiphanytide_opening_entries], +15); TWO hand-authored directly from the Missal, two major movable feasts with previously no entry at all ([movable_feast_entries], +2). 116 + 1 + 6 + 1 + 2 + 3 + 2 + 15 + 2 = 148. *) Alcotest.(check int) "entry count" 148 (List.length (Lectionary.entries l)); (match Lectionary.find l (slug "ef-lent-1-monday") with | Some [ a; b ] -> Alcotest.(check string) "epistle" "Ezech 34:11-16" a.Citation.reference; Alcotest.(check string) "gospel" "Matt 25:31-46" b.Citation.reference | Some _ -> Alcotest.fail "expected exactly two citations" | None -> Alcotest.fail "ef-lent-1-monday missing"); (* Task 8's own renamed/derived/hand-authored keys, spot-checked here so a future regeneration that silently drops one of them fails loudly and locally, not only via the much bigger differential suite. *) let check_entry name first gospel = match Lectionary.find l (slug name) with | Some [ a; b ] -> Alcotest.(check string) (name ^ ": epistle") first a.Citation.reference; Alcotest.(check string) (name ^ ": gospel") gospel b.Citation.reference | Some _ -> Alcotest.fail (name ^ ": expected exactly two citations") | None -> Alcotest.fail (name ^ ": missing") in (* Task 9 (branch ef-lectionary, layer-4 oracle): CORRECTED -- [ef-holy-name-sunday]/[ef-holy-name] used to be pinned here at "Gal 4:1-7"/"Luke 2:33-40", the Sunday-within-the-Octave's own citation this generator used to (wrongly) borrow by analogy. missalemeum's own independent extraction (2026-01-04/2027-01-03, real Holy Name Sundays) disproved it; the Missal itself confirms the real value, both scans (scan1.txt:6579-6608, scan2.txt:7225-7297, "SANCTISSIMI NOMINIS IESU... Act. 4, 8-12... Luc. 2, 21") -- see [colitur_keys]'s own CORRECTED note in bootstrap_lectionary.ml for the full account. *) check_entry "ef-holy-name-sunday" "Acts 4:8-12" "Luke 2:21"; check_entry "ef-christmas-sunday-0" "Gal 4:1-7" "Luke 2:33-40"; check_entry "ef-holy-name" "Acts 4:8-12" "Luke 2:21"; (* Task 9: RG 17(a)'s own "diebus ferialibus" rubric (2-13 January, three 5-weekday families -- bootstrap_lectionary.ml's own [epiphanytide_opening_entries] comment has the full citations and the RG 78/BVM-Saturday reasoning for why Saturday is excluded from all three). One weekday pinned per family (a regression net, not an exhaustive per-weekday audit -- the generator's own [assert_reachable] already guards every emitted key is a real Temporal_ef slug). *) check_entry "ef-christmas-1-monday" "Titus 2:11-15" "Luke 2:21"; check_entry "ef-christmas-1-friday" "Titus 2:11-15" "Luke 2:21"; check_entry "ef-christmas-2-tuesday" "Isa 60:1-6" "Matt 2:1-12"; check_entry "ef-christmas-2-monday" "Isa 60:1-6" "Matt 2:1-12"; check_entry "ef-time-after-epiphany-1-wednesday" "Rom 12:1-5" "Luke 2:42-52"; let must_stay_absent name = match Lectionary.find l (slug name) with | None -> () | Some _ -> Alcotest.fail (name ^ ": must stay absent -- RG 78's BVM Saturday Office") in must_stay_absent "ef-christmas-1-saturday"; must_stay_absent "ef-christmas-2-saturday"; must_stay_absent "ef-time-after-epiphany-1-saturday"; (* Task 9: two major, permanently-named I-class movable feasts with no entry at all before this task (bootstrap_lectionary.ml's own [movable_feast_entries] comment has the full citations, both scans). *) check_entry "ef-corpus-christi" "1 Cor 11:23-29" "John 6:56-59"; check_entry "ef-sacred-heart" "Eph 3:8-12, 14-19" "John 19:31-37"; check_entry "ef-nativity-vigil" "Rom 1:1-6" "Matt 1:18-21"; check_entry "ef-pentecost-ember-wed" "Acts 5:12-16" "John 6:44-52."; check_entry "ef-pentecost-ember-fri" "Joel 2:23-24; 26-27" "Luke 5:17-26"; check_entry "ef-pentecost-ember-sat" "Rom 5:1-5." "Luke 4:38-44."; (* Critical 2 fix (coordinator review round 1): the Lenten Ember days, previously dead keys under lectio's own "ef-lent-1-" naming -- now reachable under colitur's own slugs. Minor, fix round 2 (coordinator review): all THREE are pinned, not just Wednesday -- a mutation on Friday's or Saturday's own value used to leave the whole suite green. *) check_entry "ef-lent-ember-wed" "3 Kgs. 19:3-8" "Matt 12:38-50"; check_entry "ef-lent-ember-fri" "Ezech 18:20-28" "John 5:1-15"; check_entry "ef-lent-ember-sat" "1 Thess. 5:14-23" "Matt 17:1-9"; (* Passiontide widens into Passion week ONLY now (Critical 1 fix) -- "ef-passiontide-1-monday" keeps lectio's own citation; "ef-passiontide-2-monday" (Holy Monday) is Missal-sourced, DIFFERENT text, hand-authored in [holy_week_entries]. Fix round 2 (Important): "ef-passiontide-1-tuesday" no longer carries lectio's own (wrong -- Holy Tuesday's) ini value; it is hand-authored from the Missal's own real Passion Tuesday Mass, [passion_tuesday_entry]. *) check_entry "ef-passiontide-1-monday" "Jonas 3:1-10" "John 7:32-39"; check_entry "ef-passiontide-1-tuesday" "Dan 14:27, 28-42" "John 7:1-13"; check_entry "ef-passiontide-2-monday" "Isa 50:5-10" "John 12:1-9"; (* Gospel punctuation ("15, 1-46") intentionally kept as lectio's own ini already has it -- see bootstrap_lectionary.ml's own [holy_week_entries] comment on this one entry. *) check_entry "ef-passiontide-2-tuesday" "Jer 11:18-20" "Mark 14:32-72; 15, 1-46"; (* Critical fix, round 2: Wednesday and Good Friday were left entirely absent by the first pass, which meant step 3 quietly resumed the preceding Sunday's Mass there (Good Friday emitting PALM SUNDAY's Epistle and Passion) -- the FIRST pass's own reasoning ("neither day has a single Epistle") was right about the First slot alone; the Gospel is always unambiguous and is now always authored, and First uses the SAME "last lesson before the Gospel" convention this file's own Lenten Ember Wednesday entry above already uses for an identical two-lesson shape. *) check_entry "ef-passiontide-2-wednesday" "Isa 53:1-12" "Luke 22:39-71; 23:1-53"; check_entry "ef-passiontide-2-thursday" "1 Cor 11:20-32" "John 13:1-15"; check_entry "ef-passiontide-2-friday" "Ex 12:1-11" "John 18:1-40; 19:1-42"; check_entry "ef-passiontide-2-saturday" "Col 3:1-4" "Matt 28:1-7"; (* Important 3(a) fix (coordinator review round 1): the fixed Nativity-Octave days now carry their own direct Missal formulary ("Diebus infra octavam Nativitatis Domini"), not Holy Name Sunday's (a first guess, measured wrong, reverted -- see [nativity_octave_entries]'s own comment). *) check_entry "ef-nativity-octave-day-5" "Titus 3:4-7" "Luke 2:15-20"; check_entry "ef-nativity-octave-day-6" "Titus 3:4-7" "Luke 2:15-20"; check_entry "ef-nativity-octave-day-7" "Titus 3:4-7" "Luke 2:15-20"; (* Fix round 3 (coordinator review, Important): two more Ember Saturdays that inherited a saint's Mass verbatim from lectio's ini, the exact C26 shape -- pinned here so a future regeneration that silently reverts to the wrong (saint's) value fails loudly and locally, the same discipline the Lent Ember/Passion Tuesday pins above already established for their own fix rounds. Both readings are explicitly labelled "Lectio Epistolae" in the Missal, so neither needed the "last lesson before the Gospel" convention Holy Wednesday/Good Friday above rely on. *) check_entry "ef-advent-ember-sat" "2 Thess 2:1-8" "Luke 3:1-6"; check_entry "ef-september-ember-sat" "Heb 9:2-12" "Luke 13:6-17"; (* Lent's OWN "ef-lent-1-wednesday/friday/saturday" keys must not reappear once renamed away (colitur never computes them as its own slug at all) -- a regression net for that decision, not merely documentation of it. *) match Lectionary.find l (slug "ef-lent-1-wednesday") with | None -> () | Some _ -> Alcotest.fail "ef-lent-1-wednesday: must stay absent -- see bootstrap_lectionary.ml" let suite = [ ("find present", `Quick, test_find_present); ("find absent", `Quick, test_find_absent); ("duplicate slug rejected", `Quick, test_duplicate_slug_rejected); ("sexp round-trip", `Quick, test_sexp_round_trip); ("load never raises", `Quick, test_load_never_raises); ("ef data file loads", `Quick, test_ef_data_file_loads) ]