(* Task 1 (2026-08-25-colitur-of-phases-3-5): loads data/of/calendar-2002.sexp -- the General Roman Calendar transcribed from the 2002 Missale Romanum by tools/extract_of_calendar.py -- through Layer.load and checks it against counts independently re-derived from the shipped file itself (grep -c '(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 (the same "live hazard" test_sanctoral_ef.ml's own header names). 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 every entry's layer is Precedence_of.universal_layer. *) module L = Colitur_kernel.Layer module Cel = Colitur_kernel.Celebration module S = Colitur_kernel.Slug module DS = Colitur_kernel.Date_spec module D = Colitur_kernel.Date module Col = Colitur_kernel.Colour module Sub = Colitur_kernel.Subject module Lang = Colitur_kernel.Lang module Names = Colitur_kernel.Names module V = Rite_of.Vocab_of module PO = Rite_of.Precedence_of module T = Rite_of.Temporal_of (* 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/of/calendar-2002.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 let sha256_of_file path = let tmp = Filename.temp_file "colitur_calendar_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-25 against the shipped file itself, independently re-derived (`sha256sum data/of/calendar-2002.sexp`) -- not copied from the tool's own stdout run, per this project's "derive the new value, 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) (* Counts independently re-derived from the shipped file (`grep -c '(slug'`; `grep -oP '(?<=\(rank )[A-Za-z_]+' | sort | uniq -c`). 206 = 209 raw table rows minus the 3 dates (1 Jan, 6 Jan, 25 Dec) already computed by Temporal_of.named -- see test_excludes_temporal_of_dates below, which closes the loop on that exclusion rather than merely asserting the count moved. *) let test_load_and_counts () = let l = load () in Alcotest.(check int) "206 entries" 206 (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) "23 Festum" 23 (count (fun e -> e.L.cel.Cel.rank = V.Festum)); Alcotest.(check int) "67 Memoria_obligatoria" 67 (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 (count (fun e -> e.L.cel.Cel.status = Cel.Feast)); Alcotest.(check int) "every entry tagged Precedence_of.universal_layer" 206 (count (fun e -> e.L.cel.Cel.layer = PO.universal_layer)) let test_slugs_unique () = let l = load () in let slugs = List.map (fun e -> S.to_string e.L.cel.Cel.slug) l.L.entries in let sorted = List.sort_uniq compare slugs in Alcotest.(check int) "no duplicate slugs" (List.length slugs) (List.length sorted) let test_slugs_revalidate () = let l = load () in 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 (* Every date is a Fixed month/day; DS.fixed already rejects an out-of-range day against the leap-year maximum, so this also proves no entry claims a nonexistent day (e.g. 30 February). *) let test_dates_resolve_in_a_leap_year () = let l = load () in 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 (* Brief step 4's own named spot-checks, read straight off the Missal's printed Calendarium Romanum Generale page. *) let test_spot_check_conversion_of_paul () = let e = find (load ()) "the-conversion-of-saint-paul-apostle" in Alcotest.(check bool) "25 Jan" true (e.L.date = mkdate 1 25); Alcotest.(check bool) "Festum" true (e.L.cel.Cel.rank = V.Festum); (* IGMR 346(a)'s own explicit white exception for this date, despite the title's "apostoli" which would otherwise trigger the apostle/martyr-red default -- see tools/extract_of_calendar.py's WHITE_APOSTLE_EXCEPTIONS. *) Alcotest.(check bool) "white (IGMR 346(a) explicit exception)" true (e.L.cel.Cel.colour = Col.White) let test_spot_check_chair_of_peter () = let e = find (load ()) "chair-of-saint-peter-apostle" in Alcotest.(check bool) "22 Feb" true (e.L.date = mkdate 2 22); Alcotest.(check bool) "Festum" true (e.L.cel.Cel.rank = V.Festum); Alcotest.(check bool) "white (IGMR 346(a) explicit exception)" true (e.L.cel.Cel.colour = Col.White) let test_spot_check_presentation () = let e = find (load ()) "presentation-of-the-lord" in Alcotest.(check bool) "2 Feb" true (e.L.date = mkdate 2 2); Alcotest.(check bool) "Festum" true (e.L.cel.Cel.rank = V.Festum); Alcotest.(check bool) "subject Lord" true (e.L.cel.Cel.subject = Sub.Lord) let test_spot_check_francis_de_sales () = let e = find (load ()) "francis-de-sales-bishop-and-doctor" in Alcotest.(check bool) "24 Jan" true (e.L.date = mkdate 1 24); Alcotest.(check bool) "Memoria_obligatoria (printed grade word: Memoria)" true (e.L.cel.Cel.rank = V.Memoria_obligatoria) let test_spot_check_angela_merici () = let e = find (load ()) "angela-merici-virgin" in Alcotest.(check bool) "27 Jan" true (e.L.date = mkdate 1 27); Alcotest.(check bool) "blank grade -> Memoria_ad_libitum (calendar's own footnote)" true (e.L.cel.Cel.rank = V.Memoria_ad_libitum) (* 1 January (Mary, Mother of God) IS "Sollemnitas" on the Missal's own printed page, the brief's sixth named spot-check -- but it is DELIBERATELY ABSENT from this file: Phase 1's Temporal_of.named already computes it as a temporal-cycle office (m=1, dd=1), and shipping it here too would create two competing candidates for the same day (see tools/extract_of_calendar.py's own module docstring, point 2, and this file's SKIP_DATES table). Asserted both ways, so the exclusion is verified rather than merely claimed: absent from the data, present and correctly graded in the code that actually covers it. The same holds for 6 January (Epiphany) and 25 December (the Nativity). *) let test_excludes_temporal_of_dates () = let l = load () in let has_date m d = List.exists (fun e -> e.L.date = mkdate m d) l.L.entries in Alcotest.(check bool) "1 Jan absent from calendar-2002.sexp" false (has_date 1 1); Alcotest.(check bool) "6 Jan absent from calendar-2002.sexp" false (has_date 1 6); Alcotest.(check bool) "25 Dec absent from calendar-2002.sexp" false (has_date 12 25); let mkd m d = match D.make ~year:2026 ~month:m ~day:d with Ok d -> d | Error e -> failwith e in (match T.named (mkd 1 1) with | Some (_, _, colour, rank) -> Alcotest.(check bool) "1 Jan: Temporal_of.named yields Sollemnitas" true (rank = V.Sollemnitas); Alcotest.(check bool) "1 Jan: Temporal_of.named yields White" true (colour = Col.White) | None -> Alcotest.fail "1 Jan: Temporal_of.named returned None -- the office this file \ excludes on that basis does not actually exist in code"); (match T.named (mkd 1 6) with | Some (_, _, _, rank) -> Alcotest.(check bool) "6 Jan: Sollemnitas" true (rank = V.Sollemnitas) | None -> Alcotest.fail "6 Jan: Temporal_of.named returned None"); match T.named (mkd 12 25) with | Some (_, _, _, rank) -> Alcotest.(check bool) "25 Dec: Sollemnitas" true (rank = V.Sollemnitas) | None -> Alcotest.fail "25 Dec: Temporal_of.named returned None" let suite = ( "Calendar_of (data/of/calendar-2002.sexp)", [ Alcotest.test_case "SHA-256 pinned" `Quick test_sha256; Alcotest.test_case "load succeeds and counts match the shipped file" `Quick test_load_and_counts; Alcotest.test_case "slugs are unique" `Quick test_slugs_unique; Alcotest.test_case "slugs re-validate" `Quick test_slugs_revalidate; Alcotest.test_case "every date resolves in a leap year" `Quick test_dates_resolve_in_a_leap_year; Alcotest.test_case "spot-check: 25 Jan Festum, Conversion of Paul" `Quick test_spot_check_conversion_of_paul; Alcotest.test_case "spot-check: 22 Feb Festum, Chair of Peter" `Quick test_spot_check_chair_of_peter; Alcotest.test_case "spot-check: 2 Feb Festum, Presentation" `Quick test_spot_check_presentation; Alcotest.test_case "spot-check: 24 Jan Memoria, Francis de Sales" `Quick test_spot_check_francis_de_sales; Alcotest.test_case "spot-check: 27 Jan blank -> Memoria_ad_libitum, Angela Merici" `Quick 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 ] )