summaryrefslogtreecommitdiff
path: root/test/test_rubrics_ef.ml
blob: 2000d61da817b4ef1b970d4e82e4ed4302501902 (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
(* RG 475-476, the Creed -- see lib/rites/rite_ef/rubrics_ef.ml for the
   rubric quoted in full and every branch's own citation.

   One end-to-end test per clause of 475, plus a 476 negative, resolved
   against REAL calendar dates through the shipped sanctoral data (the same
   pipeline `colitur day`/`colitur rubrics` use) -- every expected value
   below was derived from the rubric's own text and checked against
   `colitur day <year>`'s real output (slug/rank/subject/weekday) BEFORE
   this module existed, never read off [Rubrics_ef.creed]'s own answer.
   Two synthetic unit tests isolate Trap One (RG 475(a) reads [temporal],
   never [observed]) directly, without depending on finding a real-calendar
   coincidence. *)

module Cal = Colitur_kernel.Calendar
module LD = Colitur_kernel.Liturgical_day
module Date = Colitur_kernel.Date
module Cel = Colitur_kernel.Celebration
module Colour = Colitur_kernel.Colour
module Subject = Colitur_kernel.Subject
module Slug = Colitur_kernel.Slug
module Temporal = Colitur_kernel.Temporal
module V = Rite_ef.Vocab_ef
module RE = Rite_ef.Rubrics_ef

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

(* Loaded once, module-level: every test below is a lookup against the same
   shipped calendar, and {!Cal.day} recomputes its whole liturgical year on
   every call (calendar.mli's own documented cost), so at minimum the layer
   itself should not be reloaded and re-merged per test case. *)
let layer =
  match Test_support.load_ef_layer () with Ok l -> l | Error e -> Alcotest.failf "%s" e

let ctx = Test_support.ef_context ()

let creed_on y m d = (Cal.day ctx layer (mk y m d)).LD.creed

let check name y m d expected = Alcotest.(check bool) name expected (creed_on y m d)

(* ---- RG 475(a): "in qualibet dominica, etsi eius Officium alicui festo
   locum cedat" ---- *)

let test_475a_ordinary_sunday () =
  (* 2026-01-25: an ordinary Time-after-Epiphany Sunday, Class2, green,
     "Dominica III post Epiphaniam" -- confirmed via `colitur day 2026`,
     no other clause of 475 could apply (Class2, subject Temporal, no
     octave, no apostle/vigil slug), so [true] here can only come from
     475(a) itself. *)
  check "475(a): an ordinary Sunday" 2026 1 25 true

(* Trap One, isolated directly: RG 475(a)'s own "ETSI EIUS OFFICIUM ALICUI
   FESTO LOCUM CEDAT" -- even when a feast has displaced the Sunday's own
   office, the Creed is still said. [observed] below is deliberately shaped
   so that NONE of 475(b)/(c)/(e) can produce [true] on their own (Class3,
   subject Saint, a slug on no list this module knows); the only way
   [creed] can return [true] is by reading [temporal]'s own [weekday],
   never [observed]. The synthetic [date] (an ordinary July day, itself a
   Wednesday in 2026) is chosen so nothing about the DATE itself suggests
   a Sunday either -- proving the function reads [temporal.weekday], not
   [Date.weekday date]. *)
let impeded_observed =
  Cel.make ~slug:(Slug.of_string_exn "some-impeding-feast-of-the-lord") ~rank:V.Class3
    ~status:Cel.Feast ~colour:Colour.Red ~subject:Subject.Saint ~layer:"synthetic" ()

let synthetic_temporal ~weekday : (V.season, V.rank) Temporal.t =
  { Temporal.season = V.Time_after_pentecost; week = Some 1; weekday; office = impeded_observed }

let test_475a_reads_temporal_not_observed () =
  Alcotest.(check bool) "Sunday-shaped [temporal] overrides a non-Sunday-shaped [observed]" true
    (RE.creed ~temporal:(synthetic_temporal ~weekday:Date.Sun) ~observed:impeded_observed
       ~date:(mk 2026 7 1));
  Alcotest.(check bool) "same [observed], non-Sunday [temporal]: false" false
    (RE.creed ~temporal:(synthetic_temporal ~weekday:Date.Wed) ~observed:impeded_observed
       ~date:(mk 2026 7 1))

(* ---- RG 475(b): "in festis I classis" ---- *)

let test_475b_class1_feast () =
  (* 2026-08-15: the Assumption, Class1. *)
  check "475(b): a I-class feast" 2026 8 15 true

(* ---- RG 475(c): "in festis II classis Domini et B. Mariae Virg." ---- *)

let test_475c_lord () =
  (* 2026-09-14: Exaltation of the Holy Cross, Class2, subject Lord. *)
  check "475(c): a II-class feast of the Lord" 2026 9 14 true

let test_475c_bvm_via_marian_slugs () =
  (* 2026-08-22: Immaculate Heart of Mary, Class2 -- ships [subject =
     Saint] in data/ef/sanctoral.sexp (confirmed by grep), so this can only
     come out [true] via {!Rite_ef.Precedence_ef.marian_slugs}, not via
     [subject = Bvm]. Also carries a real commemoration
     (+sts-timothy-hippolytus-and-symphorianus-martyrs), a live instance of
     476(e): the commemoration plays no part in this answer. *)
  check "475(c): a II-class BVM feast (via marian_slugs, subject=Saint in the data)" 2026 8 22 true

(* ---- RG 475(d): "per octavas Nativitatis Domini, Paschatis et
   Pentecostes, etiam in festis occurrentibus et in Missis votivis" ---- *)

let test_475d_octave_even_occurring_feast () =
  (* 2026-12-26: St Stephen, Class2, "S. Stephani Protomartyris" -- a real
     saint's feast OCCURRING within the Octave of the Nativity (RG 67's own
     "Com. octavae Nativitatis", carried as +ef-nativity-octave-day-2 in
     colitur's own commemoration). RG 475(d)'s own "etiam in festis
     occurrentibus" is written for exactly this shape: the Creed is said
     regardless. (This is the one place this suite deliberately diverges
     from the task brief's own worked example, which expected [false] here
     -- the brief mis-cited 26 December as 476(b)'s "plain II-class feast"
     case, missing that RG 475(d) explicitly overrides 476(b) inside the
     Nativity octave; see the task report.) *)
  check "475(d): a saint's feast occurring within the Nativity octave" 2026 12 26 true

let test_475d_octave_day_boundary () =
  (* 2026-01-01: the Octave Day of the Nativity itself (Circumcision),
     Class1 -- also [true] via 475(b) alone, kept as a boundary check that
     1 January is correctly included in the 8-day window. *)
  check "475(d): 1 January, the Octave Day of the Nativity" 2026 1 1 true

(* ---- RG 475(e): "in festis nataliciis Apostolorum et Evangelistarum,
   necnon in festis Cathedrae S. Petri et S. Barnabae Ap." ---- *)

let test_475e_apostle_natalicium () =
  (* 2026-11-30: St Andrew, Class2, subject Saint -- not covered by 475(c)
     (not Domini/BVM), so [true] here can only come from 475(e)'s own
     natalicia list. Also carries a real commemoration
     (+ef-advent-1-monday), another live 476(e) instance. *)
  check "475(e): an Apostle's own natalicium (Andrew)" 2026 11 30 true

let test_475e_barnabas_named_explicitly () =
  (* 2026-06-11: St Barnabas, Class3 -- named explicitly by the clause
     ("S. Barnabae Ap."); at Class3 it could not reach [true] via 475(b) or
     (c) regardless. *)
  check "475(e): St Barnabas, named explicitly" 2026 6 11 true

let test_475e_chair_of_peter_named_explicitly () =
  (* 2027-02-22 (NOT 2026: Feb 22 2026 is impeded by Lent I Sunday, so the
     Chair is not observed that year -- checked via `colitur day 2026`
     before picking 2027 instead): the Chair of St Peter, Class2, subject
     Saint -- NOT a natalicium (Peter's own is 29 June, shared with Paul),
     so [true] here can only come from the clause's own explicit "Cathedrae
     S. Petri" naming, not from the natalicium reading in general. *)
  check "475(e): the Chair of St Peter, named explicitly (not a natalicium)" 2027 2 22 true

let test_475e_excludes_conversion_of_paul () =
  (* Trap Two, directly: 2027-01-25, the Conversion of St Paul, Class3 --
     names an Apostle but is NOT his natalicium (his own is 29 June, with
     Peter); Class3 rules out 475(b)/(c), and this slug is deliberately
     absent from [creed_apostle_slugs]. Picked 2027 for the same impeded-
     Sunday reason as the Chair of Peter above (25 January 2026 is itself a
     Sunday). *)
  check "475(e) does NOT cover the Conversion of St Paul (not a natalicium)" 2027 1 25 false

(* ---- RG 23 (feriae) / RG 476(a): Ash Wednesday, Holy Week's own feriae,
   the Chrism/Lord's Supper Mass, the Easter Vigil Mass ---- *)

let test_ash_wednesday_no_creed () =
  check "RG 23(a)/476: Ash Wednesday, a I-class FERIA, not a festum" 2026 2 18 false

let test_holy_thursday_no_creed () =
  (* 2026-04-02: "Feria V in Cena Domini" -- RG 23(b)'s own "omnes feriae
     Hebdomadae sanctae"; also explicitly named by 476(a) ("sive... in
     Cena Domini"). *)
  check "RG 23(b)/476(a): Holy Thursday (Mass of the Lord's Supper)" 2026 4 2 false

let test_holy_saturday_easter_vigil_no_creed () =
  (* 2026-04-04: "Sabbato sanctum" -- RG 23(b) again; also explicitly named
     by 476(a) ("in Missa Vigiliae paschalis"). *)
  check "RG 23(b)/476(a): Holy Saturday (the Easter Vigil Mass)" 2026 4 4 false

(* ---- RG 476(b) negative: a plain II-class saint, not Domini/BVM, not an
   Apostle/Evangelist ---- *)

let test_476b_plain_class2_saint () =
  (* 2026-08-10: St Lawrence, Class2, subject Saint -- a deacon and martyr,
     no Apostle/Evangelist connection, not on any list this module reads. *)
  check "476(b): a plain II-class saint (Lawrence) does not say the Creed" 2026 8 10 false

(* ---- RG 28-34 (vigils): checked ahead of 475(c) so a Class2 vigil that
   is ALSO on marian_slugs is still excluded ---- *)

let test_vigil_excluded_even_when_class2_and_marian () =
  (* 2026-08-14: Vigil of the Assumption, Class2 -- IS on
     {!Rite_ef.Precedence_ef.marian_slugs} (a real Marian entry), so without
     the vigil check ahead of 475(c) this would wrongly come out [true].
     Also carries a real commemoration (+eusebius-confessor), a second live
     476(e) instance. *)
  check "vigils are excluded even when Class2 and Marian (Vigil of the Assumption)" 2026 8 14 false

(* ---- RG 476(d): a IV-class office (also exercises RG 78/309(a)'s votive
   Office of the BVM on Saturday, itself IV class) ---- *)

let test_476d_bvm_saturday_office () =
  (* 2026-07-11: the unoccupied Saturday's Office of Our Lady, Class4 --
     already a pinned example in test/cli.t (Task 4). *)
  check "476(d): the BVM Saturday Office, IV class" 2026 7 11 false

(* One full civil year, walked day by day: RG 475(a)'s own invariant, "every
   Sunday says the Creed, no exceptions" -- the same sanity check the task
   asks for at the domain-measurement step, pinned here as a real assertion
   rather than left to a one-off shell scan. *)
let test_every_sunday_in_2026_says_the_creed () =
  let days = Cal.year ctx layer 2026 in
  Array.iter
    (fun (d : (V.season, V.rank) LD.t) ->
      if d.LD.temporal.Temporal.weekday = Date.Sun then
        Alcotest.(check bool)
          (Printf.sprintf "%s is a Sunday: creed must be true" (Date.to_iso8601 d.LD.date))
          true d.LD.creed)
    days

let suite =
  ( "Rubrics_ef",
    [ Alcotest.test_case "475(a): ordinary Sunday" `Quick test_475a_ordinary_sunday;
      Alcotest.test_case "475(a): reads [temporal], not [observed] (Trap One)" `Quick
        test_475a_reads_temporal_not_observed;
      Alcotest.test_case "475(b): I-class feast" `Quick test_475b_class1_feast;
      Alcotest.test_case "475(c): II-class feast of the Lord" `Quick test_475c_lord;
      Alcotest.test_case "475(c): II-class BVM feast via marian_slugs" `Quick
        test_475c_bvm_via_marian_slugs;
      Alcotest.test_case "475(d): octave overrides an occurring feast" `Quick
        test_475d_octave_even_occurring_feast;
      Alcotest.test_case "475(d): 1 January octave-day boundary" `Quick test_475d_octave_day_boundary;
      Alcotest.test_case "475(e): an Apostle's own natalicium" `Quick test_475e_apostle_natalicium;
      Alcotest.test_case "475(e): Barnabas, named explicitly" `Quick
        test_475e_barnabas_named_explicitly;
      Alcotest.test_case "475(e): Chair of Peter, named explicitly" `Quick
        test_475e_chair_of_peter_named_explicitly;
      Alcotest.test_case "475(e) excludes the Conversion of St Paul (Trap Two)" `Quick
        test_475e_excludes_conversion_of_paul;
      Alcotest.test_case "RG 23/476: Ash Wednesday" `Quick test_ash_wednesday_no_creed;
      Alcotest.test_case "RG 23/476(a): Holy Thursday" `Quick test_holy_thursday_no_creed;
      Alcotest.test_case "RG 23/476(a): Holy Saturday / Easter Vigil" `Quick
        test_holy_saturday_easter_vigil_no_creed;
      Alcotest.test_case "476(b): plain II-class saint (negative)" `Quick
        test_476b_plain_class2_saint;
      Alcotest.test_case "vigils excluded even when Class2 and Marian" `Quick
        test_vigil_excluded_even_when_class2_and_marian;
      Alcotest.test_case "476(d): BVM Saturday Office, IV class" `Quick test_476d_bvm_saturday_office;
      Alcotest.test_case "every Sunday in 2026 says the Creed" `Quick
        test_every_sunday_in_2026_says_the_creed ] )