(* Loads the bootstrapped data/ef/sanctoral.sexp (Task 10, tools/ bootstrap_sanctoral.ml) through Layer.load and checks it against counts independently derived from the source INI itself (grep -c '^\[', grep '^rank' | sort | uniq -c -- see the task report), plus two named spot-checks against the INI's own text so a passing count can't hide the wrong 322 entries having been converted (task brief's "live hazard"). *) module L = Colitur_kernel.Layer module Cel = Colitur_kernel.Celebration module Citation = Colitur_kernel.Citation module S = Colitur_kernel.Slug module DS = Colitur_kernel.Date_spec module Col = Colitur_kernel.Colour module Sub = Colitur_kernel.Subject module Lang = Colitur_kernel.Lang module Names = Colitur_kernel.Names module V = Rite_ef.Vocab_ef module PE = Rite_ef.Precedence_ef (* Relative to this test's own build directory (_build/default/test/); made available there because test/dune declares it as a dep of the test stanza. *) let path = "../data/ef/sanctoral.sexp" let load () = match L.load V.rank_of_sexp path with | Ok l -> l | Error e -> Alcotest.failf "%s: failed to load: %s" path e let mkdate m d = match DS.fixed ~month:m ~day:d with Ok t -> t | Error e -> failwith e (* Counts re-derived 2026-08-12 (ef-rebootstrap) against the regenerated source INI directly (`grep -c '^\['` minus the [layer] header itself; `grep '^rank' | sort | uniq -c`) -- NOT copied from a `colitur day`/ bootstrap-tool run, per this project's own "derive the new value, don't transcribe it from output" pinning discipline. The source's SHA-256 moved 6a25e634... -> 1b303ef2..., correcting: 15 entries wrongly `rank = commemoration` that are really class-3 feasts (322 -> 327's own feast/ comm split moves 15 the other way before the net +5 below); 5 entries newly present (agnes-secundo, boniface-martyr, eusebius-confessor, evaristus, theodore -- all Commemoration_only), so 322 + 5 = 327 and 114 - 15 + 5 = 104. Class1 count (12) is untouched by this regeneration -- none of the 20 changed/added entries is Class1. *) let test_load_and_counts () = let l = load () in Alcotest.(check int) "327 entries" 327 (List.length l.L.entries); let commemoration_only = List.filter (fun e -> e.L.cel.Cel.status = Cel.Commemoration_only) l.L.entries in (* 102, not 104: ef-sanctoral-status promoted `ubaldus` (16 May) and `didacus` (13 November) to Feast. Both are ranked "III classis" outright in the Missal's own universal calendarium, with no commemoration rubric, and both have their own Mass entry in the Proprium taking its readings from a Common. They were the ONLY two defects found by auditing all 290 fixed-date entries against that calendarium -- see the register. *) Alcotest.(check int) "102 Commemoration_only" 102 (List.length commemoration_only); let class1 = List.filter (fun e -> e.L.cel.Cel.rank = V.Class1) l.L.entries in Alcotest.(check int) "12 Class1" 12 (List.length class1); let temporal_subjects = List.filter (fun e -> e.L.cel.Cel.subject = Sub.Temporal) l.L.entries in Alcotest.(check int) "no Subject.Temporal" 0 (List.length temporal_subjects); (* ef-rebootstrap fix round 1 (F8): a guard against WHOLESALE Polish-name loss, the exact defect lectio itself shipped once (a regeneration that dropped all 322 name.pl entries, the "SECOND Critical" the lectio side of this session's own work found and fixed -- register/ledger record). `parse_names`'s own `field_opt` change (this task's first commit) can legitimately drop to en-only for a genuinely pl-less source entry -- verified NOT to silently drop a PRESENT name -- but nothing before this assertion would have caught a regeneration that dropped pl WHOLESALE the way lectio's once did: `test_load_and_counts` checked entry/status/rank counts only, none of which move if every name.pl vanishes. Independently re-derived, not transcribed: `grep -c '^name.pl' tridentine-calendar.ini` = 322 (327 entries, 5 lack it -- the same 5 `parse_names` documents), cross-checked against `grep -c '(pl' data/ef/sanctoral.sexp` = 322 exactly. *) let with_pl = List.filter (fun e -> Names.find e.L.cel.Cel.names (Lang.of_string_exn "pl") <> None) l.L.entries in Alcotest.(check int) "322 of 327 entries carry a Polish name (5 legitimately do not)" 322 (List.length with_pl); (* Every slug already had to pass Slug.of_string during load (Slug.t_of_sexp is the validating parser -- an invalid slug would have failed the whole Layer.load with Error, never landing here silently). Re-checking is still worth doing explicitly, rather than resting entirely on "load didn't error", so a future loader change that weakens that guarantee has a test that would catch it directly. *) List.iter (fun e -> match S.of_string (S.to_string e.L.cel.Cel.slug) with | Ok _ -> () | Error msg -> Alcotest.failf "slug %s failed re-validation: %s" (S.to_string e.L.cel.Cel.slug) msg) l.L.entries; (* All 322 dates are Date_spec.Fixed month/day pairs constructed via Date_spec.fixed, which already rejects an out-of-range day for that month against the LEAP-year maximum -- so every one must resolve in a leap year (2028: divisible by 4, not by 100). *) List.iter (fun e -> match DS.resolve e.L.date ~year:2028 ~easter:(Colitur_kernel.Computus.gregorian_easter 2028) with | Some _ -> () | None -> Alcotest.failf "slug %s: date does not resolve in leap year 2028" (S.to_string e.L.cel.Cel.slug)) l.L.entries let find l slug = match L.find l (S.of_string_exn slug) with | Some e -> e | None -> Alcotest.failf "slug %s not found in %s" slug path (* Named spot-check 1: one of the 6 entries carrying an explicit `class` field in the source (INI lines: date=02-02, rank=class-2, colour=white, class=lord, name.en/name.pl as below). Exercises decision 1's non-default branch -- an entry where subject must NOT fall back to Saint. *) let test_spot_check_explicit_class () = let l = load () in let e = find l "purification-of-the-blessed-virgin-mary" in let cel = e.L.cel in Alcotest.(check bool) "date 02-02" true (e.L.date = mkdate 2 2); Alcotest.(check bool) "rank Class2" true (cel.Cel.rank = V.Class2); Alcotest.(check bool) "status Feast" true (cel.Cel.status = Cel.Feast); Alcotest.(check bool) "colour White" true (cel.Cel.colour = Col.White); Alcotest.(check bool) "subject Lord (from class = lord)" true (cel.Cel.subject = Sub.Lord); Alcotest.(check (option string)) "name.en" (Some "Purification of the Blessed Virgin Mary") (Names.find cel.Cel.names (Lang.of_string_exn "en")); Alcotest.(check (option string)) "name.pl" (Some "Oczyszczenie N. M. P.") (Names.find cel.Cel.names (Lang.of_string_exn "pl")); Alcotest.(check string) "layer tagged universal (Precedence_ef.universal_layer)" PE.universal_layer cel.Cel.layer (* Named spot-check 2: a `rank = commemoration` entry (INI lines: date=01-14, rank=commemoration, colour=white, no class, name.en/name.pl as below). Exercises decision 2 (Commemoration_only + inferred Class3) AND decision 1's default branch (no class field -> Saint) on the SAME entry. *) let test_spot_check_commemoration () = let l = load () in let e = find l "maur-abbot" in let cel = e.L.cel in Alcotest.(check bool) "date 01-15" true (e.L.date = mkdate 1 15); Alcotest.(check bool) "rank inferred Class3" true (cel.Cel.rank = V.Class3); Alcotest.(check bool) "status Commemoration_only" true (cel.Cel.status = Cel.Commemoration_only); Alcotest.(check bool) "colour White" true (cel.Cel.colour = Col.White); Alcotest.(check bool) "subject defaults to Saint (no class field)" true (cel.Cel.subject = Sub.Saint); Alcotest.(check (option string)) "name.en" (Some "St. Maur, Abbot") (Names.find cel.Cel.names (Lang.of_string_exn "en")); Alcotest.(check (option string)) "name.pl" (Some "św. Maura, Opata") (Names.find cel.Cel.names (Lang.of_string_exn "pl")) (* Task 3: sanctoral propers land on Celebration.citations, not a second lectionary-shaped file -- see the bootstrap tool's own reasoning. *) let test_sanctoral_carries_propers () = let l = load () in let e = find l "commemoration-of-the-baptism-of-the-lord" in let refs = List.map (fun c -> c.Citation.reference) e.L.cel.Cel.citations in Alcotest.(check (list string)) "13 January carries its proper Epistle and Gospel" [ "Isa 60:1-6"; "John 1:29-34" ] refs let test_commemoration_only_has_no_readings () = let l = load () in let bad = List.filter (fun e -> e.L.cel.Cel.status = Cel.Commemoration_only && e.L.cel.Cel.citations <> []) l.L.entries in Alcotest.(check int) "a commemoration contributes an oration, not a reading" 0 (List.length bad) let suite = ( "Sanctoral_ef (data/ef/sanctoral.sexp)", [ Alcotest.test_case "load succeeds and counts match the source INI" `Quick test_load_and_counts; Alcotest.test_case "spot-check: explicit class = lord (Purification of the BVM)" `Quick test_spot_check_explicit_class; Alcotest.test_case "spot-check: rank = commemoration (St. Maur, Abbot)" `Quick test_spot_check_commemoration; Alcotest.test_case "sanctoral entries carry their proper readings" `Quick test_sanctoral_carries_propers; Alcotest.test_case "Commemoration_only entries carry no readings" `Quick test_commemoration_only_has_no_readings ] )