aboutsummaryrefslogtreecommitdiff
path: root/test/test_rite_of.ml
blob: 1de36141abcc90c31ea30cc23ff8eb71385e3508 (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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
(* Task 5 (2026-08-25-colitur-of-phases-3-5): [Rite_of.context], wired
   together with the REAL data/of/calendar-2002.sexp + all 13 decree
   overlays + data/of/lectionary.sexp, resolved through
   {!Colitur_kernel.Calendar} and checked by {!Colitur_kernel.Validate.run}
   -- the same pipeline `colitur day --rite of` uses, and the same
   discipline test_validate.ml/test_rite_ef.ml already apply to the EF
   side: exactly one observed office per day, the year covered once with no
   gaps, slug-uniqueness within the liturgical year, and transfers reaching
   a fixed point are all invariants {!Colitur_kernel.Validate.run} already
   checks generically -- this file supplies the REAL OF rite and data and
   asserts the result is clean, rather than re-deriving each invariant by
   hand. *)

module Val = Colitur_kernel.Validate
module Layer = Colitur_kernel.Layer
module Overlay = Colitur_kernel.Overlay
module Cal = Colitur_kernel.Calendar
module LD = Colitur_kernel.Liturgical_day
module Slug = Colitur_kernel.Slug
module Date = Colitur_kernel.Date
module Cel = Colitur_kernel.Celebration
module P = Colitur_kernel.Precedence
module Computus = Colitur_kernel.Computus
module V = Rite_of.Vocab_of

(* Relative to this test's own build directory (_build/default/test/), same
   convention test_amendments_of.ml/test_calendar_of_data.ml already use --
   test/dune declares each of these as a dep of the (test ...) stanza. *)
let base_path = "../data/of/calendar-2002.sexp"
let amendments_dir = "../data/of/amendments/"
let lectionary_path = "../data/of/lectionary.sexp"

(* Decree-chronological order -- the exact list and order
   test_amendments_of.ml's own [files] table already pins by SHA-256; not
   re-pinning the hashes here (that is that file's own job), just applying
   them in the same order so the rite under test here is the one bin/main.ml
   will assemble. *)
let amendment_files =
  [ "001-padre-pio.sexp"; "002-juan-diego-cuauhtlatoatzin.sexp"; "003-our-lady-of-guadalupe.sexp";
    "004-john-xxiii-john-paul-ii.sexp"; "005-mary-magdalene-rank.sexp"; "006-mary-mother-of-the-church.sexp";
    "007-paul-vi.sexp"; "008-our-lady-of-loreto.sexp"; "009-faustina-kowalska.sexp";
    "010-narek-avila-hildegard.sexp"; "011-martha-mary-lazarus.sexp"; "012-teresa-of-calcutta.sexp";
    "013-john-henry-newman.sexp" ]

let real_of_layer =
  let base =
    match Layer.load V.rank_of_sexp base_path with
    | Ok l -> l
    | Error e -> failwith (Printf.sprintf "%s: failed to load: %s" base_path e)
  in
  let overlays =
    List.map
      (fun name ->
        let path = amendments_dir ^ name in
        match Overlay.load V.rank_of_sexp path with
        | Ok o -> o
        | Error e -> failwith (Printf.sprintf "%s: failed to load: %s" path e))
      amendment_files
  in
  let layer, diagnostics = Overlay.merge base overlays in
  if diagnostics <> [] then
    failwith
      (Printf.sprintf "unexpected amendment diagnostics: %s"
         (String.concat "; " (List.map Overlay.diagnostic_to_string diagnostics)));
  layer

(* [Rite_of.context] takes [~lectionary] as a caller-supplied parameter, not
   a value closed over an internal load -- see rite_of.ml's own citation for
   why (the exact `colitur easter <year>` startup-death lesson
   {!Rite_ef.Rite_ef.context}'s own comment records). *)
let real_of_lectionary =
  match Colitur_kernel.Lectionary.load lectionary_path with
  | Ok l -> l
  | Error e -> failwith (Printf.sprintf "%s: failed to load: %s" lectionary_path e)

let real_of_rite = Rite_of.context ~lectionary:real_of_lectionary

let run year = Val.run real_of_rite real_of_layer ~year

(* CLOSED, fix wave I8 (final-review.md, 2026-08-25-colitur-of-phases-3-5).
   This paragraph used to describe a LIVE gap: data/of/lectionary.sexp's
   own provenance header named ONE informational temporal-slug gap,
   [of-nativity] (25 December -- Christmas Day itself), because
   niedziela.pl's own source never covered the Christmas DAY Mass at all
   (only the Vigil), which made {!Colitur_kernel.Validate.run}'s
   ["citations-unresolved"]/["formulary"] checks fire every year, on 25
   December, on real shipped data. tools/bootstrap_lectionary_of.ml's own
   [hand_authored] table now injects that entry directly (Isaiah 52:7-10 /
   John 1:1-18, OLM 1981's own "16 Ad Missam in die",
   docs/research/of/olm-1981-ocr.txt:4513-4520) -- a primary-source
   citation, not a guess, the same discipline every other hand-authored
   entry in this project follows. [is_known_nativity_gap] is kept, not
   deleted, but is no longer in [is_known]'s own filter below: it now
   exists ONLY as an explicit, named "this must never fire again" check
   ({!check_year_allowing_known_gaps}'s own dedicated assertion) -- a
   regression here surfaces as an ordinary unexpected-failure test error,
   not a silently-absorbed known gap, which is the whole point of moving a
   predicate out of the filter once its own defect closes. *)
let ends_with ~suffix s =
  let ls = String.length s and lx = String.length suffix in
  ls >= lx && String.sub s (ls - lx) lx = suffix

(* No [Str]/regex (deps frozen) -- a plain substring scan. *)
let contains ~substring s =
  let ls = String.length s and lx = String.length substring in
  let rec go i = i + lx <= ls && (String.sub s i lx = substring || go (i + 1)) in
  lx = 0 || go 0

let is_known_nativity_gap (f : Val.failure) =
  (f.Val.check = "citations-unresolved" || f.Val.check = "formulary") && ends_with ~suffix:"12-25" f.Val.date

(* CLOSED (W2, CLAUDE.md; fixed in temporal_of.ml). This paragraph used to
   describe a SECOND, NEWLY-FOUND gap this task's own wiring surfaced --
   [rite_of] was never previously wired into a real
   {!Colitur_kernel.Rite.t}, so [Validate.run]'s ["anchor"] check had never
   before run [Temporal_of.anchors] against [Temporal_of.temporal] for
   real. Normae n. 35(a)'s own text is "Dominica infra octavam Nativitatis,
   VEL, EA DEFICIENTE, die 30 decembris" -- the Holy Family fallback is a
   FIXED-DATE fallback (30 December, whatever weekday it falls on that
   year), confirmed by {!Temporal_of.holy_family}'s own [None -> mk y 12 30]
   branch and its own .mli citation ("this 6-day window CAN be [empty of a
   Sunday] -- confirmed by the fallback clause's own existence"). But
   {!Temporal_of.temporal} used to only ever TEST [same d (holy_family y)]
   inside its [sunday_slug d] dispatch arm -- i.e. only when [d] genuinely
   WAS a Sunday -- so in a year whose 26-31 December window had NO Sunday
   (equivalently: 25 December, Christmas Day, is itself a Sunday), the
   fallback date was computed correctly by [holy_family]/[anchors] but
   [temporal] itself silently fell through to an ordinary ferial slug
   instead (["of-christmas-0-friday"] et al.) -- exactly the "generic day
   slug masking a real named feast" shape CLAUDE.md already records EF's
   own Holy Family/Holy Name gaps as, before they were fixed there. Not
   rare: this fired whenever Christmas Day is a Sunday, roughly one year in
   seven. [Temporal_of.temporal] now consults [Temporal_of.holy_family]
   directly in that dispatch arm, independent of weekday, carrying the
   identical identity (subject Lord, [holy_family_names], rank Festum) the
   Sunday case already builds -- see temporal_of.ml's own citation on that
   branch. [is_known_holy_family_fallback_gap] is kept, not deleted, but is
   no longer in [is_known]'s own filter below -- the same discipline
   [is_known_nativity_gap] above already established once ITS defect
   closed: it exists ONLY as an explicit, named "this must never fire
   again" check ({!check_year_allowing_known_gaps}'s own dedicated
   assertion), not silently absorbed. *)
let is_known_holy_family_fallback_gap (f : Val.failure) =
  f.Val.check = "anchor" && contains ~substring:"of-holy-family" f.Val.detail

let check_year_allowing_known_gaps year =
  let fs = run year in
  (* Fix wave I8: [is_known_nativity_gap] is deliberately NOT part of
     [is_known] any more -- see that predicate's own header. W2 fix: neither
     is [is_known_holy_family_fallback_gap] now -- see ITS own header. No
     known gaps remain filtered here (landmark years validate cleanly, full
     stop); [is_known] is kept as a named hook rather than inlined to
     [false] so a future gap has an obvious place to land. *)
  let is_known (_ : Val.failure) = false in
  let unexpected = List.filter (fun f -> not (is_known f)) fs in
  (match unexpected with
   | [] -> ()
   | fs ->
       Alcotest.failf "%d: %s" year
         (String.concat "; " (List.map Val.failure_to_string (List.filteri (fun i _ -> i < 5) fs))));
  Alcotest.(check int)
    (Printf.sprintf "%d: no of-nativity citations-unresolved/formulary gap (fix wave I8 -- was 2)" year)
    0 (List.length (List.filter is_known_nativity_gap fs));
  Alcotest.(check int)
    (Printf.sprintf "%d: no Holy Family anchor gap (W2 fix -- was 1)" year)
    0 (List.length (List.filter is_known_holy_family_fallback_gap fs))

(* Landmark years, the same choice test_validate.ml's own
   [test_landmark_years] makes for EF: both domain edges plus two ordinary
   years well inside it. *)
let test_landmark_years () = List.iter check_year_allowing_known_gaps [ 1583; 2026; 2035; 9998 ]

(* 9999 mirrors test_validate.ml's own [test_year_9999_does_not_raise]: the
   liturgical year opening in civil year 9999 continues into out-of-domain
   civil year 10000, so [Val.run]/[Calendar.year] clamp the walk to 31
   December 9999 rather than raising, and the truncated season run is
   *expected* to fail the "seasons" check -- pinning that this surfaces as
   an ordinary failure, not an uncaught exception, and that resolution
   itself ("coverage"/"lost"/"duplicated") stays clean through the clamp. *)
let test_year_9999_does_not_raise () =
  let fs = run 9999 in
  Alcotest.(check bool) "no coverage failures (temporal stayed total through the clamp)" true
    (not (List.exists (fun f -> f.Val.check = "coverage") fs));
  Alcotest.(check bool) "seasons check flags the truncated final year as incomplete" true
    (List.exists (fun f -> f.Val.check = "seasons") fs)

(* A real transfer-bearing sample, the OF counterpart of test_rite_ef.ml's
   own 2005-2050 sweep: every impeded Sollemnitas [Precedence_of.disposition]
   defers is either placed by [transfer_target] and reaches a fixed point
   (no "unconverged" failure -- Validate's own check), or is recorded
   [omitted] with a reason, never silently dropped ("lost"/"duplicated"),
   over the project's own 2005-2050 differential-testing window (CLAUDE.md).

   STRUCTURAL checks only -- ["coverage"; "weekday"; "rank"; "colour";
   "determinism"; "slugs"; "seasons"; "week"; "lost"; "duplicated";
   "unconverged"; "admission"; "observed"] -- deliberately NOT the
   citation-chain ones ("anchor"/"citations"/"citations-unresolved"/
   "formulary"), and this exclusion is itself a finding, not a shortcut:
   sweeping the full 46-year window (not just the 4 landmark years above)
   surfaced that [data/of/lectionary.sexp]'s own coverage was measured
   against exactly ONE civil year (2026, per that file's own provenance
   header) -- OTHER years exercise the OTHER weekday-cycle letter for the
   same temporal slug (OLM n.69.4, {!Lectionary_of.weekday_cycle}: which
   letter applies flips on the LITURGICAL year label's parity, independent
   of which civil year is being swept), and at least one such pairing is
   asymmetric on real shipped data: [of-christmas-0-friday-ii] exists,
   [of-christmas-0-friday-i] does not (confirmed directly against
   data/of/lectionary.sexp; first reached in this sweep at 2006-12-29).
   Separately, 2006-12-24 (a year Christmas Eve itself is a Sunday) also
   exposed a THIRD newly-found gap: {!Temporal_of.anchors} lists
   [of-nativity-vigil] on 24 December UNCONDITIONALLY, but
   {!Temporal_of.named}'s own coded exception (Tabula I.2/Normae n.5:
   Advent IV outranks the Vigil when they coincide) means
   {!Temporal_of.temporal} does not always agree. All three are OUT OF
   THIS TASK'S SCOPE to fix -- lectionary_of.* and data/of/lectionary.sexp
   are excluded from this task's brief, and temporal_of.ml's own rubric
   logic is Task 1's deliverable -- reported as concerns in this task's own
   report rather than fixed here or silently excluded without explanation.

   CORRECTED (fix-lectionary-anchors task, 2026-08-26): the of-nativity-
   vigil/[anchors] disagreement described in the paragraph above is now
   FIXED -- {!Temporal_of.anchors} carries the same Sunday guard [named]
   already had. A dedicated exhaustive sweep
   (test_temporal_of.ml's own [test_exhaustive_domain_sweep], every civil
   year 1584-9997 under COLITUR_EXHAUSTIVE_SWEEP=1) confirmed this was
   also the ONLY unexplained [anchors]/[temporal] drift anywhere in the
   domain -- the other two items in this paragraph (the lectionary
   weekday-cycle-letter asymmetry, and the Holy Family 30-December
   fallback {!is_known_holy_family_fallback_gap} below already pins) were
   unrelated and remained open, unchanged by this fix.

   CORRECTED AGAIN (W2, CLAUDE.md, same day): the Holy Family 30-December
   fallback named just above is ALSO now fixed -- {!Temporal_of.temporal}
   consults {!Temporal_of.holy_family} directly, independent of weekday.
   Only the lectionary weekday-cycle-letter asymmetry remains open; it is
   untouched by this fix (lectionary_of.*/data/of/lectionary.sexp are out
   of scope here, per the original paragraph's own citation). *)
let sample_years =
  let rec range a b = if a > b then [] else a :: range (a + 1) b in
  range 2005 2050

let structural_check = function
  | "anchor" | "citations" | "citations-unresolved" | "formulary" -> false
  | _ -> true

let check_year_structural_only year =
  match List.filter (fun f -> structural_check f.Val.check) (run year) with
  | [] -> ()
  | fs ->
      Alcotest.failf "%d: %s" year
        (String.concat "; " (List.map Val.failure_to_string (List.filteri (fun i _ -> i < 5) fs)))

let test_transfers_reach_a_fixed_point_2005_2050 () =
  List.iter check_year_structural_only sample_years

let slug_of (c : V.rank Cel.t) = Slug.to_string c.Cel.slug

(* RESOLVED ([of-normae-56f]/W1, 2026-08-26; previously the
   precedence_of.mli-documented KNOWN-UNIMPLEMENTED fourth transfer_target
   rule, final review Important #5, ledger ruling R9). Normae n. 56(f) --
   St Joseph's solemnity, falling on Palm Sunday, is ANTICIPATED BACKWARD
   to 18 March, not carried forward. {!Colitur_kernel.Rite.t.transfer_target}'s
   own obligation was relaxed from "strictly later than origin" to
   "different from origin" (rite.mli's own comment carries the argument,
   traced against {!Colitur_kernel.Calendar}'s actual placement pass, not
   merely assumed safe), and [Precedence_of.transfer_target] gained a
   Rule 0 for exactly this collision -- see that value's own .ml/.mli for
   the full citation.

   Previously pinned here as KNOWN WRONG (colitur sent Joseph forward to
   Easter+9 via the general rule 3 instead); now pinned as CORRECT. All
   years with Gregorian Easter = 26 March put Palm Sunday on 19 March,
   coinciding with Joseph; 2062 is the nearest one after today (2026)
   still inside the differential-adjacent range this project favours for
   worked examples.

   2062 is a doubly-worked example, not chosen only for Joseph: that same
   year, 25 March (the Annunciation) falls on HOLY SATURDAY (Easter - 1),
   so the Annunciation is ALSO impeded and targets Easter + 8 via the
   general rule -- independently of Joseph now, since Rule 0 is a fixed
   one-day displacement that never enters the general search or competes
   with the Annunciation's own target at all (Joseph departs 19 March for
   18 March; the Annunciation departs 25 March for Easter+8 -- the two no
   longer collide the way they did when both were pushed toward the same
   forward date). *)
let test_joseph_palm_sunday_2062_anticipated_backward () =
  let easter_2062 = Computus.gregorian_easter 2062 in
  Alcotest.(check string) "2062 Easter is really 26 March (the Normae n.56(f) trigger)" "2062-03-26"
    (Date.to_iso8601 easter_2062);
  let palm_sunday = Date.add_days easter_2062 (-7) in
  Alcotest.(check string) "Palm Sunday 2062 is really 19 March, coinciding with St Joseph" "2062-03-19"
    (Date.to_iso8601 palm_sunday);
  let holy_saturday = Date.add_days easter_2062 (-1) in
  Alcotest.(check string) "25 March 2062 is really Holy Saturday, coinciding with the Annunciation too"
    "2062-03-25" (Date.to_iso8601 holy_saturday);
  let days = Cal.year real_of_rite real_of_layer 2061 in
  let day_of d = Array.to_list days |> List.find (fun day -> Date.compare day.LD.date d = 0) in
  let joseph_day = day_of palm_sunday in
  Alcotest.(check string) "Palm Sunday itself is observed on 19 March (Joseph impeded, correctly)"
    "of-palm-sunday" (slug_of joseph_day.LD.observed);
  let joseph_slug = "joseph-husband-of-the-blessed-virgin-mary" in
  let departure =
    List.find_opt (fun (c, _) -> slug_of c = joseph_slug) joseph_day.LD.transferred_out
  in
  let eighteen_march = Date.add_days palm_sunday (-1) in
  (match departure with
  | None -> Alcotest.fail "expected Joseph to be transferred_out from 19 March 2062 (impeded by Palm Sunday)"
  | Some (_, target) ->
      Alcotest.(check string)
        "Normae n.56(f): Joseph is anticipated BACKWARD to 18 March 2062, not carried forward"
        "2062-03-18" (Date.to_iso8601 target);
      Alcotest.(check bool) "which really is the day before Palm Sunday" true
        (Date.compare target eighteen_march = 0));
  let march18_day = day_of eighteen_march in
  Alcotest.(check string) "and 18 March 2062 itself observes St Joseph (not Cyril of Jerusalem's \
                           ad-libitum memorial, not the ordinary Lenten Saturday feria)"
    joseph_slug (slug_of march18_day.LD.observed);
  let joseph_arrived =
    match march18_day.LD.transferred_in with
    | Some c -> slug_of c = joseph_slug
    | None -> false
  in
  Alcotest.(check bool) "and 18 March 2062 records the arrival (transferred_in), not a silent placement"
    true joseph_arrived;
  (* {!Colitur_kernel.Rite.t.transfer_target}'s own second obligation
     (terminate) reduces, for this liturgical year, to the whole-year
     placement pass reaching a fixed point at all -- {!Val.run}'s own
     "unconverged" check is exactly {!Colitur_kernel.Calendar}'s round
     guard surfaced as a named failure (calendar.ml's own
     [unconverged_reason]). *)
  let failures = run 2061 in
  Alcotest.(check bool) "the whole 2061 liturgical year still reaches a fixed point (no unconverged \
                         transfer placement anywhere in it)" false
    (List.exists (fun f -> f.Val.check = "unconverged") failures)

(* CLOSED, W2 fix (CLAUDE.md). This test used to pin a defect the same way
   [is_known_holy_family_fallback_gap]'s own citation just above describes:
   1583 is the domain floor AND a year Christmas Day falls on a Sunday, so
   26-31 December has no Sunday of its own and [Temporal_of.holy_family]
   falls back to its own fixed 30 December -- correctly, per
   [Temporal_of.anchors] -- but [Temporal_of.temporal] used to never reach
   that fallback (its Holy-Family test lived inside the Sunday-only dispatch
   arm), so 30 December 1583 was observed as an ordinary
   Friday-of-Christmastide ferial office instead of the Feast of the Holy
   Family. Now that {!Temporal_of.temporal} consults {!Temporal_of
   .holy_family} directly, this test asserts the FIXED, correct behaviour
   instead -- through the full real-data {!Colitur_kernel.Calendar}/
   {!Colitur_kernel.Precedence} pipeline, not merely {!Temporal_of.temporal}
   in isolation (test_temporal_of.ml's own
   [test_holy_family_fallback_temporal] already covers that layer) --
   checking identity (subject, rank), not only the slug, so a fix that
   landed the right slug on the wrong grade would still be caught. *)
let test_holy_family_fallback_1583_now_correct () =
  let christmas_1583 = Date.of_iso8601 "1583-12-25" |> Result.get_ok in
  Alcotest.(check bool) "1583 Christmas Day really is a Sunday (the Normae n.35(a) trigger)" true
    (Date.weekday christmas_1583 = Date.Sun);
  let days = Cal.year real_of_rite real_of_layer 1583 in
  let dec30 = Array.to_list days |> List.find (fun d -> Date.compare d.LD.date (Date.add_days christmas_1583 5) = 0) in
  Alcotest.(check string) "FIXED (W2): 30 December 1583 is observed as of-holy-family, not an ordinary ferial"
    "of-holy-family" (slug_of dec30.LD.observed);
  Alcotest.(check string) "subject is Lord (same identity the Sunday case carries)" "lord"
    (Colitur_kernel.Subject.to_string dec30.LD.observed.Cel.subject);
  Alcotest.(check string) "rank is Festum, not a lesser grade" "festum"
    (V.rank_to_string dec30.LD.observed.Cel.rank)

(* Fix wave I6 (final-review.md, 2026-08-25-colitur-of-phases-3-5): the
   obligatory-memorial tie data/of/amendments/006-mary-mother-of-the-church
   .sexp's own header documents at length (its "*** FINDING ***" comment)
   was documented but never PINNED -- the ledger's own remedy (F-MEMORIAL-TIE)
   asked for both halves. Mary, Mother of the Church (Easter+50) and Anthony
   of Padua (13 June, fixed) both band at Tabula III.10 (100, both universal
   Memoria_obligatoria) in any year Easter+50 lands on 13 June; 2011 is the
   worked example that amendment header itself names (Easter 24 April 2011,
   so Easter+50 = 13 June -- re-derived here, not trusted from the comment).

   RESOLVED (W3, of-known-wrongs, 2026-08-26) ON A RUBRIC, NOT COLITUR'S OWN
   JUDGEMENT -- this test used to pin the OPPOSITE outcome ("KNOWN
   ARBITRARY", the kernel's generic alphabetical Slug.compare tie-break,
   for lack of anything rite-specific). That framing is now WRONG, not
   merely superseded: colitur's own two previously-checked primary sources
   really are silent (the 2018 decree, AAS 110, 437-438, on universal-
   calendar coincidence; Normae n. 14's third clause, which resolves only
   the AD-LIBITUM/AD-LIBITUM case, "Si eodem die plures inscribuntur in
   calendario memoriae ad libitum") -- but a THIRD primary source, found by
   searching the corpus rather than invented, does resolve it: the CDWDS's
   own "Notification on the Memorial of the Blessed Virgin Mary, Mother of
   the Church" (24 March 2018, same dicastery and signatories as the decree;
   published only on vatican.va, confirmed absent from the AAS 2018
   combined index) states plainly that when this memorial "coincides with
   another Memorial of a Saint or Blessed... the Memorial of the Blessed
   Virgin Mary is to prevail." See {!Rite_of.Precedence_of.movable_bvm_
   memorials}'s own comment for the full Latin, the official English, and
   why the same rule also covers the Immaculate Heart of Mary (the
   Notification names both, by the identical link-to-a-Lord's-solemnity
   argument). {!Rite_of.Precedence_of.band} now gives both a sub-rank of
   95, strictly between Tabula entries 9 and 10, so this is no longer a
   coin-flip the kernel settles for lack of a rite rule -- it is the rite
   rule, cited. This test still pins CURRENT behaviour, for the identical
   invisible-drift reason the original comment gave; what changed is which
   behaviour is correct to pin. *)
let test_mater_ecclesiae_anthony_tie_2011_resolved () =
  let easter_2011 = Computus.gregorian_easter 2011 in
  Alcotest.(check string) "2011 Easter is really 24 April (the Easter+50 trigger)" "2011-04-24"
    (Date.to_iso8601 easter_2011);
  let tie_date = Date.add_days easter_2011 50 in
  Alcotest.(check string) "Easter+50 2011 is really 13 June, coinciding with Anthony of Padua's fixed date"
    "2011-06-13" (Date.to_iso8601 tie_date);
  (* [Cal.year]'s own [y] labels an Advent-to-Advent LITURGICAL year, not a
     civil one (year_bounds = [year_start y, year_start (y+1) - 1], and
     [year_start] is Advent Sunday WITHIN civil year y) -- so 13 June 2011
     falls inside liturgical year 2010 (Advent 2010 .. the eve of Advent
     2011), the same off-by-one every other test in this file that reaches
     for a spring date already accounts for (the Joseph test above uses
     2061 to reach March 2062). *)
  let days = Cal.year real_of_rite real_of_layer 2010 in
  let june13 = Array.to_list days |> List.find (fun d -> Date.compare d.LD.date tie_date = 0) in
  Alcotest.(check string)
    "RESOLVED (W3): the CDWDS's own 24 March 2018 Notification (see this test's own header) makes \
     Mary, Mother of the Church prevail, not Anthony of Padua"
    "mary-mother-of-the-church" (slug_of june13.LD.observed);
  let anthony_omitted =
    List.exists (fun (c, _) -> slug_of c = "anthony-of-padua-priest-and-doctor") june13.LD.omitted
  in
  Alcotest.(check bool)
    "and Anthony of Padua is recorded in `omitted`, not silently dropped -- {!Colitur_kernel \
     .Liturgical_day.t.omitted}'s own contract"
    true anthony_omitted;
  Alcotest.(check bool) "he carries no commemoration either (OF's own admit always returns [])" false
    (List.exists (fun (c, _) -> slug_of c = "anthony-of-padua-priest-and-doctor") june13.LD.commemorations)

let suite =
  ( "Rite_of (real data: assembled Rite.t end-to-end via Calendar/Validate)",
    [ Alcotest.test_case "landmark years validate cleanly (W2 and the nativity gap both closed)" `Quick
        test_landmark_years;
      Alcotest.test_case "year 9999 does not raise; seasons flags the clamp, coverage stays clean" `Quick
        test_year_9999_does_not_raise;
      Alcotest.test_case "2005-2050: transfers reach a fixed point, nothing lost or duplicated" `Slow
        test_transfers_reach_a_fixed_point_2005_2050;
      Alcotest.test_case
        "RESOLVED (W1): St Joseph on Palm Sunday 2062 is anticipated backward to 18 March per Normae \
         n.56(f), converges, and is recorded as an arrival" `Quick
        test_joseph_palm_sunday_2062_anticipated_backward;
      Alcotest.test_case
        "FIXED (W2): Holy Family's Normae n.35(a) 30-December fallback is now reached when Christmas \
         Day is a Sunday (1583)" `Quick
        test_holy_family_fallback_1583_now_correct;
      Alcotest.test_case
        "RESOLVED (W3): Mary Mother of the Church beats Anthony of Padua's tie, 2011-06-13 \
         (Easter+50), per the CDWDS's 24 March 2018 Notification" `Quick
        test_mater_ecclesiae_anthony_tie_2011_resolved ] )