summaryrefslogtreecommitdiff
path: root/test/test_rubrics_ef.ml
blob: bb3bfed5809f262a8313fbfd5a8e802b400e8e8b (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
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
(* 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.

   One further test (celebrant-rubrics-phase1 review, finding 1) is
   different in kind from the rest: not an end-to-end [creed] check
   against a single real date, but a direct sweep of {!Rite_ef.Temporal_ef}
   output across a century, asserting the unstated invariant [creed]'s own
   RG 24/25 ferial branches rely on rather than leaving it asserted only
   in prose -- see its own header comment for the full account. *)

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 Computus = Colitur_kernel.Computus
module V = Rite_ef.Vocab_ef
module RE = Rite_ef.Rubrics_ef
module TE = Rite_ef.Temporal_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)

(* ------------------------------------------------------------------------ *)
(* Breviary 237-238 (Te Deum) and RG 431-432 (Gloria) -- Phase 2 of the      *)
(* celebrant-rubrics-phase1 design. See lib/rites/rite_ef/rubrics_ef.ml for  *)
(* both rubrics quoted in full and every branch's own citation. One         *)
(* end-to-end test per clause, resolved against REAL calendar dates through *)
(* the shipped data -- every expected value below was read off              *)
(* `colitur rubrics <year>`'s real output BEFORE being pinned here, the     *)
(* same discipline the Creed tests above already establish. *)

let day_on y m d = Cal.day ctx layer (mk y m d)
let gloria_on y m d = (day_on y m d).LD.gloria
let te_deum_on y m d = RE.te_deum ~temporal:(day_on y m d).LD.temporal ~observed:(day_on y m d).LD.observed
    ~date:(mk y m d)

let check_gloria name y m d expected = Alcotest.(check bool) name expected (gloria_on y m d)
let check_te_deum name y m d expected = Alcotest.(check bool) name expected (te_deum_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(f): "in Missis defunctorum" -- the Creed is never said at a
   Requiem Mass. Whole-branch review finding 1: colitur used to say [true]
   here on rank alone (475(b), Class1) -- the LMS Ordo layer's own former
   allow-list entry L1, now closed, was this exact day. See rubrics_ef.ml's
   own header for why {!Colour.Black} is a sound RG 117 proxy for "this Mass
   is a Requiem" on the shipped data. ---- *)

let test_476f_all_souls_no_creed () =
  (* 2025-11-03: All Souls' Day (2 November, a Sunday that year, transferred
     to the next open day, RG's own Class1/black office unchanged by the
     transfer) -- `colitur day 2025` confirms class-1/black/
     commemoration-of-all-souls on this date. Before this fix [creed] here
     was [true] via 475(b); the Ordo's own text ("No Cr") says otherwise. *)
  check "RG 476(f): All Souls' Day (transferred), a Requiem, says no Creed" 2025 11 3 false

let test_476f_good_friday_no_creed () =
  (* 2026-04-03: Good Friday, [colour = Black] (temporal_ef.ml's own
     [days_between easter d = -2] branch) -- already [false] via the RG
     23(b) feria-I-classis exclusion checked earlier in [creed], so this is
     a belt-and-braces witness that the 476(f) guard, were it ever reached
     first, would agree rather than a case that depends on it: Good Friday
     has no Mass in the 1955-restored Holy Week to begin with. *)
  check "RG 476(f)/RG 23(b): Good Friday, black, says no Creed" 2026 4 3 false

(* Whole-branch review finding 1's own instruction: a test that FAILS if a
   third {!Colour.Black} celebration is ever introduced, so the RG 476(f)
   guard's colour proxy cannot silently rot into covering the wrong
   population. Two independent checks, matching the two places a black
   celebration could be added:

   DATA: every entry the shipped sanctoral layer (sanctoral.sexp, with
   adjustments.sexp merged on top -- the same [layer] every other test in
   this file resolves against) carries directly, fixed AND movable alike
   ({!Colitur_kernel.Layer.entries} holds the raw [Date_spec.t], not a
   resolved index, so this needs no date walk at all to cover every civil
   year).

   CODE: {!Rite_ef.Temporal_ef.temporal}'s own colour logic, which
   sanctoral.sexp cannot see (Good Friday's black is computed, not data) --
   swept across a century the same span and for the same coverage reason
   {!test_ferial_origin_never_carries_lord_bvm_or_apostle_slug} above
   already uses, since a Colour.Black bug introduced into a rare branch
   (an Ember day, Rogation Wednesday...) would not show up in one year. *)
let test_colour_black_population_is_exactly_two () =
  let black_data_entries =
    List.filter
      (fun (e : V.rank Colitur_kernel.Layer.entry) -> e.Colitur_kernel.Layer.cel.Cel.colour = Colour.Black)
      layer.Colitur_kernel.Layer.entries
  in
  Alcotest.(check int) "exactly one Colour.Black entry in the shipped data" 1
    (List.length black_data_entries);
  (match black_data_entries with
  | [ e ] ->
      Alcotest.(check string) "the one Colour.Black data entry is All Souls" "commemoration-of-all-souls"
        (Slug.to_string e.Colitur_kernel.Layer.cel.Cel.slug)
  | _ -> ());
  let code_black = ref [] in
  for y = 1583 to 1682 do
    let dec31 = mk y 12 31 in
    let d = ref (mk y 1 1) in
    while Date.compare !d dec31 <= 0 do
      let t = TE.temporal !d in
      if t.Temporal.office.Cel.colour = Colour.Black then code_black := (y, !d, t) :: !code_black;
      d := Date.add_days !d 1
    done
  done;
  Alcotest.(check bool) "the century sweep found at least 100 Colour.Black temporal days" true
    (List.length !code_black >= 100);
  List.iter
    (fun (_, d, (t : (V.season, V.rank) Temporal.t)) ->
      let easter = Computus.gregorian_easter (Date.year d) in
      Alcotest.(check int)
        (Printf.sprintf "%s: the only code-side Colour.Black day is Good Friday (Easter-2)"
           (Date.to_iso8601 d))
        (-2) (Date.to_rata d - Date.to_rata easter);
      Alcotest.(check string)
        (Printf.sprintf "%s: Good Friday's own slug" (Date.to_iso8601 d))
        "ef-passiontide-2-friday"
        (Slug.to_string t.Temporal.office.Cel.slug))
    !code_black

(* ---- 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

(* ---- Review finding 1 (celebrant-rubrics-phase1 fix task): RG 24/25's
   feriae II and III classis (the Advent ferias 17-23 December, the
   Advent/Lent/September Ember sets, and the numbered Lenten/Passiontide
   ferias) are excluded from the Creed by NO check in rubrics_ef.ml -- see
   that file's own RG 23 comment (the [n = -46 ...] branch) for the full
   account. [creed]'s correct [false] answer for them rests entirely on an
   unstated property of {!Rite_ef.Temporal_ef}: no ferial-origin office it
   ever builds carries [subject = Lord] or [subject = Bvm] at [Class2]
   (RG 475(c)'s own rank guard), and none carries a slug on
   [RE.creed_apostle_slugs] (RG 475(e), which has no rank guard at all).
   This test asserts that property directly against real Temporal_ef
   output, rather than leaving it asserted only in prose, so a future
   Temporal_ef change that breaks it fails HERE loudly instead of silently
   changing the Creed.

   Two real, harmless, already-documented exceptions are excluded from the
   [subject] checks rather than papered over: the Sacred Triduum
   ([subject = Lord], [Class1] -- verified in temporal_ef.ml's own
   [triduum_names] branch) and the votive Office of the BVM on Saturday,
   RG 91 entry 27 ([subject = Bvm], [Class4] -- {!TE}'s own [is_bvm_saturday]
   citation). Neither can ever reach [creed]'s 475(c) branch: the Triduum
   is caught earlier by the explicit feria-I-classis check (its own
   [Class1] is not [Class2]), and the BVM Saturday Office is [Class4], not
   [Class2], so 475(c)'s own rank guard excludes it regardless of subject.
   Excluding them here is not a loophole in the test -- it is the precise
   boundary of what RG 475(c) actually reads, confirmed rather than
   assumed by restricting the [Lord]/[Bvm] checks to exactly [Class2] (RG
   24's own II-class population) and [Class3] (RG 25's III-class
   population), the two ranks a real feria can carry that are NOT already
   handled by an explicit branch or a rank mismatch. The apostle-slug
   check has no such carve-out: it is asserted for every ferial-origin day
   of every rank, matching 475(e)'s own unguarded text.

   A THIRD exception -- not anticipated when this test was first written,
   found by actually running it, not by inspection -- had to be excluded
   from the POPULATION itself, not from the subject checks: RG 17(a)'s own
   "secus die 2 ianuarii" fallback (2 January, only in a civil year with
   no 2-5 January Sunday) is a genuine FEAST, [subject = Lord], [Class2],
   sitting outside {!TE.named}'s table for a purely architectural reason
   (temporal_ef.ml's own citation: that table's shape cannot express a
   fallback conditional on a per-year fact) and not always a Sunday
   either, so neither of [is_ferial_origin]'s two filters caught it on the
   first run -- it failed on 1584-01-02 before {!is_holy_name_fallback}
   below was added. Left in as evidence the sweep has real teeth rather
   than trimmed from the span: excluding a genuine mistaken positive by
   name, once found, is the correct fix; narrowing the span to dodge it
   would not have been.

   A day is "ferial-origin" here iff {!TE.named} returns [None] for it
   (nothing in the fixed/movable table of named feasts, vigils and octave
   days claims it), it is not a Sunday (RG 21's own definition: "Nomine
   feriae intelleguntur singuli dies hebdomadae, praeter dominicam" --
   every day of the week EXCEPT Sunday), and it is not the RG 17(a)
   fallback date just above. *)
let contains_substring ~needle haystack =
  let nl = String.length needle and hl = String.length haystack in
  let rec loop i = (i + nl <= hl) && (String.sub haystack i nl = needle || loop (i + 1)) in
  nl = 0 || loop 0

(* RG 17(a)'s own "secus die 2 ianuarii" fallback (temporal_ef.ml's own
   citation on [holy_name_fallback_date]): a genuine FEAST, [subject =
   Lord], [Class2] -- legitimately outside {!TE.named}'s table only for an
   architectural reason (that table's bare [Date.t -> ... option] shape
   cannot express a fallback conditional on a per-year fact,
   [holy_name_sunday y = None]), never a ferial one, and it is not always
   a Sunday, so the weekday filter below does not catch it either. Found
   by running this test before this exclusion existed: it failed on
   1584-01-02, a real year with no 2-5 January Sunday. *)
let is_holy_name_fallback (d : Date.t) =
  let y = Date.year d in
  Date.compare d (TE.holy_name_fallback_date y) = 0 && TE.holy_name_sunday y = None

let is_ferial_origin (d : Date.t) =
  TE.named d = None && Date.weekday d <> Date.Sun && not (is_holy_name_fallback d)

(* Span: civil years 1583-1682, the domain's own opening century -- every
   RG 24/25 population recurs every civil year (Advent 17-23, all three
   non-Whitsun Ember sets, and Lenten/Passiontide ferias are none of them
   rare or conditional), so one year already gives full COVERAGE; a
   century instead of one is for confidence the invariant is not an
   accident of a single Easter date, at a cost (day-by-day calls to the
   pure {!TE.temporal}, no Calendar/Precedence resolution) cheap enough to
   stay in the default, unsampled suite. 1583 is also the domain's own
   lower bound, and this span contains 1598, the earliest possible Easter
   (22 March) in [1583, 2500] per {!test_easter_extremes} above -- a real,
   not merely nominal, edge case. Coverage is VERIFIED below via counters,
   not assumed: a test that never actually walks an Ember day would prove
   nothing about Ember days. *)
let test_ferial_origin_never_carries_lord_bvm_or_apostle_slug () =
  let total = ref 0
  and advent_ember = ref 0
  and lent_ember = ref 0
  and september_ember = ref 0
  and advent_17_23 = ref 0
  and lenten_passiontide_class3 = ref 0 in
  for y = 1583 to 1682 do
    let dec31 = mk y 12 31 in
    let d = ref (mk y 1 1) in
    while Date.compare !d dec31 <= 0 do
      (if is_ferial_origin !d then begin
         let t = TE.temporal !d in
         incr total;
         let office = t.Temporal.office in
         let slug = Slug.to_string office.Cel.slug in
         let label = Printf.sprintf "%s (%s, rank=%s)" (Date.to_iso8601 !d) slug
             (match office.Cel.rank with
             | V.Class1 -> "1"
             | V.Class2 -> "2"
             | V.Class3 -> "3"
             | V.Class4 -> "4")
         in
         (* Coverage tallies -- checked against zero below. *)
         if contains_substring ~needle:"advent-ember" slug then incr advent_ember;
         if contains_substring ~needle:"lent-ember" slug then incr lent_ember;
         if contains_substring ~needle:"september-ember" slug then incr september_ember;
         if Date.month !d = 12 && Date.day !d >= 17 && Date.day !d <= 23 then incr advent_17_23;
         if
           office.Cel.rank = V.Class3
           && (t.Temporal.season = V.Lent || t.Temporal.season = V.Passiontide)
         then incr lenten_passiontide_class3;
         (* The invariant itself. Apostle slugs: every rank, no carve-out. *)
         Alcotest.(check bool) (label ^ ": not on the apostle-natalicium slug list") false
           (List.mem slug RE.creed_apostle_slugs);
         (* Lord/Bvm subject: only at the two ranks a real RG 24/25 feria
            can carry -- see this test's own header for why Class1
            (Triduum) and Class4 (BVM Saturday) are deliberately excluded. *)
         if office.Cel.rank = V.Class2 || office.Cel.rank = V.Class3 then begin
           Alcotest.(check bool) (label ^ ": not subject Lord") false (office.Cel.subject = Subject.Lord);
           Alcotest.(check bool) (label ^ ": not subject Bvm") false (office.Cel.subject = Subject.Bvm)
         end
       end);
      d := Date.add_days !d 1
    done
  done;
  Alcotest.(check bool) "span examined a real number of ferial-origin days" true (!total > 10_000);
  Alcotest.(check bool) "span reached Advent Ember days" true (!advent_ember > 0);
  Alcotest.(check bool) "span reached Lent Ember days" true (!lent_ember > 0);
  Alcotest.(check bool) "span reached September Ember days" true (!september_ember > 0);
  Alcotest.(check bool) "span reached Advent 17-23 ferias" true (!advent_17_23 > 0);
  Alcotest.(check bool) "span reached Lenten/Passiontide III-class ferias" true
    (!lenten_passiontide_class3 > 0)

(* ---- 237(a): the three named Paschaltide days ---- *)

let test_237a_easter_sunday () = check_te_deum "237(a): Easter Sunday" 2026 4 5 true
let test_237a_low_sunday () = check_te_deum "237(a): Low Sunday" 2026 4 12 true
let test_237a_pentecost () = check_te_deum "237(a): Pentecost Sunday" 2026 5 24 true

(* ---- 237(f): the vigils of Ascension and Pentecost, both otherwise
   reachable by 238(b) (the Ascension vigil is Class2) or moot to it
   (the Pentecost vigil is Class1) ---- *)

let test_237f_ascension_vigil () = check_te_deum "237(f): Vigil of the Ascension" 2026 5 13 true
let test_237f_pentecost_vigil () = check_te_deum "237(f): Vigil of Pentecost" 2026 5 23 true

(* ---- 237(d): the three privileged octaves, reusing [creed]'s own windows
   -- a day within the Nativity octave whose OWN office is a real saint
   (RG 67's "Com. octavae Nativitatis"), and an Easter-week feria outside
   the narrower 237(a) list ---- *)

let test_237d_nativity_octave_saint () =
  check_te_deum "237(d): St John within the Nativity octave" 2026 12 27 true

let test_237d_easter_week_feria () = check_te_deum "237(d): Easter Tuesday" 2026 4 7 true

(* ---- 238(b): the omissible (Class2/Class3) vigils -- St Lawrence's own,
   picked in a year where nothing displaces it, confirmed via
   `colitur day 2026` first the same way the Creed tests were derived ---- *)

let test_238b_omissible_vigil () =
  (* 9 August 2026 is itself a Sunday (the vigil impeded, see 237(b)'s own
     test below, which reuses that exact date) -- 2027 is picked instead,
     confirmed via `colitur day 2027` first, the same discipline every
     other date in this file follows. *)
  check_te_deum "238(b): Vigil of St Lawrence" 2027 8 9 false

(* ---- The Nativity Vigil (I class): NOT literally named by 238(b)'s own
   "II et III classis" text -- excluded here on the structural RG 21/35
   taxonomy inference [te_deum]'s own comment states explicitly as an
   inference, not a citation. Flagged the same way in the task report;
   checked against the FIUV Ordo's own Te Deum marker for 24 December in
   test_fiuv_ordo.ml, which is this inference's real corroboration. ---- *)

let test_nativity_vigil_excluded_by_inference () =
  check_te_deum "Nativity Vigil (I class): excluded, inference not literal 238(b)" 2026 12 24 false

(* ---- 238(c)/RG23: Ash Wednesday and Good Friday (moot -- no Mass, but the
   RG23 feria-I-classis exclusion still answers [false] regardless of
   colour) ---- *)

let test_238c_ash_wednesday () = check_te_deum "238(c)/RG23: Ash Wednesday" 2026 2 18 false
let test_238c_good_friday () = check_te_deum "238(c)/RG23: Good Friday" 2026 4 3 false

(* ---- 237(g): the votive Office of the BVM on Saturday -- reused directly
   from test/cli.t's own pinned example ---- *)

let test_237g_bvm_saturday () = check_te_deum "237(g): BVM Saturday Office" 2026 7 11 true

(* ---- 237(e): a plain, non-octave, non-Sunday feria of Christmastide (2-5
   January) ---- *)

let test_237e_christmastide_feria () =
  check_te_deum "237(e): 2 January, a Christmastide feria" 2026 1 2 true

(* ---- 237(b): an ordinary II-class Sunday outside Septuagesima, and its
   own explicit exception (a Septuagesima/Sexagesima Sunday, [Class2],
   excepted by name) ---- *)

let test_237b_ordinary_class2_sunday () =
  check_te_deum "237(b): an ordinary Time-after-Pentecost Sunday" 2026 8 9 true

(* RE-VERIFIED against the CORRECTED FIUV extraction ([te_deum]'s own
   237(a) comment has the full account of the extractor bug an earlier
   pass of this task found and fixed): 237(b)'s own literal "exceptis
   dominicis in Septuagesima, in Sexagesima et in Quinquagesima" holds
   after all -- these two Sundays do NOT say the Te Deum, confirmed
   against the corrected data, not merely the original transcription
   alone. *)
let test_237b_septuagesima_exception () =
  check_te_deum "237(b)'s own exception: Septuagesima Sunday" 2027 1 24 false

let test_237b_sexagesima_exception () =
  check_te_deum "237(b)'s own exception: Sexagesima Sunday" 2027 1 31 false

(* ---- 237(c): a genuine sanctoral feast kept during a penitential season
   (St Paul of the Cross, Class3, outside any privileged window) and one in
   Ordinary Time (Lawrence, Class2) ---- *)

let test_237c_sanctoral_feast () = check_te_deum "237(c): a sanctoral feast (Lawrence)" 2026 8 10 true

(* Found alongside the FIUV extractor-bug fix ([te_deum]'s own 237(c)
   comment has the full account): Passion Sunday and Palm Sunday are BOTH
   entries in {!Temporal_ef.named} (named individually for RG 91 entry 6),
   which without the [temporal.weekday <> Sun] guard on 237(c)'s own
   [named<>None] disjunct would wrongly grant them [true] by ACCIDENT of
   table membership -- neither is a genuine "festum" (RG 35's own "dies
   dominica" is its own category), and both are [Class1] Sundays 237(b)'s
   own [Class2] guard already excludes. Confirmed [false] directly against
   the corrected FIUV extraction. *)
let test_passion_sunday_not_a_festum () = check_te_deum "Passion Sunday is not a festum for 237(c)" 2026 3 22 false
let test_palm_sunday_not_a_festum () = check_te_deum "Palm Sunday is not a festum for 237(c)" 2026 3 29 false

(* ---- 238(d): the Requiem proxy, shared with [creed]'s own 476(f) and the
   SAME two-member {!Colour.Black} population
   {!test_colour_black_population_is_exactly_two} already asserts ---- *)

let test_238d_all_souls () = check_te_deum "238(d): All Souls' Day (transferred)" 2025 11 3 false

(* ---- RG 431(c): Holy Thursday and the Easter Vigil Mass say the Gloria
   even though neither day's own Matins says the Te Deum (both are
   feria-I-classis, [te_deum]'s own 238(c) branch) -- the one place [gloria]
   and [te_deum] genuinely disagree on real data. Holy Saturday also proves
   431(c) outranks 432(b): its own colour is [Violet]
   (Passiontide's [season_colour]), yet the Gloria is still said. ---- *)

let test_431c_holy_thursday () = check_gloria "431(c): Holy Thursday" 2026 4 2 true
let test_431c_easter_vigil () = check_gloria "431(c): the Easter Vigil Mass (Holy Saturday)" 2026 4 4 true

(* ---- RG 432(b): violet vestments -- independent of [te_deum], checked
   against an Advent Sunday (I class, so [te_deum] would ALSO answer
   [false] here via its own 237(b) rank guard: this is a same-answer
   witness, not proof of independent teeth -- see the task report for why
   no fully independent (te_deum=true, violet) witness exists anywhere in
   the shipped 1583-9999 domain: every real [Colour.Violet] [Feast] entry
   is one of the five sanctoral vigils, already excluded by [te_deum]'s
   own [is_vigil] branch either way). ---- *)

let test_432b_violet_sunday () = check_gloria "432(b): Advent I Sunday, violet" 2026 11 29 false

(* ---- RG 432(d): the Requiem proxy, same population as 238(d) above ---- *)

let test_432d_all_souls () = check_gloria "432(d): All Souls' Day (transferred)" 2025 11 3 false

(* ---- RG 431(a)/432(a): mirrors [te_deum] for everything not already
   decided above -- one true, one false, neither reachable via 431(c)/432(b)/
   432(d) ---- *)

let test_431a_mirrors_te_deum_true () =
  check_gloria "431(a): mirrors [te_deum]=true (a sanctoral feast, Lawrence)" 2026 8 10 true

let test_432a_mirrors_te_deum_false () =
  check_gloria "432(a): mirrors [te_deum]=false (an ordinary Advent feria)" 2025 12 1 false

(* ---- The Rogation Monday/Tuesday cascade (celebrant-rubrics-phase1 Bug 1
   fix, 2026-08-22): {!Rite_ef.Temporal_ef.temporal} used to hardcode
   [Colour.Violet] on these two days with no citation, which silenced
   [gloria] via 432(b) even though 237(e)'s own Paschaltide-feria branch
   already said [te_deum]=true underneath. Fixed to [season_colour] (RG
   119: white from the Paschal Vigil to the Pentecost Vigil exclusive; RG
   128's own violet list never names Rogation days; RG 88 -- the violet
   belongs to the Rogation MASS, not the Office this field describes).
   2028-05-22/23: neither day is impeded that year (test_golden.ml's own
   [test_rogation_{monday,tuesday}_white_2028] pins the colour side of the
   same fix), so both resolve to white and both now say the Gloria, MIRROR
   OF [te_deum] via 431(a) once 432(b)'s violet guard no longer fires --
   MATCHING the LMS Ordo witness (2023-2024, 2024-05-06, "FERIA IV Cl W
   ... Gl"). Reverting temporal_ef.ml's fix alone (mutation-checked while
   building this task) turns both [false], the pre-fix answer. *)
let test_rogation_monday_gloria_2028 () = check_gloria "Rogation Monday: white -> Gloria said (2028)" 2028 5 22 true
let test_rogation_tuesday_gloria_2028 () = check_gloria "Rogation Tuesday: white -> Gloria said (2028)" 2028 5 23 true

(* ---- Domain-wide sanity, task requirement 5: "every violet day must be
   false (RG 432 b), and every Requiem must be false (432 d)". Two
   invariants, checked over every day {!Cal.year} resolves -- [Cal.year],
   not [Cal.day] in a loop, for the same cost reason
   {!test_every_sunday_in_2026_says_the_creed} above already gives.

   FAST (default suite): a fixed 200-year span, 1583-1782 -- large enough
   to cross multiple Easter cycles and every season repeatedly, cheap
   enough to stay in `dune test`'s own budget. EXHAUSTIVE (gated the same
   way {!test_validate.test_exhaustive_domain_sweep} already is, via
   COLITUR_EXHAUSTIVE_SWEEP): the full 1583-9999 domain, also tallying and
   printing the true/false distribution so the measurement this task asks
   for is not merely "did the invariant hold" but has a number attached --
   see the task report for the printed figures. *)
(* NOT a blanket "every violet day is [gloria]=false" (the task brief's own
   phrasing, taken literally, is one exception too strong): RG 431(c) is a
   NAMED, lex-specialis override for the Easter Vigil Mass, and colitur's
   own per-day colour model gives Holy Saturday [Colour.Violet]
   ([gloria]'s own header has the full argument for why -- the historical
   violet-to-white vestment change happens AT the Gloria itself, a
   per-action nuance this whole day/colour model already cannot express).
   So the real invariant, checked here, is "every violet day is
   [gloria]=false EXCEPT the Easter Vigil (Easter offset -1), which is
   [gloria]=true BY DESIGN" -- and the exception is asserted to be EXACTLY
   that one shape, every year, nothing else: [n = -1] is checked directly
   rather than merely excluded, so a second, unexpected (violet,
   gloria=true) day anywhere in the domain still fails loudly. Pushed back
   on the task brief's own simplified phrasing rather than silently
   special-cased -- see the task report. *)
let check_gloria_invariants_for_year y (counts : (int * int * int * int) ref) =
  let days = Cal.year ctx layer y in
  (* Per-date, NOT once per loop iteration off [y]: a liturgical year
     "opening in civil year y" ({!Rite.t.year_start}'s own doc comment)
     runs from that year's Advent into MOST of civil year y+1 -- so
     [Cal.year ctx layer y]'s own array holds dates whose civil year is
     y+1 for the whole Christmas-to-Pentecost span, governed by EASTER OF
     y+1, not y. A single [Computus.gregorian_easter y] computed once here
     wrongly used year y's own Easter for those dates -- found live: it
     misidentified 1584-03-31 (Holy Saturday, governed by 1584's Easter,
     surfaced while processing loop iteration y=1583) as an ordinary
     violet day instead of the Vigil's own 431(c) exception, because
     Easter 1583 (not 1584) was subtracted. Fixed by keying off
     [Date.year d.LD.date] instead, which is always safe here: Holy
     Saturday and Easter Sunday are never more than a few days apart and
     never cross a civil-year boundary. *)
  let violet, black, gloria_true, gloria_false = !counts in
  let violet = ref violet
  and black = ref black
  and gloria_true = ref gloria_true
  and gloria_false = ref gloria_false in
  Array.iter
    (fun (d : (V.season, V.rank) LD.t) ->
      (if d.LD.gloria then incr gloria_true else incr gloria_false);
      if d.LD.observed.Cel.colour = Colour.Violet then begin
        incr violet;
        let easter = Computus.gregorian_easter (Date.year d.LD.date) in
        let n = Date.to_rata d.LD.date - Date.to_rata easter in
        if n = -1 then
          Alcotest.(check bool)
            (Printf.sprintf "%s: the Easter Vigil's own 431(c) override, [gloria]=true despite violet"
               (Date.to_iso8601 d.LD.date))
            true d.LD.gloria
        else
          Alcotest.(check bool)
            (Printf.sprintf "%s: violet -> [gloria]=false (RG 432(b))" (Date.to_iso8601 d.LD.date))
            false d.LD.gloria
      end;
      if d.LD.observed.Cel.colour = Colour.Rose then
        (* Gaudete/Laetare -- [gloria] reads [false] here via [te_deum]'s
           own [Class2] guard on 237(b) (both Rose Sundays are [Class1] BY
           CONSTRUCTION -- {!Rite_ef.Rubrics_ef.gloria}'s own 432(b)
           citation has the full "checked, then found redundant, then
           removed" account of why 432(b) itself does NOT need its own
           Rose branch). Not folded into the [violet] counter above:
           keeping the two colours separately tallied is what let this
           invariant catch the Gaudete/Laetare gap live in the first place
           (found via the LMS Ordo, 2023-12-17 and 2024-03-10) rather than
           silently averaging it away inside one shared bucket. *)
        Alcotest.(check bool)
          (Printf.sprintf "%s: Rose (Gaudete/Laetare) -> [gloria]=false" (Date.to_iso8601 d.LD.date))
          false d.LD.gloria;
      if d.LD.observed.Cel.colour = Colour.Black then begin
        incr black;
        Alcotest.(check bool)
          (Printf.sprintf "%s: Requiem (black) -> [gloria]=false (RG 432(d))" (Date.to_iso8601 d.LD.date))
          false d.LD.gloria
      end)
    days;
  counts := (!violet, !black, !gloria_true, !gloria_false)

let test_domain_violet_implies_no_gloria_sample () =
  let counts = ref (0, 0, 0, 0) in
  for y = 1583 to 1782 do
    check_gloria_invariants_for_year y counts
  done;
  let violet, _, _, _ = !counts in
  Alcotest.(check bool) "the 200-year sample reached a real number of violet days" true (violet > 1000)

let test_domain_requiem_implies_no_gloria_sample () =
  (* Separate test name, same underlying sweep as the one immediately above
     -- {!Cal.year} is only computed once per year regardless (module-level
     [ctx]/[layer], no per-test reload), so this is not a second pass over
     the domain, only a second, independently-named assertion on the same
     tally, matching how {!test_colour_black_population_is_exactly_two}
     above separates its own DATA and CODE checks into one function while
     this pair keeps violet and black as two named outcomes. *)
  let counts = ref (0, 0, 0, 0) in
  for y = 1583 to 1782 do
    check_gloria_invariants_for_year y counts
  done;
  let _, black, _, _ = !counts in
  Alcotest.(check bool) "the 200-year sample reached at least one Requiem day" true (black > 0)

let colitur_exhaustive_sweep_env = "COLITUR_EXHAUSTIVE_SWEEP"

let test_exhaustive_gloria_domain_sweep () =
  if Sys.getenv_opt colitur_exhaustive_sweep_env = None then Alcotest.skip ()
  else begin
    let counts = ref (0, 0, 0, 0) in
    for y = 1583 to 9999 do
      check_gloria_invariants_for_year y counts
    done;
    let violet, black, gloria_true, gloria_false = !counts in
    Printf.printf
      "gloria domain sweep 1583..9999: violet=%d black=%d gloria_true=%d gloria_false=%d total=%d\n%!"
      violet black gloria_true gloria_false (gloria_true + gloria_false);
    Alcotest.(check bool) "the full domain reached a real number of violet days" true (violet > 100_000);
    Alcotest.(check bool) "the full domain reached a real number of Requiem days" true (black > 1_000)
  end

(* ---- RG 482-499, the preface -- 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 branch, resolved against REAL calendar dates
   through the shipped data, the same discipline every other section of
   this file already follows -- most of these dates were cross-checked
   directly against the FIUV Ordo's own [praef] column
   (test/fixtures/fiuv-ordo-2025-2026.sexp) before being pinned here, not
   merely derived from the Latin text in isolation; see [preface]'s own
   header for the full account of what that cross-check settled. ---- *)

module Pref = Colitur_kernel.Preface

let preface_on y m d = (day_on y m d).LD.preface

let preface_string_on y m d =
  match preface_on y m d with Some p -> Pref.to_string p | None -> "-"

let check_preface name expected y m d =
  Alcotest.(check string) name (Pref.to_string expected) (preface_string_on y m d)

let check_no_preface name y m d = Alcotest.(check string) name "-" (preface_string_on y m d)

(* ---- Good Friday: no Mass at all, [None] -- checked ahead of the
   Requiem/Black-colour branch (this file's own [preface] header explains
   why the two need different answers, unlike creed/gloria/te_deum). ---- *)
let test_preface_good_friday_no_mass () = check_no_preface "Good Friday: no Mass, no preface" 2026 4 3

(* ---- RG 499: the Requiem proxy, the SAME two-member {!Colour.Black}
   population {!test_colour_black_population_is_exactly_two} already
   asserts, with Good Friday split out above. ---- *)
let test_preface_499_all_souls () =
  check_preface "RG 499: All Souls' Day (transferred)" Pref.Requiem 2025 11 3

(* ---- RG 487(a): the two fixed Holy Cross feast triggers. ---- *)
let test_preface_487a_exaltation () =
  check_preface "RG 487(a): the Exaltation of the Holy Cross" Pref.Holy_cross 2026 9 14

let test_preface_487a_precious_blood () =
  check_preface "RG 487(a): the Most Precious Blood" Pref.Holy_cross 2026 7 1

(* ---- RG 491/492: Sacred Heart, Christ the King -- single named days. ---- *)
let test_preface_491_sacred_heart () = check_preface "RG 491: the Sacred Heart" Pref.Sacred_heart 2026 6 12
let test_preface_492_christ_the_king () =
  check_preface "RG 492: Christ the King" Pref.Christ_the_king 2026 10 25

(* ---- RG 490(a): Ascension Day itself. ---- *)
let test_preface_490a_ascension_day () = check_preface "RG 490(a): Ascension Day" Pref.Ascension 2026 5 14

(* ---- RG 494(a): Trinity Sunday itself. ---- *)
let test_preface_494a_trinity_sunday () = check_preface "RG 494(a): Trinity Sunday" Pref.Trinity 2026 5 31

(* ---- RG 496: St Joseph's two feasts. ---- *)
let test_preface_496_joseph_spouse () = check_preface "RG 496: St Joseph, Spouse of the BVM" Pref.St_joseph 2026 3 19
let test_preface_496_joseph_workman () = check_preface "RG 496: St Joseph the Workman" Pref.St_joseph 2027 5 1

(* ---- RG 495: a genuine Marian FEAST via [marian_slugs] (the Assumption),
   and the VOTIVE-shaped BVM Saturday Office via [subject = Bvm] -- the
   latter is the live witness for RG 495's own "et votivis" half on this
   engine's data (this file's own [preface] header has the full account,
   corroborated against the Ordo on 3/10 January 2026). ---- *)
let test_preface_495_assumption () = check_preface "RG 495: the Assumption (marian_slugs)" Pref.Bvm 2026 8 15
let test_preface_495_bvm_saturday_office () =
  check_preface "RG 495: the BVM Saturday Office (subject=Bvm, votive-shaped)" Pref.Bvm 2026 7 11

(* ---- RG 484(a)'s own explicit Purification clause: 2 February, checked
   as a standalone slug trigger regardless of season -- see [preface]'s
   own header for why this needed to be independent of the Nativity
   WINDOW below (2 February is nowhere near it). ---- *)
let test_preface_484a_purification () = check_preface "RG 484(a): the Purification" Pref.Nativity 2026 2 2

(* ---- RG 484(a)/(b) merged: the Nativity octave window, 25 December-1
   January, PLUS RG 484(b)'s own extra "2 ad 5 ianuarii" days -- one
   contiguous window. St Stephen (26 December) is the live witness that
   this OUTRANKS an ordinary saint's own otherwise-Common preface;
   {!creed}'s own 475(d) test picks the identical date for the identical
   "occurring inside a privileged window" shape. ---- *)
let test_preface_484_nativity_day () = check_preface "RG 484(a): Christmas Day itself" Pref.Nativity 2026 12 25
let test_preface_484_stephen_in_octave () =
  check_preface "RG 484(b): St Stephen, occurring within the Nativity octave" Pref.Nativity 2026 12 26

let test_preface_484_jan1_octave_day () =
  check_preface "RG 484(a): 1 January, the Octave Day" Pref.Nativity 2026 1 1

let test_preface_484b_jan2to5 () =
  (* 2 January 2026 (a plain Christmastide feria, "ef-christmas-1-friday",
     confirmed via `colitur day 2026`) -- 3 January that year is the BVM
     Saturday Office instead (RG 495's own "et votivis" outranking this
     window, this file's own [test_preface_495_bvm_saturday_office] and
     [preface]'s own header have the full account), so this date is picked
     specifically to witness the PLAIN de-Tempore grant, uncomplicated by
     that override. *)
  check_preface "RG 484(b): 2 January, the extra de-Tempore days" Pref.Nativity 2026 1 2

(* ---- RG 484(b)'s own NARROWER exception: an Apostle/Evangelist inside
   the Nativity octave is STILL overridden to [Nativity] (unlike every
   other window, which an Apostle's own preface outranks -- the next test
   below). John the Evangelist, 27 December, is the live witness this
   file's own [preface] header cites. ---- *)
let test_preface_484b_overrides_apostle_in_octave () =
  check_preface "RG 484(b): St John the Evangelist, inside the Nativity octave, still [Nativity]"
    Pref.Nativity 2026 12 27

(* ---- RG 497: the Apostle/Evangelist natalicia list, reused from
   {!creed_apostle_slugs} -- OUTSIDE the Nativity octave, an Apostle keeps
   his own preface even inside another window (Sts Philip & James, 11 May,
   inside the Paschaltide/Easter window -- the FIUV Ordo's own witness
   this file's own [preface] header cites for RG 484(b)'s narrower
   carve-out, checked from the other direction). ---- *)
let test_preface_497_barnabas () = check_preface "RG 497: St Barnabas" Pref.Apostles 2026 6 11
let test_preface_497_peter_paul () = check_preface "RG 497: Sts Peter & Paul" Pref.Apostles 2026 6 29
let test_preface_497_philip_james_inside_easter_window () =
  check_preface "RG 497: Sts Philip & James, inside the Easter window, still [Apostles]" Pref.Apostles 2026 5 11

(* ---- RG 485(a)/(b): Epiphany itself and its own Baptism commemoration
   (a), the wider 7-13 January window (b) -- Holy Family Sunday (11
   January 2026) is the live witness that a temporal-origin Sunday inside
   this window gets [Epiphany], not some Holy-Family-specific preface this
   engine has none of. ---- *)
let test_preface_485a_epiphany_day () = check_preface "RG 485(a): Epiphany Day" Pref.Epiphany 2026 1 6
let test_preface_485a_baptism_commemoration () =
  check_preface "RG 485(a): the Commemoration of the Baptism of the Lord" Pref.Epiphany 2026 1 13

let test_preface_485b_holy_family_sunday () =
  check_preface "RG 485(b): Holy Family Sunday, inside the 7-13 January window" Pref.Epiphany 2026 1 11

(* ---- RG 486(a)/(b): the Lenten window, Ash Wednesday through the
   Saturday before Passion Sunday I. ---- *)
let test_preface_486_ash_wednesday () = check_preface "RG 486(a): Ash Wednesday" Pref.Lent 2026 2 18
let test_preface_486_lent_saturday_boundary () =
  check_preface "RG 486: the Saturday immediately before Passion Sunday I" Pref.Lent 2026 3 21

(* ---- RG 487(a)/(b): the Passiontide window, Passion Sunday I through
   Holy Thursday inclusive. ---- *)
let test_preface_487_passion_sunday () = check_preface "RG 487: Passion Sunday I" Pref.Holy_cross 2026 3 22
let test_preface_487_palm_sunday () = check_preface "RG 487: Palm Sunday" Pref.Holy_cross 2026 3 29
let test_preface_487_holy_thursday () = check_preface "RG 487: Holy Thursday" Pref.Holy_cross 2026 4 2

(* ---- RG 489(a)/(b): the Easter window, the Vigil Mass through the vigil
   of the Ascension. ---- *)
let test_preface_489_easter_vigil_mass () = check_preface "RG 489(a): the Easter Vigil Mass" Pref.Easter 2026 4 4
let test_preface_489_easter_sunday () = check_preface "RG 489(a): Easter Sunday" Pref.Easter 2026 4 5
let test_preface_489_ascension_vigil () = check_preface "RG 489(b): the vigil of the Ascension" Pref.Easter 2026 5 13

(* ---- RG 490(b): the post-Ascension window. ---- *)
let test_preface_490b_post_ascension_feria () =
  check_preface "RG 490(b): the Friday after Ascension" Pref.Ascension 2026 5 15

let test_preface_490b_sunday_after_ascension () =
  check_preface "RG 490(b): the Sunday after Ascension" Pref.Ascension 2026 5 17

(* ---- RG 493(a)/(b): the Pentecost-octave window, the vigil through the
   following Saturday (the Ember Saturday). ---- *)
let test_preface_493_pentecost_vigil () = check_preface "RG 493(a): the vigil of Pentecost" Pref.Holy_spirit 2026 5 23
let test_preface_493_pentecost_sunday () = check_preface "RG 493(a): Pentecost Sunday" Pref.Holy_spirit 2026 5 24
let test_preface_493_ember_saturday () =
  check_preface "RG 493(a): the Ember Saturday within the Pentecost octave" Pref.Holy_spirit 2026 5 30

(* ---- RG 494(b): the Trinity de-Tempore Sundays -- an Advent Sunday
   ([temporal.season = Advent], [Class1] by construction) and an ordinary
   Time-after-Pentecost Sunday. Both now read off [temporal]'s own season,
   not [observed]'s rank -- see the next test for why the distinction is
   live, not merely stylistic. ---- *)
let test_preface_494b_advent_sunday () = check_preface "RG 494(b): Advent I Sunday" Pref.Trinity 2026 11 29
let test_preface_494b_ordinary_class2_sunday () =
  check_preface "RG 494(b): an ordinary Time-after-Pentecost Sunday" Pref.Trinity 2026 8 9

(* ---- RG 494(b), the RG 16(a)-shaped fix round: All Saints' Day (1
   November), [Class1], observed OUTRIGHT over the ordinary Sunday it
   commemorates -- found via the FIUV Ordo (its own entry reads "Trinit.
   vel de Omnibus Sanctis et Patronis", the "vel..." half an alternate
   ORATIO reference, the SAME pattern every other "X vel Y" [praef] value
   in that fixture already follows, never a second genuine preface). No
   dedicated preface exists for All Saints among RG 484-497's own
   fourteen, so RG 482's chain falls to the SUNDAY's own de-Tempore grant
   regardless of which rank actually won the day -- exactly why this
   branch must read [temporal]'s season, not [observed]'s [Class1] rank,
   which an EARLIER version of [preface] wrongly required to equal
   [Class2] and so answered [Common] here instead.

   1 November 2026 is itself a Sunday (confirmed via `colitur day 2026`:
   "all-saints class-1 white +ef-time-after-pentecost-sunday-23"), the
   exact shape this fix concerns -- picked for that reason, not merely
   because it is All Saints' Day. *)
let test_preface_494b_all_saints_class1_sunday () =
  check_preface "RG 494(b): All Saints' Day, Class1, still Trinity (no dedicated preface exists)" Pref.Trinity
    2026 11 1

(* ---- RG 498: the Common residual -- Corpus Christi is the deliberately
   chosen witness (NOT simply "any ordinary weekday"): the 1962 Missal
   gives it no preface of its own at all, only an OPTIONAL alternative
   (Sacred Heart's), so it takes the plain Common, confirmed against the
   Ordo directly (4 June 2026: "comm. vel de Cor Sacratissimo") -- a real
   trap this engine's own tier1 list does NOT fall into (no
   "ef-corpus-christi" entry anywhere in it). ---- *)
let test_preface_498_corpus_christi () = check_preface "RG 498: Corpus Christi (no proper of its own)" Pref.Common 2026 6 4

let test_preface_498_plain_saint () = check_preface "RG 498: a plain sanctoral saint (Lawrence)" Pref.Common 2026 8 10

(* ---- Corroborating real-data invariants, task requirement 5: "every day
   in Paschaltide should take the Easter preface unless it has a proper
   one; every Lenten feria the Lent preface". Checked as a SWEEP, not a
   single date, over a fixed sample span (1583-2200, matching the century+
   spans other domain checks in this file already use) -- FAST in the
   default suite, EXHAUSTIVE (1583-9999) gated behind
   COLITUR_EXHAUSTIVE_SWEEP the same way {!test_exhaustive_gloria_domain_sweep}
   above already is.

   The invariant actually checked is NOT the brief's own literal phrasing
   (which is one exception too strong, the identical "pushed back, not
   silently special-cased" stance {!check_gloria_invariants_for_year}'s own
   header already takes for [gloria]/432(b)): every Paschaltide-season day
   takes [Easter], [Ascension] or [Holy_spirit] (its own three seasonal
   windows), OR one of the season-independent title triggers this engine
   can construct inside Paschaltide's real date range ([Bvm]/[St_joseph]/
   [Apostles]/[Requiem]) -- but NEVER [Nativity]/[Epiphany]/[Lent]/[Trinity]/
   [Sacred_heart]/[Christ_the_king]/[Common], none of which any real date
   inside Paschaltide can trigger. Symmetrically for Lent: [Lent] or one of
   [Bvm]/[St_joseph]/[Apostles]/[Requiem], never a preface belonging to a
   date-disjoint window. Two further invariants the sweep found and kept,
   not originally asked for but load-bearing: EVERY Christmastide-season
   day resolves [Nativity], [Epiphany] or [Bvm] (RG 484/485 between them
   leave no gap at all in that season on their own -- confirmed
   exhaustively -- but RG 495's own votive-Mass half, live for the BVM
   Saturday Office, can and does fall on a Christmastide Saturday too;
   found live at 1584-01-07 while first running this exact sweep, kept as
   a real witness rather than narrowed away); and Passiontide, uniquely,
   legitimately
   ALSO produces [Easter] exactly once a year (Holy Saturday's own Vigil
   Mass, [n = -1] -- {!Rite_ef.Rubrics_ef.preface}'s own RG 489(a) comment;
   this engine's day/colour model keeps that date [Passiontide] by season
   even though the Vigil Mass's own preface has already moved to Easter's,
   the same acknowledged per-action nuance [gloria]'s own RG 431(c) comment
   already documents) -- asserted to be EXACTLY once per year, not merely
   allowed, so a second, unexplained (Passiontide, Easter) day anywhere in
   the domain still fails loudly. *)
let check_preface_season_invariants_for_year y (counts : (int * int * int * int) ref) =
  let days = Cal.year ctx layer y in
  let christmastide_gaps = ref 0
  and paschaltide_gaps = ref 0
  and lent_gaps = ref 0
  and passiontide_easter = ref 0 in
  let christmastide_gaps0, paschaltide_gaps0, lent_gaps0, passiontide_easter0 = !counts in
  christmastide_gaps := christmastide_gaps0;
  paschaltide_gaps := paschaltide_gaps0;
  lent_gaps := lent_gaps0;
  passiontide_easter := passiontide_easter0;
  Array.iter
    (fun (d : (V.season, V.rank) LD.t) ->
      let label = Printf.sprintf "%s (preface=%s)" (Date.to_iso8601 d.LD.date)
          (match d.LD.preface with Some p -> Pref.to_string p | None -> "NONE")
      in
      (match d.LD.temporal.Temporal.season with
      | V.Christmastide -> (
          match d.LD.preface with
          | Some (Pref.Nativity | Pref.Epiphany | Pref.Bvm) -> ()
          | _ ->
              incr christmastide_gaps;
              Alcotest.failf "%s: Christmastide day with neither Nativity, Epiphany nor Bvm" label)
      | V.Paschaltide -> (
          match d.LD.preface with
          | Some (Pref.Easter | Pref.Ascension | Pref.Holy_spirit | Pref.Bvm | Pref.St_joseph
                 | Pref.Apostles | Pref.Requiem) ->
              ()
          | _ ->
              incr paschaltide_gaps;
              Alcotest.failf "%s: Paschaltide day with an out-of-window preface" label)
      | V.Lent -> (
          match d.LD.preface with
          | Some (Pref.Lent | Pref.Bvm | Pref.St_joseph | Pref.Apostles | Pref.Requiem) -> ()
          | _ ->
              incr lent_gaps;
              Alcotest.failf "%s: Lent day with an out-of-window preface" label)
      | V.Passiontide ->
          if d.LD.preface = Some Pref.Easter then begin
            incr passiontide_easter;
            let easter = Computus.gregorian_easter (Date.year d.LD.date) in
            Alcotest.(check int)
              (Printf.sprintf "%s: the ONLY Passiontide/Easter day is the Vigil Mass (Easter-1)" label)
              (-1) (Date.to_rata d.LD.date - Date.to_rata easter)
          end
      | _ -> ()))
    days;
  counts := (!christmastide_gaps, !paschaltide_gaps, !lent_gaps, !passiontide_easter)

let test_preface_season_invariants_sample () =
  let counts = ref (0, 0, 0, 0) in
  for y = 1583 to 1782 do
    check_preface_season_invariants_for_year y counts
  done;
  let _, _, _, passiontide_easter = !counts in
  Alcotest.(check bool) "the 200-year sample found the once-a-year Passiontide/Easter exception" true
    (passiontide_easter >= 200)

let test_exhaustive_preface_season_domain_sweep () =
  if Sys.getenv_opt colitur_exhaustive_sweep_env = None then Alcotest.skip ()
  else begin
    let counts = ref (0, 0, 0, 0) in
    for y = 1583 to 9999 do
      check_preface_season_invariants_for_year y counts
    done;
    let christmastide_gaps, paschaltide_gaps, lent_gaps, passiontide_easter = !counts in
    Printf.printf
      "preface season sweep 1583..9999: christmastide_gaps=%d paschaltide_gaps=%d lent_gaps=%d \
       passiontide_easter=%d\n%!"
      christmastide_gaps paschaltide_gaps lent_gaps passiontide_easter;
    Alcotest.(check int) "zero Christmastide gaps anywhere in the domain" 0 christmastide_gaps;
    Alcotest.(check int) "zero Paschaltide out-of-window prefaces anywhere in the domain" 0 paschaltide_gaps;
    Alcotest.(check int) "zero Lent out-of-window prefaces anywhere in the domain" 0 lent_gaps;
    (* 8416, not 8417 (the domain's own year count): a genuine, pre-existing
       DOMAIN-BOUNDARY edge, not a preface defect -- {!Cal.year}'s own
       liturgical year "opening in civil year 9999" cannot construct any
       date past 1583-9999 ({!Date.make}'s own validated range), so it
       returns only 34 days (28 November-31 December), never reaching its
       own Easter/Holy Saturday (which would fall in year 10000). Found by
       running this exact sweep: it returned 8416 first, not asserted
       blindly at 8417 -- checked directly ([Cal.year ctx layer 9999]
       alone, printed length 34) before writing this comment, the same
       "measure before adjudicating" discipline every allow-list in this
       project follows. The identical edge is why
       docs/research/rules-register.md's own Layer.index citation already
       reads "both edges, 1582 and 10000, bit during development" --
       this sweep is a second witness to the same known boundary, not a
       new one. *)
    Alcotest.(check int) "one Passiontide/Easter day per year, every year but the domain's own last" 8416
      passiontide_easter
  end

(* ---- ITEM 1: RG 111(a), the sung-Mass commemoration cap
   ({!Colitur_kernel.Precedence.sung_mass_commemorations}) -- two real
   calendar days, one of each shape, resolved through the identical
   [Cal.day] pipeline every other test in this file uses. See
   precedence.mli's own citation for the rubric and the reasoning for why
   this is a derivation over [LD.commemorations] (the Low-Mass admitted
   set), not a second stored field. ---- *)

module Prec = Colitur_kernel.Precedence

let sung_slugs y m d =
  List.map
    (fun (c, _) -> Slug.to_string c.Cel.slug)
    (Prec.sung_mass_commemorations (day_on y m d).LD.commemorations)

let test_rg111a_ordinary_only_dropped () =
  (* 2026-08-14: Vigil of the Assumption, commemorating [eusebius-confessor]
     ORDINARILY (no privileged category applies) -- confirmed via `colitur
     day 2026`. At Low Mass this is admitted (RG 111(c)); at a
     non-conventual Sung Mass RG 111(a) admits it not at all. *)
  Alcotest.(check (list string)) "RG 111(a): an ordinary-only Low-Mass set is empty at Sung Mass" []
    (sung_slugs 2026 8 14)

let test_rg111a_privileged_kept () =
  (* 2026-04-25: St Mark, commemorating the Major Litanies -- RG 80/109(f),
     a PRIVILEGED commemoration (data/ef/adjustments.sexp's own [Add
     major-litanies]) -- confirmed via `colitur day 2026`. Kept at BOTH Low
     Mass (RG 111(c), one privileged) and Sung Mass (RG 111(a), the same
     single privileged commemoration). *)
  Alcotest.(check (list string)) "RG 111(a): the one privileged commemoration survives at Sung Mass"
    [ "major-litanies" ] (sung_slugs 2026 4 25)

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(f): All Souls' Day (transferred), a Requiem, no Creed" `Quick
        test_476f_all_souls_no_creed;
      Alcotest.test_case "476(f)/RG 23(b): Good Friday, black, no Creed" `Quick
        test_476f_good_friday_no_creed;
      Alcotest.test_case "Colour.Black population is exactly two (data + code)" `Quick
        test_colour_black_population_is_exactly_two;
      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;
      Alcotest.test_case "RG 24/25: no ferial-origin office carries Lord/Bvm or an apostle slug" `Quick
        test_ferial_origin_never_carries_lord_bvm_or_apostle_slug;
      Alcotest.test_case "237(a): Easter Sunday" `Quick test_237a_easter_sunday;
      Alcotest.test_case "237(a): Low Sunday" `Quick test_237a_low_sunday;
      Alcotest.test_case "237(a): Pentecost Sunday" `Quick test_237a_pentecost;
      Alcotest.test_case "237(f): Vigil of the Ascension" `Quick test_237f_ascension_vigil;
      Alcotest.test_case "237(f): Vigil of Pentecost" `Quick test_237f_pentecost_vigil;
      Alcotest.test_case "237(d): St John within the Nativity octave" `Quick
        test_237d_nativity_octave_saint;
      Alcotest.test_case "237(d): Easter Tuesday" `Quick test_237d_easter_week_feria;
      Alcotest.test_case "238(b): Vigil of St Lawrence" `Quick test_238b_omissible_vigil;
      Alcotest.test_case "Nativity Vigil excluded by structural inference, not literal 238(b)" `Quick
        test_nativity_vigil_excluded_by_inference;
      Alcotest.test_case "238(c)/RG23: Ash Wednesday" `Quick test_238c_ash_wednesday;
      Alcotest.test_case "238(c)/RG23: Good Friday" `Quick test_238c_good_friday;
      Alcotest.test_case "237(g): BVM Saturday Office" `Quick test_237g_bvm_saturday;
      Alcotest.test_case "237(e): 2 January, a Christmastide feria" `Quick test_237e_christmastide_feria;
      Alcotest.test_case "237(b): an ordinary Class2 Sunday" `Quick test_237b_ordinary_class2_sunday;
      Alcotest.test_case "237(b)'s own exception: Septuagesima Sunday" `Quick test_237b_septuagesima_exception;
      Alcotest.test_case "237(b)'s own exception: Sexagesima Sunday" `Quick test_237b_sexagesima_exception;
      Alcotest.test_case "237(c): a sanctoral feast (Lawrence)" `Quick test_237c_sanctoral_feast;
      Alcotest.test_case "Passion Sunday is not a festum for 237(c)" `Quick test_passion_sunday_not_a_festum;
      Alcotest.test_case "Palm Sunday is not a festum for 237(c)" `Quick test_palm_sunday_not_a_festum;
      Alcotest.test_case "238(d): All Souls' Day (transferred)" `Quick test_238d_all_souls;
      Alcotest.test_case "431(c): Holy Thursday" `Quick test_431c_holy_thursday;
      Alcotest.test_case "431(c): the Easter Vigil Mass" `Quick test_431c_easter_vigil;
      Alcotest.test_case "432(b): Advent I Sunday, violet" `Quick test_432b_violet_sunday;
      Alcotest.test_case "432(d): All Souls' Day (transferred)" `Quick test_432d_all_souls;
      Alcotest.test_case "431(a): mirrors [te_deum]=true" `Quick test_431a_mirrors_te_deum_true;
      Alcotest.test_case "432(a): mirrors [te_deum]=false" `Quick test_432a_mirrors_te_deum_false;
      Alcotest.test_case "Rogation Monday: white -> Gloria said (2028)" `Quick test_rogation_monday_gloria_2028;
      Alcotest.test_case "Rogation Tuesday: white -> Gloria said (2028)" `Quick test_rogation_tuesday_gloria_2028;
      Alcotest.test_case "domain sanity: every violet day has [gloria]=false (sample)" `Quick
        test_domain_violet_implies_no_gloria_sample;
      Alcotest.test_case "domain sanity: every Requiem day has [gloria]=false (sample)" `Quick
        test_domain_requiem_implies_no_gloria_sample;
      Alcotest.test_case "domain sweep 1583..9999: violet/Requiem invariants, committed not sampled"
        `Slow test_exhaustive_gloria_domain_sweep;
      Alcotest.test_case "RG 111(a): an ordinary-only Low-Mass set is empty at Sung Mass" `Quick
        test_rg111a_ordinary_only_dropped;
      Alcotest.test_case "RG 111(a): a privileged commemoration survives at Sung Mass" `Quick
        test_rg111a_privileged_kept;
      Alcotest.test_case "preface: Good Friday, no Mass" `Quick test_preface_good_friday_no_mass;
      Alcotest.test_case "RG 499: All Souls' Day (transferred)" `Quick test_preface_499_all_souls;
      Alcotest.test_case "RG 487(a): the Exaltation of the Holy Cross" `Quick test_preface_487a_exaltation;
      Alcotest.test_case "RG 487(a): the Most Precious Blood" `Quick test_preface_487a_precious_blood;
      Alcotest.test_case "RG 491: the Sacred Heart" `Quick test_preface_491_sacred_heart;
      Alcotest.test_case "RG 492: Christ the King" `Quick test_preface_492_christ_the_king;
      Alcotest.test_case "RG 490(a): Ascension Day" `Quick test_preface_490a_ascension_day;
      Alcotest.test_case "RG 494(a): Trinity Sunday" `Quick test_preface_494a_trinity_sunday;
      Alcotest.test_case "RG 496: St Joseph, Spouse of the BVM" `Quick test_preface_496_joseph_spouse;
      Alcotest.test_case "RG 496: St Joseph the Workman" `Quick test_preface_496_joseph_workman;
      Alcotest.test_case "RG 495: the Assumption (marian_slugs)" `Quick test_preface_495_assumption;
      Alcotest.test_case "RG 495: the BVM Saturday Office (subject=Bvm)" `Quick
        test_preface_495_bvm_saturday_office;
      Alcotest.test_case "RG 484(a): the Purification" `Quick test_preface_484a_purification;
      Alcotest.test_case "RG 484(a): Christmas Day itself" `Quick test_preface_484_nativity_day;
      Alcotest.test_case "RG 484(b): St Stephen, inside the Nativity octave" `Quick
        test_preface_484_stephen_in_octave;
      Alcotest.test_case "RG 484(a): 1 January, the Octave Day" `Quick test_preface_484_jan1_octave_day;
      Alcotest.test_case "RG 484(b): 3 January, the extra de-Tempore days" `Quick test_preface_484b_jan2to5;
      Alcotest.test_case "RG 484(b): St John the Evangelist, inside the Nativity octave" `Quick
        test_preface_484b_overrides_apostle_in_octave;
      Alcotest.test_case "RG 497: St Barnabas" `Quick test_preface_497_barnabas;
      Alcotest.test_case "RG 497: Sts Peter & Paul" `Quick test_preface_497_peter_paul;
      Alcotest.test_case "RG 497: Sts Philip & James, inside the Easter window" `Quick
        test_preface_497_philip_james_inside_easter_window;
      Alcotest.test_case "RG 485(a): Epiphany Day" `Quick test_preface_485a_epiphany_day;
      Alcotest.test_case "RG 485(a): the Commemoration of the Baptism of the Lord" `Quick
        test_preface_485a_baptism_commemoration;
      Alcotest.test_case "RG 485(b): Holy Family Sunday, inside the 7-13 January window" `Quick
        test_preface_485b_holy_family_sunday;
      Alcotest.test_case "RG 486(a): Ash Wednesday" `Quick test_preface_486_ash_wednesday;
      Alcotest.test_case "RG 486: the Saturday before Passion Sunday I" `Quick
        test_preface_486_lent_saturday_boundary;
      Alcotest.test_case "RG 487: Passion Sunday I" `Quick test_preface_487_passion_sunday;
      Alcotest.test_case "RG 487: Palm Sunday" `Quick test_preface_487_palm_sunday;
      Alcotest.test_case "RG 487: Holy Thursday" `Quick test_preface_487_holy_thursday;
      Alcotest.test_case "RG 489(a): the Easter Vigil Mass" `Quick test_preface_489_easter_vigil_mass;
      Alcotest.test_case "RG 489(a): Easter Sunday" `Quick test_preface_489_easter_sunday;
      Alcotest.test_case "RG 489(b): the vigil of the Ascension" `Quick test_preface_489_ascension_vigil;
      Alcotest.test_case "RG 490(b): the Friday after Ascension" `Quick test_preface_490b_post_ascension_feria;
      Alcotest.test_case "RG 490(b): the Sunday after Ascension" `Quick
        test_preface_490b_sunday_after_ascension;
      Alcotest.test_case "RG 493(a): the vigil of Pentecost" `Quick test_preface_493_pentecost_vigil;
      Alcotest.test_case "RG 493(a): Pentecost Sunday" `Quick test_preface_493_pentecost_sunday;
      Alcotest.test_case "RG 493(a): the Ember Saturday within the Pentecost octave" `Quick
        test_preface_493_ember_saturday;
      Alcotest.test_case "RG 494(b): Advent I Sunday" `Quick test_preface_494b_advent_sunday;
      Alcotest.test_case "RG 494(b): an ordinary Time-after-Pentecost Sunday" `Quick
        test_preface_494b_ordinary_class2_sunday;
      Alcotest.test_case "RG 494(b): All Saints' Day, Class1, still Trinity" `Quick
        test_preface_494b_all_saints_class1_sunday;
      Alcotest.test_case "RG 498: Corpus Christi (no proper of its own)" `Quick test_preface_498_corpus_christi;
      Alcotest.test_case "RG 498: a plain sanctoral saint (Lawrence)" `Quick test_preface_498_plain_saint;
      Alcotest.test_case "preface domain sanity: Christmastide/Paschaltide/Lent window invariants (sample)"
        `Quick test_preface_season_invariants_sample;
      Alcotest.test_case
        "preface domain sweep 1583..9999: window invariants, committed not sampled" `Slow
        test_exhaustive_preface_season_domain_sweep ] )