(* 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 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 Alcotest.(check int) "104 Commemoration_only" 104 (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 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")) 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 ] )