aboutsummaryrefslogtreecommitdiff
path: root/test/test_rubrics_of.ml
blob: c29de8e2d406b64b90604afc2296de67ac258fdd (plain) (blame)
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
(* IGMR n. 53 (Gloria), n. 67-68 (Symbolum/Credo), n. 364-365 and the
   Missale's own INDEX PRAEFATIONUM -- see lib/rites/rite_of/rubrics_of.ml
   for every rubric quoted in full and every branch's own citation. The OF
   counterpart of test/test_rubrics_ef.ml; one Alcotest case per rule.

   NO FULL OF [Colitur_kernel.Rite.t]/[Calendar] EXISTS YET (this task
   supplies three of its four remaining fields; wiring them into a bundle
   is a later task) -- so, unlike test_rubrics_ef.ml, there is no
   `Cal.day`-style pipeline here to resolve a real SANCTORAL winner for a
   date. Every TEMPORAL-origin row below therefore builds [temporal] (and,
   where the temporal cycle's own office IS what is observed -- an ordinary
   Sunday, a feria, a named solemnity like the Nativity) also [observed],
   via [Temporal_of.temporal] on a real calendar date -- genuine Phase 1
   output, per the task brief. Every SANCTORAL-origin row (a saint's
   solemnity/feast/memorial outranking the day's own temporal office)
   builds [temporal] the same way, via a real date, but necessarily builds
   [observed] directly -- there is no pipeline yet to derive it. It is
   NOT fabricated: every field is copied verbatim from the matching real
   entry in data/of/calendar-2002.sexp (cited at each site), the same
   "grounded in shipped data, not invented" discipline
   test/test_precedence_of.ml's own [mk_cel] already establishes for this
   rite. *)

module RO = Rite_of.Rubrics_of
module T = Rite_of.Temporal_of
module D = Colitur_kernel.Date
module Cel = Colitur_kernel.Celebration
module Colour = Colitur_kernel.Colour
module Subject = Colitur_kernel.Subject
module Slug = Colitur_kernel.Slug
module Vocab_of = Rite_of.Vocab_of

let mk y m d = match D.make ~year:y ~month:m ~day:d with Ok t -> t | Error e -> failwith e

(* A real, shipped sanctoral entry (data/of/calendar-2002.sexp), reused
   directly as [observed] where no full precedence pipeline exists yet to
   derive it. Status defaults to [Feast] (every entry cited below is). *)
let mk_saint ~slug ~rank ~colour ~subject () =
  Cel.make ~slug:(Slug.of_string_exn slug) ~rank ~colour ~subject ~layer:"of-universal" ()

(* ---- Gloria (IGMR n. 53) ---- *)

let gloria_on_temporal y m d =
  let temporal = T.temporal (mk y m d) in
  RO.gloria ~temporal ~observed:temporal.Colitur_kernel.Temporal.office ~date:(mk y m d)

let check_gloria name expected actual = Alcotest.(check bool) name expected actual

let test_gloria_advent_sunday () =
  (* 2026-12-06: the Second Sunday of Advent -- confirmed via `date -d`,
     [Temporal_of.temporal] tags it [Vocab_of.Sollemnitas] (the privileged-
     season convention, temporal_of.ml's own [build]), which is exactly the
     trap n. 53's own "extra tempus Adventus" clause exists to catch: a
     naive [observed.rank = Sollemnitas] test would wrongly say [true]. *)
  check_gloria "no Gloria on a Sunday of Advent" false (gloria_on_temporal 2026 12 6)

let test_gloria_lent_sunday () =
  (* 2026-03-01: the First Sunday of Lent 2026 (Ash Wednesday 2026-02-18,
     Easter 2026-04-05). Same trap as Advent, the other of n. 53's two
     named exceptions. *)
  check_gloria "no Gloria on a Sunday of Lent" false (gloria_on_temporal 2026 3 1)

let test_gloria_palm_sunday () =
  (* 2026-03-29: Palm Sunday, [Vocab_of.Lent], [Sollemnitas] (named's own
     entry) -- confirms the Advent/Lent exclusion above also correctly
     reaches a NAMED solemnity, not only a plain numbered Sunday of Lent,
     matching real, uncontested practice (Palm Sunday's Mass has no
     Gloria). *)
  check_gloria "no Gloria on Palm Sunday" false (gloria_on_temporal 2026 3 29)

let test_gloria_ordinary_sunday () =
  (* 2026-01-25: an Ordinary Time Sunday (Baptism of the Lord 2026-01-11,
     so Ordinary Time begins 2026-01-12) -- [Festum]-ranked
     (temporal_of.ml's own [build]), reaching [true] via n. 53's "diebus
     dominicis" clause, not its "et festis" one (see the .ml's own
     citation for why [temporal.weekday], not [observed.rank], is what
     this must be read off). *)
  check_gloria "Gloria on a Sunday per annum" true (gloria_on_temporal 2026 1 25)

let test_gloria_christmas_sunday () =
  (* 2026-12-27: a Sunday within the Octave of the Nativity (temporal_of.ml's
     own [holy_family]/generic-Sunday dispatch), [Vocab_of.Christmas]
     season, outside Advent/Lent -- a second, independent witness to the
     same "diebus dominicis extra Adventum/Quadragesimam" clause as the
     Ordinary Time row above, from the other season n. 53 does not
     exclude. *)
  check_gloria "Gloria on a Sunday of Christmas time" true (gloria_on_temporal 2026 12 27)

let test_gloria_nativity () =
  (* 2026-12-25: the Nativity itself -- [Vocab_of.Christmas], [Sollemnitas],
     a Friday (never a Sunday that year), reaching [true] via n. 53's "in
     sollemnitatibus" clause on its own, independent of the Sunday
     clauses. Pure [Temporal_of.temporal] output; no sanctoral data
     needed, since the Nativity IS the temporal cycle's own named office
     (temporal_of.ml's own [named]). *)
  check_gloria "Gloria on the Nativity" true (gloria_on_temporal 2026 12 25)

let test_gloria_holy_thursday () =
  (* 2026-04-02: the Mass of the Lord's Supper (Easter-3, Easter 2026-04-05)
     -- Missale Romanum, "FERIA V IN CENA DOMINI" n. 7's own explicit
     "Dicitur Gloria in excelsis" (see the .ml's own citation), overriding
     what would otherwise be a Lenten-feria [false]. *)
  check_gloria "Gloria at the Mass of the Lord's Supper" true (gloria_on_temporal 2026 4 2)

let test_gloria_easter_vigil () =
  (* 2026-04-04: the Easter Vigil (Easter-1) -- the same Missale rubric's
     own "usque ad Gloria in excelsis Vigiliae paschalis", confirming this
     Mass also has one. *)
  check_gloria "Gloria at the Easter Vigil Mass" true (gloria_on_temporal 2026 4 4)

let test_gloria_solemnity_in_lent () =
  (* Saint Joseph, Husband of the Blessed Virgin Mary, 19 March --
     data/of/calendar-2002.sexp: (rank Sollemnitas) (colour White)
     (subject Saint). 2026-03-19 is a Thursday (Easter 2026-04-05, so Holy
     Week is 2026-03-29..04-04 -- this date sits safely inside ordinary
     Lent, not impeded) -- confirmed, not assumed, via `date -d`. 19 March
     is ALWAYS inside Lent by construction (Gregorian Easter's earliest
     possible date is 22 March, so Ash Wednesday can never fall later than
     10 March; see the .ml's own citation). Exercises n. 53's "in
     sollemnitatibus" clause on a genuine sanctoral winner, the one row in
     this suite [observed] cannot come from [Temporal_of.temporal] alone
     -- see this file's own header for why. *)
  let temporal = T.temporal (mk 2026 3 19) in
  let observed =
    mk_saint ~slug:"joseph-husband-of-the-blessed-virgin-mary" ~rank:Vocab_of.Sollemnitas
      ~colour:Colour.White ~subject:Subject.Saint ()
  in
  check_gloria "Gloria: a solemnity falling in Lent" true
    (RO.gloria ~temporal ~observed ~date:(mk 2026 3 19))

let test_gloria_festum () =
  (* Chair of Saint Peter, apostle, 22 February -- data/of/calendar-2002.sexp:
     (rank Festum) (colour White) (subject Saint). 2027-02-22 is a Monday
     (checked via `date -d`, so this date is not itself impeded by an
     Ordinary Time Sunday outranking a Festum, Tabula entries 6 vs 7).
     Exercises n. 53's "et festis" clause specifically -- the one member of
     n. 53's list that n. 68 (Credo, below) does NOT share. *)
  let temporal = T.temporal (mk 2027 2 22) in
  let observed =
    mk_saint ~slug:"chair-of-saint-peter-apostle" ~rank:Vocab_of.Festum ~colour:Colour.White
      ~subject:Subject.Saint ()
  in
  check_gloria "Gloria on an ordinary Festum" true (RO.gloria ~temporal ~observed ~date:(mk 2027 2 22))

let test_gloria_obligatory_memorial () =
  (* Saint Aloysius Gonzaga, religious, 21 June -- data/of/calendar-2002.sexp:
     (rank Memoria_obligatoria) (colour White) (subject Saint). 2027-06-21
     is a Monday. Neither n. 53's "sollemnitatibus et festis" nor its
     "dominicis" clause names a memorial. *)
  let temporal = T.temporal (mk 2027 6 21) in
  let observed =
    mk_saint ~slug:"aloysius-gonzaga-religious" ~rank:Vocab_of.Memoria_obligatoria
      ~colour:Colour.White ~subject:Subject.Saint ()
  in
  check_gloria "no Gloria on a weekday obligatory memorial" false
    (RO.gloria ~temporal ~observed ~date:(mk 2027 6 21))

let test_gloria_ordinary_feria () =
  (* 2026-01-27: an Ordinary Time Tuesday feria, no memorial -- pure
     [Temporal_of.temporal] output, [Vocab_of.Feria], [Subject.Temporal].
     Named by neither n. 53 clause. *)
  check_gloria "no Gloria on a plain feria" false (gloria_on_temporal 2026 1 27)

let test_gloria_all_souls () =
  (* All Souls, 2 November -- data/of/calendar-2002.sexp: (rank Sollemnitas)
     (colour Violet) (subject Saint) -- the ONE Requiem-shaped entity this
     engine's shipped OF data can construct as [observed]
     ({!Rite_of.Rubrics_of.requiem_slugs}). Its own [rank] is tagged
     [Sollemnitas] by the extractor's own override (precedence_of.ml's own
     citation), so WITHOUT the requiem guard n. 53's "in sollemnitatibus"
     clause would wrongly grant it [true] -- exactly the case this guard
     exists for. 2026-11-02 is a Monday. *)
  let temporal = T.temporal (mk 2026 11 2) in
  let observed =
    mk_saint ~slug:"all-souls" ~rank:Vocab_of.Sollemnitas ~colour:Colour.Violet ~subject:Subject.Saint ()
  in
  check_gloria "no Gloria on All Souls" false (RO.gloria ~temporal ~observed ~date:(mk 2026 11 2))

(* ---- Credo / Symbolum (IGMR n. 67-68) ---- *)

let credo_on_temporal y m d =
  let temporal = T.temporal (mk y m d) in
  RO.creed ~temporal ~observed:temporal.Colitur_kernel.Temporal.office ~date:(mk y m d)

let check_credo name expected actual = Alcotest.(check bool) name expected actual

let test_credo_advent_sunday () =
  (* Same date as [test_gloria_advent_sunday], deliberately: n. 68 carries
     no Advent/Lent exception at all, unlike n. 53 -- this is [true] here
     where the identical day was [false] for Gloria, proving the two rules
     genuinely diverge rather than one silently mirroring the other. *)
  check_credo "Credo on a Sunday of Advent (unlike Gloria)" true (credo_on_temporal 2026 12 6)

let test_credo_ordinary_sunday () =
  check_credo "Credo on a Sunday per annum" true (credo_on_temporal 2026 1 25)

let test_credo_solemnity () =
  (* Same St Joseph data as [test_gloria_solemnity_in_lent]. *)
  let temporal = T.temporal (mk 2026 3 19) in
  let observed =
    mk_saint ~slug:"joseph-husband-of-the-blessed-virgin-mary" ~rank:Vocab_of.Sollemnitas
      ~colour:Colour.White ~subject:Subject.Saint ()
  in
  check_credo "Credo: a solemnity" true (RO.creed ~temporal ~observed ~date:(mk 2026 3 19))

let test_credo_festum () =
  (* Same Chair-of-Peter data as [test_gloria_festum] -- [false] here,
     unlike Gloria's [true] on the identical day: n. 68 has no "et festis"
     clause at all (see the .ml's own citation), the single sharpest
     divergence between this function and [gloria]. *)
  let temporal = T.temporal (mk 2027 2 22) in
  let observed =
    mk_saint ~slug:"chair-of-saint-peter-apostle" ~rank:Vocab_of.Festum ~colour:Colour.White
      ~subject:Subject.Saint ()
  in
  check_credo "no Credo on an ordinary Festum (unlike Gloria)" false
    (RO.creed ~temporal ~observed ~date:(mk 2027 2 22))

let test_credo_weekday_obligatory_memorial () =
  let temporal = T.temporal (mk 2027 6 21) in
  let observed =
    mk_saint ~slug:"aloysius-gonzaga-religious" ~rank:Vocab_of.Memoria_obligatoria
      ~colour:Colour.White ~subject:Subject.Saint ()
  in
  check_credo "no Credo on a weekday obligatory memorial" false
    (RO.creed ~temporal ~observed ~date:(mk 2027 6 21))

let test_credo_ordinary_feria () =
  check_credo "no Credo on a plain feria" false (credo_on_temporal 2026 1 27)

let test_credo_all_souls () =
  let temporal = T.temporal (mk 2026 11 2) in
  let observed =
    mk_saint ~slug:"all-souls" ~rank:Vocab_of.Sollemnitas ~colour:Colour.Violet ~subject:Subject.Saint ()
  in
  check_credo "no Credo on All Souls" false (RO.creed ~temporal ~observed ~date:(mk 2026 11 2))

(* ---- Preface -- always [None]; see rubrics_of.ml's own header for the
   full INDEX-PRAEFATIONUM census this rests on. One row per major class,
   proving the function was deliberately built to reject every shape
   rather than merely left unimplemented. ---- *)

let check_preface_none name y m d =
  let temporal = T.temporal (mk y m d) in
  Alcotest.(check bool) name true
    (RO.preface ~temporal ~observed:temporal.Colitur_kernel.Temporal.office ~date:(mk y m d) = None)

let test_preface_none_nativity () = check_preface_none "preface: Nativity is None" 2026 12 25
let test_preface_none_ordinary_sunday () = check_preface_none "preface: Sunday per annum is None" 2026 1 25
let test_preface_none_feria () = check_preface_none "preface: a plain feria is None" 2026 1 27

let test_preface_none_solemnity_with_own_single_text () =
  (* Trinity Sunday -- exactly the "single OF text, same feast as an EF
     constructor" near-miss the .ml's own header explains rejecting: still
     [None], on purpose. *)
  let easter = Colitur_kernel.Computus.gregorian_easter 2026 in
  let trinity = D.add_days easter 56 in
  let temporal = T.temporal trinity in
  Alcotest.(check bool) "preface: Trinity Sunday is None" true
    (RO.preface ~temporal ~observed:temporal.Colitur_kernel.Temporal.office ~date:trinity = None)

let test_preface_none_all_souls () =
  let temporal = T.temporal (mk 2026 11 2) in
  let observed =
    mk_saint ~slug:"all-souls" ~rank:Vocab_of.Sollemnitas ~colour:Colour.Violet ~subject:Subject.Saint ()
  in
  Alcotest.(check bool) "preface: All Souls is None" true
    (RO.preface ~temporal ~observed ~date:(mk 2026 11 2) = None)

(* Population guard, mirroring test_rubrics_ef.ml's own
   [test_colour_black_population_is_exactly_two]: fails loudly the day a
   second Requiem-shaped slug is added anywhere in the shipped OF data
   without this list being revisited. *)
let test_requiem_slugs_population () =
  Alcotest.(check (list string)) "requiem_slugs is exactly {all-souls}" [ "all-souls" ] RO.requiem_slugs

let suite =
  ( "rubrics-of",
    [ Alcotest.test_case "Gloria: no Gloria on a Sunday of Advent" `Quick test_gloria_advent_sunday;
      Alcotest.test_case "Gloria: no Gloria on a Sunday of Lent" `Quick test_gloria_lent_sunday;
      Alcotest.test_case "Gloria: no Gloria on Palm Sunday" `Quick test_gloria_palm_sunday;
      Alcotest.test_case "Gloria: Gloria on a Sunday per annum" `Quick test_gloria_ordinary_sunday;
      Alcotest.test_case "Gloria: Gloria on a Sunday of Christmas time" `Quick test_gloria_christmas_sunday;
      Alcotest.test_case "Gloria: Gloria on the Nativity" `Quick test_gloria_nativity;
      Alcotest.test_case "Gloria: Gloria at the Mass of the Lord's Supper" `Quick test_gloria_holy_thursday;
      Alcotest.test_case "Gloria: Gloria at the Easter Vigil Mass" `Quick test_gloria_easter_vigil;
      Alcotest.test_case "Gloria: a solemnity falling in Lent" `Quick test_gloria_solemnity_in_lent;
      Alcotest.test_case "Gloria: an ordinary Festum" `Quick test_gloria_festum;
      Alcotest.test_case "Gloria: no Gloria on a weekday obligatory memorial" `Quick
        test_gloria_obligatory_memorial;
      Alcotest.test_case "Gloria: no Gloria on a plain feria" `Quick test_gloria_ordinary_feria;
      Alcotest.test_case "Gloria: no Gloria on All Souls" `Quick test_gloria_all_souls;
      Alcotest.test_case "Credo: Credo on a Sunday of Advent" `Quick test_credo_advent_sunday;
      Alcotest.test_case "Credo: Credo on a Sunday per annum" `Quick test_credo_ordinary_sunday;
      Alcotest.test_case "Credo: a solemnity" `Quick test_credo_solemnity;
      Alcotest.test_case "Credo: no Credo on an ordinary Festum" `Quick test_credo_festum;
      Alcotest.test_case "Credo: no Credo on a weekday obligatory memorial" `Quick
        test_credo_weekday_obligatory_memorial;
      Alcotest.test_case "Credo: no Credo on a plain feria" `Quick test_credo_ordinary_feria;
      Alcotest.test_case "Credo: no Credo on All Souls" `Quick test_credo_all_souls;
      Alcotest.test_case "Preface: Nativity is None" `Quick test_preface_none_nativity;
      Alcotest.test_case "Preface: Sunday per annum is None" `Quick test_preface_none_ordinary_sunday;
      Alcotest.test_case "Preface: a plain feria is None" `Quick test_preface_none_feria;
      Alcotest.test_case "Preface: Trinity Sunday is None" `Quick test_preface_none_solemnity_with_own_single_text;
      Alcotest.test_case "Preface: All Souls is None" `Quick test_preface_none_all_souls;
      Alcotest.test_case "requiem_slugs population is exactly {all-souls}" `Quick
        test_requiem_slugs_population ] )