1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
|
(* 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 ] )
|