summaryrefslogtreecommitdiff
path: root/test/test_lectionary_ef.ml
blob: 8af57f61d505e9f7265cd31e3ab449002ea01458 (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
open Colitur_kernel
open Rite_ef

(* Same pattern test_rite_ef.ml already uses: both [Calendar.year] and
   [Calendar.day] take the rite AND the sanctoral layer (Rite.t carries no
   layer of its own -- see rite_ef.mli's own note on why), so step 1's
   sanctoral-proper case needs the REAL data/ef/sanctoral.sexp +
   data/ef/adjustments.sexp loaded, not a bare [Rite_ef.context]. Relative
   to this test's own build directory (_build/default/test/); test/dune
   declares both as deps. *)
let sanctoral_path = "../data/ef/sanctoral.sexp"
let adjustments_path = "../data/ef/adjustments.sexp"
let lectionary_path = "../data/ef/lectionary.sexp"

let real_layer () =
  let layer =
    match Layer.load Vocab_ef.rank_of_sexp sanctoral_path with
    | Ok l -> l
    | Error e -> Alcotest.failf "%s: failed to load: %s" sanctoral_path e
  in
  let overlay =
    match Overlay.load Vocab_ef.rank_of_sexp adjustments_path with
    | Ok o -> o
    | Error e -> Alcotest.failf "%s: failed to load: %s" adjustments_path e
  in
  let layer, diagnostics = Overlay.apply layer overlay in
  Alcotest.(check (list string)) "the committed overlay applies cleanly, no diagnostics" []
    (List.map Overlay.diagnostic_to_string diagnostics);
  layer

(* [Rite_ef.context] takes [~lectionary] (fix round 1, coordinator review):
   the module used to load data/ef/lectionary.sexp itself, as a side effect
   of being linked, which meant a bare `dune build` produced a `colitur`
   that died at startup on EVERY subcommand -- including ones (`easter`)
   that touch no lectionary data at all -- the moment that file was absent
   from the default build target. Caller-supplied now, same as the
   sanctoral layer above. *)
let real_lectionary () =
  match Lectionary.load lectionary_path with
  | Ok l -> l
  | Error e -> Alcotest.failf "%s: failed to load: %s" lectionary_path e

(* [Calendar.day] (not [year]): the liturgical year "opening in civil year y"
   is Advent-anchored (RG 61), so [Calendar.year _ _ 2030] covers Advent 2030
   through November 2031 -- it would never contain 13 January 2030, which
   belongs to the liturgical year that opened in Advent 2029. [Calendar.day]
   finds the containing liturgical year itself; see calendar.mli. *)
let day y m d =
  let date = match Date.make ~year:y ~month:m ~day:d with
    | Ok x -> x | Error e -> Alcotest.fail e in
  Calendar.day (Rite_ef.context ~lectionary:(real_lectionary ())) (real_layer ()) date

let refs (ld : _ Liturgical_day.t) =
  List.map (fun c -> c.Citation.reference) ld.citations

(* Chain step 1: the observed celebration's own proper wins.

   NOT 2030: that is the one pinned collision year (test_golden.ml,
   [test_holy_family_excludes_baptism_2030]) where 13 January is itself the
   Holy Family Sunday and RG 112(a) excludes the Baptism commemoration
   entirely -- [observed] there is [ef-time-after-epiphany-sunday-1], not
   this slug, so it is the wrong year to exercise step 1 against. 2026 is an
   ordinary year (13 January a Tuesday, no Sunday collision), where the
   fixed Commemoration of the Baptism of the Lord (Class2, subject Lord,
   data/ef/sanctoral.sexp) is observed outright. *)
let test_step1_sanctoral_proper () =
  Alcotest.(check (list string))
    "13 January 2026, Commemoration of the Baptism of the Lord"
    [ "Isa 60:1-6"; "John 1:29-34" ]
    (refs (day 2026 1 13))

(* Chain step 2: the day's own temporal slug has a proper (Lent has one daily). *)
let test_step2_lenten_feria_has_its_own () =
  Alcotest.(check (list string))
    "Monday of the 1st week of Lent is not the Sunday's Mass"
    [ "Ezech 34:11-16"; "Matt 25:31-46" ]
    (refs (day 2026 2 23))

(* Fix round 1 (coordinator review, Important finding 2): neither test above
   actually distinguishes step 1 from step 2 -- both survive swapping the
   chain order. 13 January 2026's temporal slug (ef-time-after-epiphany-1-
   tuesday) has no lectionary entry at all, so a swapped chain falls through
   to the same []-then-sanctoral answer; 23 February 2026's OBSERVED
   celebration IS the temporal office (ef-lent-1-monday carries no sanctoral
   entry of its own), so [observed.citations] and the temporal-slug lookup
   are the same lookup wearing two names -- order is a no-op either way.

   19 March 2026 (St Joseph) genuinely needs step 1 to run FIRST: the
   observed celebration (Joseph, Class1, a real sanctoral entry with its own
   citations) and the day's own temporal slug (ef-lent-4-thursday, ALSO a
   real lectionary entry, with different citations) disagree. Verified
   directly against the real data (not transcribed): both value pairs below
   were read off the actual resolved day and the actual
   data/ef/lectionary.sexp entry, not assumed. *)
let test_step1_wins_over_a_competing_step2_entry () =
  Alcotest.(check (list string))
    "19 March 2026: Joseph's own proper wins over Lent 4 Thursday's, which the temporal slug also has"
    [ "Ecclus 45:1-6"; "Matt 1:18-21" ]
    (refs (day 2026 3 19))

(* Fix round 1 (coordinator review): the mirror-image pin for step 2 -- a day
   whose OBSERVED celebration carries no citations of its own (Holy Family,
   synthesised by [Temporal_ef] itself, not sourced from
   data/ef/sanctoral.sexp, so [Celebration.citations] is empty) falls
   through to the temporal slug, and the temporal slug's own lectionary
   entry is genuinely Holy Family's Mass, not the Baptism's -- RG 112(a)
   (see test_golden.ml's own [test_holy_family_excludes_baptism_2030])
   excludes the Baptism from this day entirely, so there is no sanctoral
   citation anywhere to fall back to even in principle. Verified directly
   against the real data. *)
let test_step2_holy_family_reached_through_temporal_slug () =
  Alcotest.(check (list string))
    "13 January 2030: Holy Family reached via the temporal slug, the Baptism entirely absent"
    [ "Col 3:12-17"; "Luke 2:42-52" ]
    (refs (day 2030 1 13))

(* Chain step 3: a feria with no proper of its own says the preceding
   Sunday's Mass. *)
let test_step3_advent_feria_resumes_sunday () =
  Alcotest.(check (list string))
    "Monday after Advent I says Advent I's Mass"
    [ "Rom 13:11-14"; "Luke 21:25-33" ]
    (refs (day 2025 12 1))

let test_step3_christmas_feria_resumes_sunday () =
  Alcotest.(check (list string))
    "Monday after the Sunday within the octave of Christmas"
    [ "Gal 4:1-7"; "Luke 2:33-40" ]
    (refs (day 2025 12 29))

(* Fix round 1 (coordinator review, Important finding 1): the original test
   here (2028-12-26) did not exercise step 3 at all -- 26 December is always
   Stephen (Class2, a real sanctoral entry with its own citations, fixed
   regardless of weekday), so that date resolves entirely at step 1 and never
   reaches step 2 or 3. Its justifying comment was also factually wrong: 24
   December's TEMPORAL slug is [ef-nativity-vigil], identical to its OBSERVED
   slug -- [Temporal_ef.named] hard-codes the Vigil for 24 December ahead of
   any Sunday computation, so there is no temporal/observed split on that
   date to exploit in the first place.

   2025-02-03 genuinely discriminates, verified directly against the real
   data (data/ef/sanctoral.sexp, data/ef/lectionary.sexp) and the real
   resolver, not transcribed: 2025-02-02 (a Sunday) is observed as the
   Purification of the BVM (RG 16(a): a Feast of the Lord takes the Sunday's
   place outright), with its own citations Mal 3:1-4 / Luke 2:22-32 -- but
   its TEMPORAL identity is [ef-time-after-epiphany-sunday-4], whose
   lectionary entry is the different Rom 13:8-10 / Matt 8:23-27. 2025-02-03
   (Monday) has no sanctoral office of its own and no entry for its own
   temporal slug ([ef-time-after-epiphany-4-monday], absent from
   data/ef/lectionary.sexp), so it reaches step 3 and must return the
   Sunday's TEMPORAL readings (Rom 13:8-10 / Matt 8:23-27). A "step 3 reads
   the observed office instead of the temporal one" bug would instead return
   the Purification's (Mal 3:1-4 / Luke 2:22-32) -- different, checkable
   values, not a bare length check. *)
let test_step3_uses_temporal_not_observed () =
  Alcotest.(check (list string))
    "3 February 2025 takes the preceding Sunday's TEMPORAL Mass (Time after Epiphany IV), not the Purification's"
    [ "Rom 13:8-10"; "Matt 8:23-27" ]
    (refs (day 2025 2 3))

(* Termination: a Sunday that reaches step 3 would consult itself. It must
   not: the guard is weekday <> Sun. *)
let test_step3_sunday_does_not_recurse () =
  let d = day 2026 6 14 in
  Alcotest.(check int) "a Sunday resolves without looping" 2 (List.length (refs d))

let suite =
  [ ("step 1: sanctoral proper", `Quick, test_step1_sanctoral_proper);
    ("step 2: own temporal proper", `Quick, test_step2_lenten_feria_has_its_own);
    ("step 1 wins over a competing step 2 entry", `Quick,
      test_step1_wins_over_a_competing_step2_entry);
    ("step 2: Holy Family reached through the temporal slug, Baptism absent", `Quick,
      test_step2_holy_family_reached_through_temporal_slug);
    ("step 3: Advent feria resumes the preceding Sunday", `Quick,
      test_step3_advent_feria_resumes_sunday);
    ("step 3: Christmas feria resumes the preceding Sunday", `Quick,
      test_step3_christmas_feria_resumes_sunday);
    ("step 3: 3 Feb 2025 takes the Sunday's temporal Mass, not the Purification's", `Quick,
      test_step3_uses_temporal_not_observed);
    ("step 3: a Sunday does not recurse into itself", `Quick,
      test_step3_sunday_does_not_recurse) ]