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
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
|
(* RG 91's Table of Precedence, transcribed by Rite_ef.Precedence_ef.band.
Table-driven, one row (hence one Alcotest.test_case) per RG 91 entry, so a
misplaced or missing entry names itself in the failure output instead of
failing anonymously (docs/research/rules-register.md §4). Each row's date
is checked against the register to make sure it is not ALSO an instance of
some other entry at the same band (the vacuous-test trap this project has
caught before -- see the Advent-Ember-day note on entry 18 below). *)
module P = Colitur_kernel.Precedence
module Cel = Colitur_kernel.Celebration
module S = Colitur_kernel.Slug
module Col = Colitur_kernel.Colour
module D = Colitur_kernel.Date
module Sub = Colitur_kernel.Subject
module Comp = Colitur_kernel.Computus
module T = Rite_ef.Temporal_ef
module V = Rite_ef.Vocab_ef
module PE = Rite_ef.Precedence_ef
let mk y m dd = match D.make ~year:y ~month:m ~day:dd with Ok t -> t | Error e -> failwith e
(* [T.season] is the same function Calendar itself would use to build a
context, so a row's [season]/[weekday] are exactly what the real engine
would compute for that date, not a hand-picked value that might not
actually occur together with it. *)
let ctx date = { P.date; season = T.season date; weekday = D.weekday date }
let cand ?(origin = P.Temporal) ?(rank = V.Class1) ?(status = Cel.Feast) ?(subject = Sub.Temporal)
?(layer = "temporal") slug =
{ P.cel = Cel.make ~slug:(S.of_string_exn slug) ~rank ~status ~colour:Col.White ~subject ~layer ();
origin }
(* A candidate built from [Temporal_ef.temporal]'s own real output, not a
hand-typed slug -- review finding 3: [band]'s Ember/vigil detection reads
temporal_ef.ml's slug conventions, and a row that also hand-types the same
literal proves nothing if that convention ever drifts (both sides would
drift together, silently). Rows built with this instead fail loudly on
such a drift, because they source the slug from the same place [band]
itself is implicitly trusting. *)
let of_temporal date =
let day = T.temporal date in
{ P.cel = day.Colitur_kernel.Temporal.office; origin = P.Temporal }
(* Every Easter-relative date below is anchored to this single computed
Easter rather than a hand-typed calendar date, so an arithmetic slip in a
test date cannot silently pass by accident. *)
let easter = Comp.gregorian_easter 2026
let off n = D.add_days easter n
(* (description, date, candidate, expected RG 91 entry). *)
(* CORRECTED, fix round 1 (coordinator finding 1): every expected [int]
below is now the real RG 91 entry number TIMES TEN (entry 1 -> 10, entry
28 -> 280), matching [PE.band]'s own rescale -- see precedence_ef.ml's
own [entry_14_fixed_band] comment for why the bare 1..28 scale was a real
ordering bug, not a style choice. The description strings still say the
bare entry number ("14 Feast of the Lord...") -- that names the RG 91
ROW, not the band integer, and is unaffected by the scale. *)
let cases =
[ (* Entry 1 -- RG 91 entry 1 (§4): Nativity, Easter Sunday, Pentecost Sunday. *)
("1 Nativity", mk 2026 12 25, cand "ef-nativity", 10);
("1 Easter Sunday", off 0, cand "ef-easter-sunday", 10);
("1 Pentecost Sunday", off 49, cand "ef-pentecost", 10);
(* Entry 2 -- RG 91 entry 2 (§4): Sacred Triduum. Thu-Sat of Holy Week,
NOT entry 7 (which stops at Wednesday -- see entry 7 below). *)
("2 Holy Thursday", off (-3), cand "ef-holy-thursday", 20);
("2 Good Friday", off (-2), cand "ef-good-friday", 20);
("2 Holy Saturday", off (-1), cand "ef-holy-saturday", 20);
(* Entry 3 -- RG 91 entry 3 (§4). *)
("3 Epiphany", mk 2026 1 6, cand "ef-epiphany", 30);
("3 Ascension", off 39, cand "ef-ascension", 30);
("3 Trinity", off 56, cand "ef-trinity", 30);
("3 Corpus Christi", off 60, cand "ef-corpus-christi", 30);
("3 Sacred Heart", off 68, cand "ef-sacred-heart", 30);
("3 Christ the King", T.christ_the_king 2026, cand "ef-christ-the-king", 30);
(* Entry 4 -- RG 91 entry 4 (§4). Sanctoral-origin: neither feast is part
of temporal_ef's movable cycle. *)
( "4 Immaculate Conception", mk 2026 12 8,
cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer
"ef-immaculate-conception",
40 );
("4 Assumption", mk 2026 8 15, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-assumption", 40);
(* Entry 5 -- RG 91 entry 5 (§4). *)
("5 Nativity Vigil", mk 2026 12 24, cand "ef-nativity-vigil", 50);
("5 Octave day (Circumcision)", mk 2026 1 1, cand "ef-circumcision", 50);
(* Entry 6 -- RG 91 entry 6 (§4). *)
("6 Advent Sunday", T.advent_start 2026, cand "ef-advent-sunday-1", 60);
("6 Lent Sunday", off (-42), cand "ef-lent-sunday-1", 60);
("6 Passion Sunday (I Passiontide)", off (-14), cand "ef-passion-sunday", 60);
("6 Palm Sunday (II Passiontide)", off (-7), cand "ef-palm-sunday", 60);
("6 Low Sunday", off 7, cand "ef-low-sunday", 60);
(* Entry 7 -- RG 91 entry 7 (§4): Ash Wednesday and Mon/Tue/Wed of Holy
Week ONLY -- Thu-Sat are entry 2 above, not this entry. *)
("7 Ash Wednesday", off (-46), cand "ef-ash-wednesday", 70);
("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 70);
("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 70);
("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 70);
(* Entry 8 -- RG 91 entry 8 (§4). 2 Nov 2026 is a Monday (verified
independently below the table), so this row is the plain case. The
register's own qualifying case -- "yields to an occurring Sunday" --
gets its own row and its own end-to-end test after this table (2 Nov
2025 is a real Sunday). *)
("8 All Souls (non-Sunday)", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 80);
(* Entry 8's qualifier: "(yields to an occurring Sunday)". 2 Nov 2025 is
a Sunday, so this must NOT be 80 -- it must lose to entry 15 (151 =
entry 15's own value, 150, + 1, the exact value precedence_ef.ml
documents and justifies at entry 8's branch). The end-to-end
resolve-level proof that the Sunday actually wins the day is
[test_all_souls_yields_to_sunday] below; this row pins the specific
integer [band] returns. *)
("8 All Souls (yields to a Sunday, 2 Nov 2025)", mk 2025 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 151);
(* Entry 9 -- RG 91 entry 9 (§4). *)
("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 90);
(* Entry 10 -- RG 91 entry 10 (§4): both range boundaries, to guard the
off-by-one an inclusive Easter-offset window invites. *)
("10 Easter octave, day+1", off 1, cand "ef-easter-1-mon", 100);
("10 Easter octave, day+6", off 6, cand "ef-easter-1-sat", 100);
("10 Pentecost octave, day+50", off 50, cand "ef-pentecost-1-mon", 100);
("10 Pentecost octave, day+55", off 55, cand "ef-pentecost-1-sat", 100);
(* Entry 11 -- RG 91 entry 11 (§4). *)
( "11 Universal I-class feast", mk 2026 6 29,
cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-ss-peter-paul",
110 );
(* Entry 12 -- RG 91 entry 12 (§4). The one non-base-layer case the brief
asks for explicitly: same date/rank/subject as 11, only the layer
differs, so this row isolates the layer test as the deciding factor. *)
( "12 Proper I-class feast (non-base layer)", mk 2026 6 29,
cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:"diocese-warsaw" "ef-local-patron",
120 );
(* Entry 13 -- RG 91 entry 13 (§4). *)
( "13 Indult I-class feast", mk 2026 6 29,
cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:(PE.indult_prefix ^ "local-grant")
"ef-indult-feast-1",
130 );
(* fix round 1 (coordinator finding 1), NEW: a synthetic I-CLASS
candidate landing INSIDE Holy Family's own 7-13 January window --
the exact counter-example that found the original [-14] sentinel's
inversion (Holy Family used to beat every I-class entry outright,
not merely the fixed Baptism it was built to beat). Must still
outrank the movable-14 entry below (110 < entry_14_movable_band),
proving the fix, not merely the absence of the old bug's OWN
witness (13 January). Not live against the shipped universal data
(register §6.2) -- this is exactly the kind of I-class PROPER/
INDULT feast an overlay could add, entries 11-13's own [not
is_temporal] tests notwithstanding. *)
( "11 SYNTHETIC: a universal I-class feast inside Holy Family's own window \
(fix round 1, coordinator finding 1) must still outrank the movable half",
mk 2026 1 11,
cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer
"ef-synthetic-class1-probe",
110 );
(* Entry 14 -- RG 91 entry 14 (§4), deliberately UNQUALIFIED (contrast
entry 16, which explicitly says "not of the Lord"). *)
( "14 Feast of the Lord, II class", mk 2026 7 1,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"ef-precious-blood",
PE.entry_14_fixed_band );
(* Entry 14, non-base layer: unlike 11-13/16/19/20/23/24, entry 14 draws
no universal/proper/indult line at all, so this must STILL be
[PE.entry_14_fixed_band], not 19 -- the exact restriction review
finding 2 flagged and this row exists to keep from silently coming
back. *)
( "14 Feast of the Lord, II class (non-base layer)", mk 2026 7 2,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:"diocese-warsaw"
"ef-local-feast-of-the-lord",
PE.entry_14_fixed_band );
(* Entry 14, MOVABLE half -- RG 91 entry 14's own text (§4, scan-verified):
"Festa Domini II classis, PRIMUM MOBILIA, DEINDE FIXA" -- movable
FIRST, then fixed. Holy Family (RG 17(b), temporal_ef.ml's own
[holy_family_sunday]) is the one movable Class2 Lord feast this
codebase builds -- sourced from [Temporal_ef.temporal]'s own real
output ([of_temporal], the same review-finding-3 discipline entry
18's own Ember row above uses), not a hand-typed slug, so a rename
or a dropped [~subject:Lord] on temporal_ef.ml's own side fails this
row too, not only test_temporal_ef.ml's. Must be a value STRICTLY
BETTER than the fixed half ([PE.entry_14_fixed_band] immediately
above, not merely different) -- the two are structurally disjoint by
[is_temporal] alone so they can never literally collide on any one
candidate, but {!compare_by}'s own kernel-level tie-break
(Slug.compare, deliberately NOT rubric-authoritative -- the exact
anti-pattern RG 113's own fix already corrected once, this file's
own header) is what would decide between them if they returned the
SAME band on the one date they can coincide (13 January, when it is
itself Holy Family's date) -- "commemoration-of-the-baptism-of-the-
lord" sorts before "ef-time-after-epiphany-sunday-1" alphabetically,
backwards from RG 91's own stated order. CORRECTED, fix round 1
(coordinator finding 1): [PE.entry_14_movable_band] used to be a
negative sentinel, which beat every I-class entry too, not merely
entry 14's own fixed half -- see the SYNTHETIC I-class probe row
above, and [PE.entry_14_movable_band]'s own comment in
precedence_ef.ml, for the fix and the counter-example that found it. *)
( "14 Feast of the Lord, II class, MOVABLE half (Holy Family, RG 17(b))",
T.holy_family_sunday 2026, of_temporal (T.holy_family_sunday 2026), PE.entry_14_movable_band );
(* Entry 15 -- RG 91 entry 15 (§4): an ordinary Sunday not named at entry 6
-- Septuagesima is II class (RG 11-12 names only Advent/Lent/
Passiontide/Easter/Low/Pentecost as I class). *)
("15 II-class Sunday (Septuagesima)", off (-63), cand ~rank:V.Class2 "ef-septuagesima-sunday", 150);
(* Entry 16 -- RG 91 entry 16 (§4). *)
( "16 Universal II-class feast, not of the Lord", mk 2026 1 20,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:PE.universal_layer
"ef-some-saint",
160 );
(* Entry 17 -- RG 91 entry 17 (§4): days WITHIN the Nativity octave (26-28
Dec are Stephen/John/Innocents -- sanctoral, not this entry; 1 Jan is
entry 5's Octave DAY, not this entry either). *)
("17 Nativity octave, 29 Dec", mk 2026 12 29, cand ~rank:V.Class2 "ef-nativity-octave-day-5", 170);
("17 Nativity octave, 31 Dec", mk 2026 12 31, cand ~rank:V.Class2 "ef-nativity-octave-day-7", 170);
(* Entry 18 -- RG 91 entry 18 (§4): Advent 17-23 Dec ferias AND the
Ember days of Advent/Lent/September share this one entry. The second
row is deliberately a Lent date (season Lent, NOT Advent) to prove the
Ember-slug path fires on its own, not merely because it also happens
to fall in the Dec 17-23 window -- the exact trap the brief warns
about, worked the other way round: this Ember day must NOT be
mistaken for an ordinary entry-22 Lent feria either. *)
("18 Advent 17-23 Dec feria", mk 2026 12 21, cand ~rank:V.Class2 "ef-advent-4-mon", 180);
(* Sourced from Temporal_ef.temporal's own output (see [of_temporal])
rather than a hand-typed "ef-lent-ember-wed" -- closes review finding
3's coupling concern for the Ember prefixes specifically. *)
("18 Lent Ember Wednesday (from Temporal_ef.temporal)", off (-39), of_temporal (off (-39)), 180);
(* Entry 19 -- RG 91 entry 19 (§4). *)
( "19 Proper II-class feast", mk 2026 1 20,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:"diocese-warsaw"
"ef-local-saint-2",
190 );
(* Entry 20 -- RG 91 entry 20 (§4). *)
( "20 Indult II-class feast", mk 2026 1 20,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint
~layer:(PE.indult_prefix ^ "local-grant-2") "ef-indult-feast-2",
200 );
(* Entry 21 -- RG 91 entry 21 (§4, RG 28-34). Two rows: the Ascension
Vigil is the one II-class vigil temporal_ef already produces today
(temporal-origin); the Assumption Vigil stands in for the
sanctoral-origin case no task has loaded data for yet -- proving
[band] does not gate this entry on [origin] (see precedence_ef.ml's
file comment). *)
(* Sourced from Temporal_ef.temporal's own output (see [of_temporal])
rather than a hand-typed "ef-ascension-vigil" -- closes review finding
3's coupling concern for [vigil_suffix]. *)
("21 Ascension Vigil (from Temporal_ef.temporal)", off 38, of_temporal (off 38), 210);
( "21 Assumption Vigil (sanctoral-origin)", mk 2026 8 14,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-assumption-vigil",
210 );
(* Also review finding 3 / "worth doing": a UNIVERSAL-layer Class2 vigil
whose subject is the Lord must still be 21, not 14 -- pins entry 14's
[not is_vigil] guard even after finding 2 dropped its layer test. *)
( "21 Universal II-class vigil of the Lord", mk 2026 6 23,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"ef-precious-blood-vigil",
210 );
(* Entry 22 -- RG 91 entry 22 (§4) (corrected: ends at Palm Sunday, not
Passion Sunday). Both a Lent and a Passiontide feria, clear of Ash
Wednesday, Holy Week and the Ember days. *)
("22 Lent feria", off (-41), cand ~rank:V.Class3 "ef-lent-1-mon", 220);
("22 Passiontide feria", off (-12), cand ~rank:V.Class3 "ef-passiontide-1-tue", 220);
(* Entry 23 -- RG 91 entry 23 (§4). NOTE the table's own order here is the
REVERSE of 11/12 and 14/16/19/20 above: entry 23 (particular
calendars) is numbered BELOW entry 24 (universal), so a proper
III-class feast outranks a universal one -- transcribed as the
register states it, not "corrected" to match the other classes. *)
( "23 Proper III-class feast (non-base layer)", mk 2026 6 30,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-saint-3",
230 );
(* Entry 24 -- RG 91 entry 24 (§4). *)
( "24 Universal III-class feast", mk 2026 6 30,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3",
240 );
(* Entry 25 -- RG 91 entry 25 (§4). *)
("25 Advent feria to 16 Dec", mk 2026 12 1, cand ~rank:V.Class3 "ef-advent-1-tue", 250);
(* Entry 26 -- RG 91 entry 26 (§4). *)
( "26 III-class vigil", mk 2026 8 9,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil",
260 );
(* Also worth doing: a NON-universal-layer Class3 vigil must still be 26,
not 23 -- pins entry 23's [not is_vigil] guard. *)
( "26 III-class vigil (non-base layer)", mk 2026 8 10,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-patron-vigil",
260 );
(* Task 11, issue (a): the sanctoral bootstrap (data/ef/sanctoral.sexp)
names its vigils with lectio's OWN "vigil-of-X" PREFIX convention, not
[PE.vigil_suffix]'s "-vigil" SUFFIX every row above uses -- exactly
the mismatch Task 7's review predicted. These two rows use the real
bootstrapped slugs verbatim (data/ef/sanctoral.sexp: 28 Jun, 9 Aug),
proving [band] recognises the prefix convention too: without it, both
would misfile at 160/240 (an ordinary feast of the same rank) instead
of 210/260. *)
( "21 II-class vigil via the sanctoral data's own \"vigil-of-X\" prefix",
mk 2026 6 28,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "vigil-of-sts-peter-paul",
210 );
( "26 III-class vigil via the sanctoral data's own \"vigil-of-X\" prefix",
mk 2026 8 9,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "vigil-of-st-lawrence",
260 );
(* Entry 27 -- RG 91 entry 27 (§4): an otherwise-unoccupied IV-class
Saturday. ef-bvm-saturday task: sourced from [Temporal_ef.temporal]'s
own real output (see [of_temporal], the same review-finding-3
discipline entry 18's/21's own rows above use), not a hand-typed
slug/rank pair -- this row used to be the one place in this whole
suite where [band]'s entry-27 branch was exercised by a value nothing
in [Temporal_ef.temporal] could actually produce (the office itself
was unbuilt): [band] read [rank = Class4 && weekday = Sat]
correctly, but nothing proved [temporal] ever handed it a candidate
shaped that way for the RIGHT reason. Now it does: [of_temporal (off
62)] is 6 June 2026, a real otherwise-contested Time-after-Pentecost
Saturday (Norbert, Class3, wins the DAY outright in the real
pipeline -- irrelevant here, since this row tests [band] on the
TEMPORAL CANDIDATE alone, which [Temporal_ef.temporal] builds as the
BVM Saturday office regardless of what wins). *)
( "27 Office of the BVM on Saturday (from Temporal_ef.temporal)", off 62, of_temporal (off 62), 270 );
(* Entry 28 -- RG 91 entry 28 (§4): the unqualified IV-class catch-all. *)
("28 IV-class feria", off 65, cand ~rank:V.Class4 "ef-time-after-pentecost-1-tue", 280);
(* Not an RG 91 row at all: a I-class candidate marked as a vigil, which
is not the Nativity or Pentecost (entries 5/9, the only I-class
vigils the table names) and so has no entry to fall into. Proves the
documented fallback -- not entry 11/12/13, which the [not is_vigil]
guard exists specifically to keep this out of. *)
( "unclassified: I-class vigil outside Nativity/Pentecost", mk 2026 3 10,
cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-mystery-vigil",
PE.unclassified );
(* Also worth doing: a temporal-origin Class1 candidate on a date none of
entries 1/2/3/5/6/7/9/10 name. 15 Jul 2026 is a Wednesday, off=101
from Easter -- clear of every Easter-relative window this module
checks, and not one of the fixed dates either. Without the
[not is_temporal] guard on entries 11-13, this would wrongly reach 12
(its default layer, "temporal", is not [universal_layer] and does not
carry [indult_prefix], so it reads as "proper" by the layer test
alone -- precisely the bug the guard exists to prevent; see the
[not is_temporal] guard's role in the entry-25 mutation test recorded
in the task report). *)
("unclassified: I-class temporal candidate on an unnamed date", mk 2026 7 15, cand "ef-unnamed-day", PE.unclassified);
(* RG 91's own vigil list (§4, "Vigils / octaves / Rogations / Sunday classes") stops at III class --
there is no IV-class vigil for entry 28's ferial catch-all to absorb. *)
( "unclassified: IV-class candidate marked as a vigil", mk 2026 6 20,
cand ~rank:V.Class4 "ef-second-mystery-vigil", PE.unclassified );
(* Task B fix round 1 (coordinator finding 1): RG 91's table enumerates
only "dies liturgici" -- a [Cel.Commemoration_only] candidate is
never a row in the table at all, regardless of what rank it would
otherwise band to. [Class1]/universal-layer is deliberate: absent
the status guard this would band to entry 11, the FIRST, highest-
priority real entry any sanctoral candidate can reach, so this
proves the guard is checked BEFORE and overrides every rank-keyed
branch, not only the low-priority ones an accidental placement
might happen to catch. Real-world witness: "maurice-and-companions-
martyrs" (data/ef/sanctoral.sexp, 22 September, Class3,
Commemoration_only) used to band to entry 24 alongside the genuine
Class3 feast "thomas-of-villanova" -- test_oracle.ml's own former
M17 allow-list entry, deleted once this fix made the divergence it
named disappear. *)
( "unclassified: a Commemoration_only candidate has no row in RG91's \
table at all, even at Class1", mk 2026 5 20,
cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~layer:PE.universal_layer
"ef-some-commemoration-only",
PE.unclassified )
]
(* Review finding 1's end-to-end proof: on a real Sunday landing on 2
November, [Precedence.resolve] -- not just [band] in isolation -- observes
the Sunday, not All Souls. This exercises the exact mechanism the finding
named ("resolve observes the lowest band, so whenever 2 November falls on
a Sunday, All Souls wins and the Sunday loses"), rather than only the
integer [band] returns for the standalone row above. [disposition] and
[admit] are stubs -- only [observed] is under test here. *)
let test_all_souls_yields_to_sunday () =
let date = mk 2025 11 2 in
let day_ctx = ctx date in
let sunday =
{ P.cel =
Cel.make ~slug:(S.of_string_exn "ef-time-after-pentecost-sunday-x") ~rank:V.Class2
~colour:Col.Green ~subject:Sub.Temporal ~layer:"temporal" ();
origin = P.Temporal }
in
let all_souls = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls" in
let rules =
{ P.band = (fun c cd -> PE.band c cd);
disposition = (fun ~winner:_ ~loser:_ -> P.Omit);
admit = (fun ~observed:_ ~temporal:_ cs -> List.map (fun (c, p, (_ : int)) -> (c, p)) cs) }
in
let resolution = P.resolve rules day_ctx ~temporal:sunday ~sanctoral:[ all_souls ] in
Alcotest.(check string) "the Sunday is observed, not All Souls"
"ef-time-after-pentecost-sunday-x"
(S.to_string resolution.P.observed.P.cel.Cel.slug)
(* Task 8: [disposition] -- what happens to the day's LOSING candidate (RG
92-95, 33, 94; §4). Table-driven like [band]'s
own [cases] above, one row per rule, each checked against a description of
which register clause it pins. [disposition] takes no context (see
precedence.mli's [rules.disposition]), so "is the winner a Sunday" is read
off the winner's own slug the same way [band] itself reads "is this a
vigil" off the loser's -- see precedence_ef.ml's [sunday_marker]. *)
let string_of_disposition = function
| P.Omit -> "Omit"
| P.Commemorate P.Privileged -> "Commemorate(Privileged)"
| P.Commemorate P.Ordinary -> "Commemorate(Ordinary)"
| P.Transfer -> "Transfer"
| P.Repose -> "Repose"
(* A II-class ordinary Sunday, built the same way [test_all_souls_yields_to_sunday]
builds its Sunday -- a hand-typed slug matching temporal_ef.ml's own
"ef-<season>-sunday-<n>" convention, since [disposition] only ever reads
this string, never the real computed date. *)
let an_ordinary_sunday =
cand ~rank:V.Class2 "ef-time-after-pentecost-sunday-11"
let disposition_cases =
[ (* RG 95 -- §4, "Occurrence": only I-class feasts transfer; a
II-class feast loses to a I-class day and is COMMEMORATED, not
transferred. Paired with the next row (a I-class loser, same shape of
winner) so the discriminating factor is provably the LOSER's own
rank, not the winner's -- the brief's explicit "one without the other
proves nothing" pairing. *)
( "RG95 II-class feast loses to I-class day -> Commemorate",
cand "ef-nativity",
cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-saint",
"Commemorate(Ordinary)" );
( "RG95 I-class feast loses to a higher I-class day -> Transfer",
cand "ef-nativity",
cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-local-i-class-feast",
"Transfer" );
(* Fix round 1 (post-Task-9 review): RG 95 (§4, "Occurrence" and "Transfer/translation")
restricts the right of translation to I-class FEASTS -- RG 91's own
table lists Sundays as a separate row (entry 6)
from feasts (entries 11-13) -- so an impeded I-class
Sunday must NOT transfer, unlike the plain I-class feast row above:
same [Class1] rank, same kind of winner, the ONLY difference is that
this loser's slug carries [PE.sunday_marker]. RG 109(a) (§4)
confirms this from the other direction: "of a Sunday" is a
privileged commemoration category, which presupposes an impeded
Sunday stays put rather than moving to another day the way a feast
does. Sourced from [Temporal_ef.temporal]'s own real output (Advent I
Sunday 2026, Class1, "ef-advent-sunday-1"), the same coupling-safety
reason [of_temporal]'s other callers use it -- this is also a
realistic shape: 8 December falls on an Advent Sunday in 2024, 2030
and 2041 (Immaculate Conception, RG 91 entry 4, outranking entry 6),
and 24 December falls on Advent IV in 2023, 2028, 2034 and 2045 (the
Nativity Vigil, also entry 5 outranking entry 6). *)
( "RG95/RG109(a): an impeded I-class SUNDAY does NOT transfer -- it is \
Commemorated and Privileged",
cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-immaculate-conception",
of_temporal (T.advent_start 2026),
"Commemorate(Privileged)" );
(* RG 33 -- §4, "Vigils / octaves / Rogations / Sunday classes": a II- or
III-class vigil impeded by any Sunday or a I-class feast is entirely
OMITTED, not commemorated. The
vigil is sourced from [Temporal_ef.temporal]'s own real output (as
[of_temporal]'s existing callers above do), not a hand-typed
"ef-ascension-vigil", so a drift in temporal_ef's vigil-slug
convention cannot silently defeat this row the way a duplicated
literal could. This is the row the brief singles out as most likely
to pass vacuously if the fallback below happened to already be
[Omit] -- it is not: the fallback is [Commemorate] (see the next two
rows), so this genuinely exercises RG 33's own branch. *)
( "RG33 II-class vigil loses to an ordinary Sunday -> Omit",
an_ordinary_sunday,
of_temporal (off 38) (* Ascension Vigil *),
"Omit" );
( "RG33 II-class vigil loses to a I-class feast (non-Sunday) -> Omit",
cand "ef-immaculate-conception",
of_temporal (off 38),
"Omit" );
(* RG 33's own boundary, proved from both sides so the rule is shown to
gate on the WINNER too, not "any vigil is always omitted": winner is
neither a Sunday nor I class, so RG 33's omission does not fire; a
vigil, per RG 31's own text ("si impediuntur, commemorantur"), is
ALWAYS commemorated once RG 33 does not omit it outright, regardless
of RG 109's closed list.
CORRECTED comment (final fix wave, item 2b): this row's own comment
used to claim it "proves [the RG26 Class4-Omit branch's] own [not
(is_vigil ...)] guard" -- that claim does NOT survive under the
current [rank = Class4] gate (it may have been true under an earlier
predicate). The vigil sourced here ({!of_temporal}, the real
Ascension Vigil) is [Class2], not [Class4], so the RG26 branch's own
[rank = Class4] test already excludes it on rank alone, with or
without the [not (is_vigil ...)] guard -- verified: deleting that
guard entirely leaves 257/257 green. This row still genuinely proves
RG 31's own "if impeded, commemorated" vigil mandate (a vigil that
RG 33 does not omit outright is commemorated, not omitted by some
OTHER mechanism), which is real and worth keeping; it just does not
discriminate the [not (is_vigil ...)] guard specifically. *)
( "RG33 boundary: vigil loses to an ordinary (non-Sunday, non-I-class) \
II-class day -> Commemorate, NOT Omit (RG31's own vigil mandate)",
cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-other-feast",
of_temporal (off 38),
"Commemorate(Ordinary)" );
(* The GENUINE witness for the RG26 branch's [not (is_vigil ...)] guard
(final fix wave, item 2b): no REAL vigil this engine constructs can
exercise it, since RG 91 has no IV-class vigil at all (this file's own
entry-27/28 comments) -- every real vigil is Class2 or Class3, already
excluded by [rank = Class4] alone, the exact shape the row above is.
This row is therefore deliberately SYNTHETIC: a temporal-origin loser,
[Class4], whose slug ALSO carries {!PE.vigil_suffix} -- a shape RG 91
itself does not describe, but one {!Precedence.resolve} can still be
asked to construct (see precedence_ef.ml's own comment on this guard).
With the guard present, [is_vigil] excludes it from the RG26 branch,
so it falls through to the ordinary [Commemorate] catch-all (RG 31's
"if impeded, commemorated" mandate, the same rule the row above
exercises for a realistic Class2 vigil). Deleting [not (is_vigil ...)]
from that branch turns this row -- and only this row -- red: the
branch would then fire on [rank = Class4] alone and return [Omit]. *)
( "SYNTHETIC (final fix wave, item 2b): a temporal-origin Class4 loser \
ALSO vigil-shaped is Commemorate (RG31's vigil mandate via the \
not-is_vigil guard), not Omit",
an_ordinary_sunday,
cand ~rank:V.Class4 "ef-synthetic-class4-vigil",
"Commemorate(Ordinary)" );
(* CORRECTED 2026-08-12 (Task 16, primary-source-verified): the register
previously (mis-)transcribed RG 33 as covering only I/II-class
vigils, so this row's own title used to read "outside RG33's
I/II-class scope" and expect Commemorate. The primary text ("Vigilia
II AUT III classis penitus omittitur...") covers II OR III class --
St Lawrence's vigil (III class, RG 32) falling on ANY Sunday ("in
dominica quavis") is entirely omitted, exactly like a II-class vigil.
See {!PE.is_omissible_vigil}'s own comment for the full primary text
and the register correction. *)
( "RG33 (corrected): a III-class vigil loses to a Sunday -> Omit, not \
Commemorate",
an_ordinary_sunday,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil",
"Omit" );
(* Task 11, issue (a): [disposition]'s own [is_vigil] check (the RG 33
omission test) is a SEPARATE call site from [band]'s -- both read the
same private [is_vigil], but each needed its own witness, since a fix
to one call site could in principle miss the other. Real bootstrapped
slug (data/ef/sanctoral.sexp's "vigil-of-the-assumption", 14 Aug),
not a hand-typed one, for the same coupling-safety reason [of_temporal]
rows use real data elsewhere in this file. Before the fix this vigil
was invisible to [is_vigil] entirely, so it would have fallen through
to the ordinary Commemorate branch below instead of Omit -- the exact
failure the task brief describes. *)
( "RG33 (prefix convention): a \"vigil-of-X\"-named II-class vigil loses \
to an ordinary Sunday -> Omit",
an_ordinary_sunday,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "vigil-of-the-assumption",
"Omit" );
(* Brief: a Commemoration_only loser is ALWAYS Commemorate -- checked
here with a loser that ALSO carries a Class1 rank and a vigil-suffixed
slug losing to a Sunday, so this row only passes if the
Commemoration_only check is checked BEFORE both RG 33's omission and
RG 95's transfer, not after. Its expected privilege is [Privileged],
not [Ordinary]: this loser's [rank] is [Class1] (the default [cand]
leaves unless overridden, deliberately kept here for the
branch-order proof above), and RG 109(b) (§4, "of
a I-class day") makes any [Class1] commemoration privileged
regardless of how it reached [Commemorate] -- Task 8's placeholder
[interim_privilege] used to hide this (always [Ordinary]); Task 9's
real [privilege_of] does not. This row is also this suite's ONLY
witness for RG 109(b): a plain [Feast]-status [Class1] loser never
reaches [Commemorate] at all (RG 95 sends it to [Transfer] instead,
see the row above), so [Commemoration_only] is the only shape that
can exercise it here (see the task report). *)
( "Commemoration_only loser is always Commemorate, even if I-class and \
vigil-shaped, even losing to a Sunday -- and RG109(b) makes it \
privileged",
an_ordinary_sunday,
cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~layer:PE.universal_layer
"ef-suppressed-vigil",
"Commemorate(Privileged)" );
(* RG 80 (ef-major-litanies task, NEW) -- {!PE.major_litanies_slug}'s
own citation (precedence_ef.ml, above [privilege_of]) has the full
text. The row immediately above proves "Commemoration_only is
ALWAYS Commemorate" as a GENERAL rule; these three rows prove
[PE.major_litanies_slug] is the ONE deliberate, cited EXCEPTION to
it, and prove the exception is gated on the WINNER's own slug, not
merely on the loser being this particular Commemoration_only
candidate. *)
( "RG80 first trigger: the Litanies lose to Easter Sunday itself -> \
Transfer, not Commemorate (25 April IS Easter Sunday this year)",
cand "ef-easter-sunday",
cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer
PE.major_litanies_slug,
"Transfer" );
( "RG80 second trigger: the Litanies lose to Easter Monday -> \
Transfer (25 April IS Easter Monday this year, i.e. Easter itself \
is 24 April)",
cand "ef-easter-1-monday",
cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer
PE.major_litanies_slug,
"Transfer" );
(* MUTATION-PROOFING the WINNER-slug guard specifically (not merely
"the Litanies can transfer at all"): losing to a THIRD I-class
temporal winner that is neither of RG 80's two named trigger days
must NOT transfer -- RG 80's own condition is exactly "Easter
Sunday or Easter Monday", not "any I-class day", and 25 April can
structurally never actually coincide with, say, the Nativity in
real data (this row is a deliberately synthetic collision, the
same "proves the guard, not merely the absence of its own bug"
shape {!band}'s own Holy-Family-window row uses) -- if
{!PE.disposition}'s own winner-slug test were ever loosened to
"any Class1 winner", this row would wrongly turn [Transfer] too. *)
( "SYNTHETIC: the Litanies losing to an UNRELATED I-class day (not \
Easter Sunday or Monday) is an ordinary Commemoration_only loser \
-- Commemorate(Privileged) via RG109(f), NOT Transfer",
cand "ef-nativity",
cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer
PE.major_litanies_slug,
"Commemorate(Privileged)" );
(* Totality (SANCTORAL side): the lower ranks the RG 33/RG 95/Task-16
branches never touch still reach the RG 95 "commemorated or omitted"
branch's [Commemorate] side, not an unhandled/exceptional case -- RG
111(c)/(d) admit an "ordinary" SAINT commemoration freely, with none
of RG 109's closed-list restriction the temporal branch below has. *)
( "III-class feast loses to a I-class day -> Commemorate",
cand "ef-nativity",
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3",
"Commemorate(Ordinary)" );
(* Task 16 (primary-source-verified: RG 95 + RG 109's closed list + RG
113 -- see this branch's own comment in precedence_ef.ml for the full
three-text argument): an ORDINARY, non-privileged TEMPORAL-cycle
loser -- a bog-standard green-season feria of Time after Pentecost,
none of RG 109(a)-(f) -- has NO standing to be commemorated at all
when impeded; it is entirely omitted, not the "ordinary"
commemoration a losing SAINT would get (contrast the SANCTORAL row
immediately above, same rank, same kind of winner, opposite
[Commemorate]/[Omit] outcome -- the discriminating factor is
[origin], nothing else). Before this fix the engine wrongly
commemorated the losing feria itself here; confirmed wrong against
the missalemeum oracle (Task 16 report): every one of ~190
structurally identical days (an ordinary sanctoral feast impeding an
ordinary temporal feria, 2026-2027) shows zero commemorations in an
independent published EF calendar. *)
( "TASK16: an ORDINARY temporal feria loses to a II-class Sunday -> \
Omit, not Commemorate (RG109's closed list; contrast the sanctoral \
row above)",
an_ordinary_sunday,
cand ~rank:V.Class4 "ef-time-after-pentecost-1-sat",
"Omit" );
(* Totality's other half: a SANCTORAL loser of the exact same rank as
the row above still reaches [Commemorate], proving the branch above
is gated on [origin] and not merely on rank -- without this row nothing
here would distinguish "temporal losers are omitted" from "IV-class
losers are omitted", which would be a much bigger (and wrong) claim. *)
( "TASK16 contrast: a SANCTORAL IV-class loser still reaches \
Commemorate(Ordinary)",
an_ordinary_sunday,
cand ~origin:P.Sanctoral ~rank:V.Class4 ~layer:PE.universal_layer "ef-some-minor-saint",
"Commemorate(Ordinary)" );
(* Final fix wave, item 2(a): the RG 26 gate above is [rank = Class4],
and until this row nothing in the suite discriminated that from the
REFUTED predicate it replaced, [privilege_of loser = Ordinary]
(precedence_ef.ml's own comment on this branch names that exact
predicate as the wrong reading it corrects). The two predicates are
extensionally identical over every candidate this engine actually
constructs -- confirmed by enumerating the whole candidate space over
1583-9999: substituting the old predicate back leaves 257/257 green
and produces byte-identical `colitur day` output for every sampled
year. No REALISTIC row can tell them apart, so this one is
deliberately SYNTHETIC: a temporal-origin loser, [Class3] (not
[Class4]), whose slug matches none of RG 109(a)-(f) -- [privilege_of]
therefore returns [Ordinary] for it, the SAME privilege value as the
Class4 row above. The two gates now diverge: RG 26 ([rank = Class4])
says this loser is NOT a IV-class feria, so it falls through to the
ordinary [Commemorate] branch (RG 24/25's mandate for a II/III-class
feria); the refuted gate ([privilege_of loser = Ordinary]) tests only
the privilege value, which is [Ordinary] here exactly as it is for
the Class4 row, so it would wrongly return [Omit]. Reverting the
branch's condition to [privilege_of loser = Ordinary] turns this row
(and only this row, of the two) red -- see the final-fix-report for
the exact Alcotest failure message this produces and its reversion. *)
( "SYNTHETIC (final fix wave, item 2a): a temporal-origin ORDINARY \
Class3 loser is Commemorate under RG26 (rank=Class4 gate), would be \
Omit under the refuted privilege_of=Ordinary gate",
an_ordinary_sunday,
cand ~rank:V.Class3 "ef-synthetic-ordinary-class3-feria",
"Commemorate(Ordinary)" );
(* RG 16(a) (register §6.0, Caput III "De Dominicis"): "festum Domini I
aut II classis, in dominica II classis occurrens, ... de dominica,
proinde, nulla fit commemoratio" -- a Feast of the Lord, I or II
class, impeding a II-class Sunday, leaves the Sunday with NO
commemoration at all, unlike every other impeded II-class Sunday
(contrast the very first row of this table, and [privilege_cases]'s
own (a): both plain [Class1]/other-sanctoral winners still admit the
Sunday, [Commemorate(Privileged)]). Two positive rows, I and II
class, both real slugs/ranks/subjects this project's own audit
confirmed against the calendarium (this task's own report): the
Transfiguration (6 Aug, II class, "IN TRANSFIGURATIONE D. N. I. C.")
and the Precious Blood (1 Jul, I class, "PRETIOSISSIMI SANGUINIS
D. N. I. C."). *)
( "RG16(a): a Feast of the Lord, II class, impedes a II-class Sunday -> \
Omit, no commemoration at all",
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"transfiguration-of-our-lord",
an_ordinary_sunday,
"Omit" );
( "RG16(a): a Feast of the Lord, I class, impedes a II-class Sunday -> \
Omit, no commemoration at all",
cand ~origin:P.Sanctoral ~rank:V.Class1 ~subject:Sub.Lord ~layer:PE.universal_layer
"precious-blood-of-our-lord-jesus-christ",
an_ordinary_sunday,
"Omit" );
(* Conjunct 1/4, WINNER subject: RG 16(a) names a feast "Domini", not any
I-class feast. {!band} entries 11-13 admit any I-class feast ahead of
a II-class Sunday with no subject test (unlike entry 14, see that
entry's own comment) -- so a real I-class SAINT feast can win the day
exactly as a Lord feast would, and must NOT trigger RG 16(a). Real
slug/rank/subject from data/ef/sanctoral.sexp (24 June, I class,
"NATIVITAS S. IOANNIS BAPTISTAE", subject Saint): 24 June falls on a
Time-after-Pentecost Sunday in real years (e.g. 2029), so this is a
live, not merely synthetic, shape -- {!band} entries 11-13 rank it
ahead of the ordinary Sunday's own entry 15 regardless of subject.
Dropping the [subject = Lord] conjunct would wrongly Omit the Sunday
here too. *)
( "RG16(a) does NOT fire for a I-class feast of a SAINT (not the Lord) \
impeding a II-class Sunday -- stays Commemorate(Privileged)",
cand ~origin:P.Sanctoral ~rank:V.Class1 ~subject:Sub.Saint ~layer:PE.universal_layer
"nativity-of-st-john-the-baptist",
an_ordinary_sunday,
"Commemorate(Privileged)" );
(* Conjunct 2/4, LOSER rank: RG 16(a) names "Dominica II classis", not
any Sunday. A I-class Sunday (Advent/Lent/Passiontide, Low Sunday,
{!band} entry 6) can never actually reach here as a loser against a
Class1-or-2 Lord winner in real banding -- CORRECTED (fix round 1):
not because "entry 6's own band value (6) beats every entry such a
winner could occupy, 3/11-14" (WRONG: 6 is not lower than 3, and the
claim proves the opposite of what it was cited for anyway -- see
precedence_ef.ml's own corrected comment on this same conjunct for
the full argument). Against entries 11-14 the numeric claim genuinely
holds (6 < 11-14); against entry 3 (Epiphany/Ascension/Trinity/
Corpus Christi/Sacred Heart/Christ the King) it is a STRUCTURAL fact
instead -- every band-3 celebration is temporal-origin, and
[Precedence.resolve] takes exactly one temporal candidate per day, so
a band-3 Lord feast IS that date's own single temporal candidate,
never a second one contesting a separately-produced Sunday. Either
way, this is exactly why the primary text restricts itself to II
class -- but [disposition] takes no [context] and cannot itself
re-derive that fact, so the rank check is what actually enforces it
here. Sourced from [Temporal_ef.temporal]'s own real output (Advent I
Sunday 2026, Class1, "ef-advent-sunday-1"), the same row
[disposition_cases]'s own RG95/RG109(a) entry above already uses.
Dropping the [rank = Class2] conjunct on the loser would wrongly Omit
this Sunday's commemoration too. *)
( "RG16(a) does NOT fire for a I-class Sunday, even losing to a Feast of \
the Lord -- stays Commemorate(Privileged)",
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"transfiguration-of-our-lord",
of_temporal (T.advent_start 2026),
"Commemorate(Privileged)" );
(* Conjunct 3/4, LOSER is a Sunday at all: RG 16(a) protects "de
dominica" specifically, not every ordinary office a Feast of the Lord
happens to impede. An ordinary sanctoral SAINT losing to the same
Lord-subject Class2 winner as the row above must still reach the
ordinary RG 95/111(c) commemorate-or-omit flow, not RG 16(a)'s
special no-commemoration rule. Dropping [is_sunday_slug] (or its
[is_temporal] companion) here would wrongly Omit this ordinary
saint's commemoration too. *)
( "RG16(a) does NOT fire for an ordinary (non-Sunday) SAINT losing to a \
Feast of the Lord -- stays Commemorate(Ordinary)",
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"transfiguration-of-our-lord",
cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-other-saint",
"Commemorate(Ordinary)" );
(* Conjunct 4/4, WINNER rank: RG 16(a) says "I aut II classis", not any
class. Every real [subject = Lord] entry this project's own sanctoral
audit found is Class1 or Class2 (this task's own report), so this
conjunct is not independently reachable on real data -- SYNTHETIC,
the same defensive-but-not-load-bearing status this file already
gives the RG26 branch's own [not is_vigil] guard above. Without the
rank conjunct, [subject = Lord] alone would wrongly Omit this
Sunday's commemoration under a hypothetical Class3 "feast of the
Lord" too, which RG 16(a)'s own "I aut II classis" text does not
cover. *)
( "SYNTHETIC: RG16(a) does NOT fire for a Class3 feast of the Lord \
(outside \"I aut II classis\") impeding a II-class Sunday -- stays \
Commemorate(Privileged)",
cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Lord ~layer:PE.universal_layer
"ef-synthetic-class3-lord-feast",
an_ordinary_sunday,
"Commemorate(Privileged)" );
(* PRIMARY authority -- CORRECTED, fix round 1 (coordinator finding 7):
RG 95's own second paragraph, present in ALL THREE documents
including the electronic transcription: "Si vero duo festa eiusdem
Divinae Personae... simul occurrunt, fit de festo, quod in tabella
praecedentiae superiorem obtinet locum et aliud omittitur" -- if two
feasts of the SAME DIVINE PERSON occur together, the one higher in
the table is kept, the OTHER OMITTED -- an occurrence-level rule
closer to this exact collision than RG 112(a) below.
CORROBORATION, RG 112(a) (docs/research/rules-register.md §4/§6.0,
primary text, verified against all three documents): "112. Ad
commemorationes et orationes quod attinet, haec insuper serventur:
a) Officium, Missa aut commemoratio de aliquo festo vel mysterio
unius Divinae Personae excludit commemorationem aut orationem de
alio festo vel mysterio eiusdem Divinae personae" -- the Office,
Mass or commemoration of some feast or MYSTERY of ONE Divine Person
excludes a commemoration or oration of ANOTHER feast or mystery of
the SAME Divine Person. Real witness, the ONE date these two
candidates can ever coincide: Holy Family (RG 17(b)) as winner, the
Commemoration of the Baptism of the Lord (13 January, data/ef/
sanctoral.sexp: Class2, subject Lord, status Feast) as loser, on the
seven years 13 January is itself Holy Family's own date (2008, 2013,
2019, 2030, 2036, 2041, 2047).
CORROBORATION 2, the Holy Family Mass propers' own more specific
rubric, both photographic scans, word for word (real, and settles 13
January on its own regardless of RG 95/112(a)): "Si festum S.
Familiae occurrerit die 13 ianuarii, Missa dicitur de festo S.
Familiae, SINE COMMEMORATIONE BAPTISMATIS D.N.I.C., et sine
commemoratione dominicae" -- if the feast of the Holy Family occurs
on 13 January, the Mass is said of the Holy Family, WITHOUT
commemoration of the Baptism, and without commemoration of the
Sunday (RG 17(b)'s own general rule, restated).
CORRECTED, fix round 1 (coordinator finding 2): a previous version of
this comment claimed the electronic transcription's absence of this
specific rubric was "exactly the transcription's own documented
defect" -- WRONG, struck. That transcription (a 2006 web capture)
carries almost no Mass-propers text of any kind (5 Introit
occurrences in 26,322 lines against 61/402 in the two photographic
scans; its own "Proprium de Tempore" page is a table of contents
linking to PDFs the capture never pulled in) -- a coverage gap, not
a silent drop from content it otherwise has. RG 112 itself, unlike
this rubric, IS present in full in the transcription (all four
sub-clauses, verbatim) -- checked directly, not assumed. *)
( "RG112(a): a feast/mystery of the Lord excludes a commemoration of \
ANOTHER feast/mystery of the Lord -- Omit, not Commemorate (Holy \
Family vs the Baptism of the Lord, 13 January)",
of_temporal (T.holy_family_sunday 2019),
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"commemoration-of-the-baptism-of-the-lord",
"Omit" );
(* Conjunct 1/2, LOSER subject: an ORDINARY saint losing to the SAME
Lord-subject winner must still reach the ordinary commemorate flow,
not RG 112(a)'s exclusion -- RG 112(a) excludes another mystery of
the SAME Divine Person, not every commemoration a Lord feast happens
to outrank. Dropping the loser's own [subject = Lord] test would
wrongly Omit this ordinary saint's commemoration too. *)
( "RG112(a) does NOT fire when only the WINNER is a mystery of the Lord \
-- an ordinary saint stays Commemorate(Ordinary)",
of_temporal (T.holy_family_sunday 2019),
cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer
"ef-synthetic-ordinary-saint",
"Commemorate(Ordinary)" );
(* Conjunct 2/2, WINNER subject: a Class2 SAINT'S feast (not of the
Lord) impeding an ordinary temporal candidate must not exclude an
otherwise-due Lord commemoration either -- RG 112(a) is keyed on the
WINNER's own subject too, not just the loser's. Real slug/rank from
data/ef/sanctoral.sexp (25 January, Class3, "Conversion of St
Paul") standing in for a same-rank saint winner; only the [subject]
distinction is under test here, not a real-date collision (Holy
Family's own subject is what makes this branch reachable at all --
an ordinary sanctoral saint never wins against Holy Family in
practice, {!band}'s [entry_14_movable_band] sees to that, so this
row is SYNTHETIC in the same sense the RG16(a) block's own Class3
row above is: defensive, not a live witness). *)
( "SYNTHETIC: RG112(a) does NOT fire when only the LOSER is a mystery \
of the Lord -- stays Commemorate(Ordinary)",
cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer
"conversion-of-st-paul",
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"commemoration-of-the-baptism-of-the-lord",
"Commemorate(Ordinary)" );
(* RG112(d), fix round 1 (coordinator finding F1) -- PE.marian_slugs's
own citation has the full argument (both scans, including the 16
July "Missa dici potest aut ... aut ..." rubric). [of_temporal] on a
REAL 16-July-Saturday (2033) sources the winner from
Temporal_ef.temporal's own real output, the same review-finding-3
discipline this file's Ember/vigil rows already use -- not a
hand-typed slug/subject pair that could silently drift from what the
office actually builds. The loser is the real data/ef/sanctoral.sexp
entry, [Commemoration_only], the exact shape that made this branch
need to be checked BEFORE the Commemoration_only catch-all. *)
( "RG112(d): the BVM Saturday Office excludes a commemoration invoking \
the SAME BVM's intercession -- Omit, not Commemorate (Mt Carmel, \
16 July 2033, a real otherwise-unoccupied Saturday)",
of_temporal (mk 2033 7 16),
cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint
~layer:PE.universal_layer "our-lady-of-mt-carmel",
"Omit" );
(* Conjunct 1/2, LOSER identity: an ORDINARY Commemoration_only saint
NOT on PE.marian_slugs must still reach the ordinary commemorate
flow even when the winner is the BVM Saturday Office -- proves
RG112(d) excludes a specific same-BVM collision, not every
commemoration the office happens to outrank (the real live shape,
1 August, Holy Machabees, PE.marian_slugs's own comment). *)
( "RG112(d) does NOT fire when the LOSER is not a Marian commemoration \
-- an ordinary saint stays Commemorate(Ordinary) even under the BVM \
Saturday Office (1 August 2026, Holy Machabees)",
of_temporal (mk 2026 8 1),
cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint
~layer:PE.universal_layer "holy-machabees",
"Commemorate(Ordinary)" );
(* Conjunct 2/2, WINNER identity: an ordinary (non-Bvm, non-Marian-
titled) winner must not exclude Mt Carmel either -- RG112(d) is
keyed on the WINNER also being "de B. Maria Virg.", not merely on
the loser's own identity. SYNTHETIC: no live date has an ordinary
Class3 saint winning outright while Mt Carmel also loses that same
day (Mt Carmel's own fixed date, 16 July, has no other real
sanctoral entry to construct this from) -- proves the conjunct is
real, not merely untested, the same "SYNTHETIC" discipline the
RG112(a) rows above already use for their own second conjunct. *)
( "SYNTHETIC: RG112(d) does NOT fire when only the LOSER is Marian -- \
an ordinary winner leaves Mt Carmel Commemorate(Ordinary)",
cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer
"ef-synthetic-ordinary-winner",
cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint
~layer:PE.universal_layer "our-lady-of-mt-carmel",
"Commemorate(Ordinary)" )
]
(* Task 9: [privilege_of]'s RG 109 categories (§4, "Commemorations"),
exercised through [PE.disposition]'s [Commemorate] payload -- [privilege_of]
itself is private, so this is the only vantage point a test outside
precedence_ef.ml has on it. Each row below is built to match ONLY the one
category it names (see each row's own comment for why), closing the
hazard flagged in the task brief ("a test day that is both a Sunday and a
I-class day proves nothing about either"). Category (b), "of a I-class
day", already has its sole witness above (the Commemoration_only row):
a plain [Feast]-status [Class1] loser can never reach [Commemorate] at
all in this ruleset (RG 95 routes it to [Transfer] instead), so no
further row for (b) is added here -- see the task report. Category (f),
"of the Major Litanies, in Mass" -- NOW LIVE (ef-major-litanies task):
[PE.major_litanies_slug]'s own data/ef/adjustments.sexp entry is the
real candidate; the positive row below is added alongside the existing
negative one, which still proves the one slug this engine ALSO computes
that could be mistaken for it (the Minor Rogations, RG 87 -- a
different observance RG 109(f) does not name) is correctly NOT
conflated with it. *)
let privilege_cases =
[ (* (a) RG 109(a) (§4): "of a Sunday". [an_ordinary_sunday] is Class2,
not Class1, not within the Nativity octave, not an Ember day, not a
feria of Advent/Lent/Passiontide -- matches (a) alone. *)
( "(a) an ordinary Sunday commemoration is privileged",
cand "ef-nativity",
an_ordinary_sunday,
"Commemorate(Privileged)" );
(* (c) RG 109(c) (§4): "of days within the Octave of the Nativity" --
sourced from [Temporal_ef.temporal]'s own output (29 Dec 2026, Class2,
"ef-nativity-octave-day-5"), not a hand-typed slug, for the same
coupling-safety reason the file's own [of_temporal] rows use it
elsewhere. Not a Sunday, not Class1, not an Ember day, not an
Advent/Lent/Passiontide feria slug. *)
( "(c) a day within the Nativity octave is privileged",
cand "ef-nativity",
of_temporal (mk 2026 12 29),
"Commemorate(Privileged)" );
(* (d) RG 109(d) (§4): "of September Ember days" -- 23 Sep 2026 is
the September Ember Wednesday (independently derived from
[Temporal_ef]'s own third-Sunday-of-September rule: first Sunday of
September 2026 is the 6th, +14 days = 20th, +3 = 23rd), sourced from
[Temporal_ef.temporal] itself, Class2. Not a Sunday, not Class1, not
within the Nativity octave, not a plain Advent/Lent/Passiontide feria
slug either -- and, unlike the Advent/Lent Ember rows below, its own
slug ("ef-september-ember-wed") never starts with any of (e)'s own
[alp_feria_prefixes] ("ef-advent-"/"ef-lent-"/"ef-passiontide-"), so
(d) is this candidate's ONLY route to [Privileged] -- a genuine,
still-necessary distinction from (e), unlike the Advent/Lent Ember
case below (fix round 1). *)
( "(d) a September Ember day is privileged",
cand "ef-nativity",
of_temporal (mk 2026 9 23),
"Commemorate(Privileged)" );
(* (e) RG 109(e) (§4): "of ferias of Advent, Lent and Passiontide" --
two rows, one per season named, both from [Temporal_ef.temporal]'s
own generic ferial fallback, neither a Sunday, Ember day, or within
the Nativity octave. *)
( "(e) an Advent feria is privileged",
cand "ef-nativity",
of_temporal (mk 2026 12 1),
"Commemorate(Privileged)" );
( "(e) a Lent feria is privileged",
cand "ef-nativity",
of_temporal (off (-41)),
"Commemorate(Privileged)" );
(* CHANGED, fix round 1 (F1/F2/F6): these two rows used to be titled
"boundary: ... is NOT privileged (only September is, RG109(d))" and
expected [Omit] (a Task-16-pass reading that treated RG 109(e)'s bare
"feriis Adventus, Quadragesimae" as tacitly excluding the Advent and
Lent Ember sub-days, by analogy with (d)'s own separate, explicit
September carve-out). Review round 1 (F1) reproduced the real
consequence directly -- 1900-12-21, an Advent Ember Friday, lost its
own commemoration entirely, while an ordinary (lower-solemnity,
non-Ember) Advent feria the same week kept its commemoration --
backwards on any reading, and traced it to this exact
misclassification (F2).
Corrected reading (precedence_ef.ml's own [privilege_of], (e)
branch, carries the full argument): RG 91's TABLE needs an explicit
"exceptis feriis Quatuor Temporum" at its own entries 22 and 25 to
keep Ember days from being double-listed against their own entry 18
-- an exception that would be unnecessary drafting if "feriae
Adventus"/"feriae Quadragesimae" did not ALREADY include their Ember
sub-days by default. RG 109(e) carries no such "exceptis" clause, so
its bare text is read at that same default, INCLUSIVE scope: the
Advent and Lent Ember ferias ARE privileged under (e), not merely
commemorable-but-ordinary. (d)'s own separate existence survives
this reading intact -- September Ember days sit outside Advent/Lent/
Passiontide under ANY reading, so (d) remains the ONLY way they
reach [Privileged], the point the row immediately above this one
makes explicit.
16 Dec 2026 is the Advent Ember Wednesday (independently derived:
Advent I 2026 is 29 Nov, +14 days = 13 Dec, +3 = 16 Dec); the Lent
Ember Wednesday is the same date [off (-39)] already used by the
entry-18 [band] row above. Both sourced from [Temporal_ef.temporal],
not hand-typed, for the same coupling-safety reason every
[of_temporal] row in this file uses it. *)
( "(e), corrected fix round 1: an Advent Ember day is ALSO privileged, \
not excluded from (e)",
cand "ef-nativity",
of_temporal (mk 2026 12 16),
"Commemorate(Privileged)" );
( "(e), corrected fix round 1: a Lent Ember day is ALSO privileged, \
not excluded from (e)",
cand "ef-nativity",
of_temporal (off (-39)),
"Commemorate(Privileged)" );
(* (f) RG 109(f) (§4; Caput XVI "De Commemorationibus"): "de Litaniis
maioribus, in Missa" -- ef-major-litanies task, NEW. [rank] is
DELIBERATELY [Class4], not [cand]'s own [Class1] default: category
(b) above ("of a I-class day") fires on [rank = Class1] alone and
is checked BEFORE (f) in [privilege_of]'s own branch order, so a
[Class1] row here would reach [Privileged] via (b) regardless of
whether (f) itself is even wired -- proving nothing about (f)
specifically (the exact "test day that is both [X] and [Y] proves
nothing about either" hazard this table's own header warns about,
worked for the FIRST time on this axis rather than Sunday-vs-
I-class). [Class4] (data/ef/adjustments.sexp's own honestly-flagged
placeholder rank, chosen for exactly this reason) rules out (b),
and the slug is neither a Sunday, within the Nativity octave, an
Ember day, nor an Advent/Lent/Passiontide feria -- (f) is this
row's only route to [Privileged], the real proof. *)
( "(f) the Major Litanies commemoration is privileged (RG109(f))",
cand "ef-nativity",
cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer
PE.major_litanies_slug,
"Commemorate(Privileged)" );
(* Negative, RG 109(f)'s own boundary: the Minor Litanies/Rogations
(Monday/Tuesday before Ascension, RG 87 -- [Temporal_ef.temporal]
DOES compute these, unlike the Major Litanies RG 109(f) actually
names, see [privilege_of]'s own comment) must NOT be mistaken for the
Major Rogations RG 109(f) privileges: RG 88 says the Minor Rogations
change nothing in the Office at all, so nothing about them is
privileged either -- and (Task 16) being temporal+ordinary, a Minor
Rogation day impeded by a saint is now omitted outright, matching the
missalemeum oracle exactly (Task 16 report: 11 May 2026 and 12 May
2026, both Minor Rogation days impeded by a saint, show zero
commemoration of the Rogation in the independent oracle). *)
( "boundary: a Minor Rogation day is NOT privileged (RG109(f) names \
the Major Litanies, not these) -- TASK16 omits it entirely",
cand "ef-nativity",
of_temporal (off 36),
"Omit" )
]
(* Task 9: [PE.admit] -- RG 111's admission counts (§4, "Commemorations"),
given commemorations ALREADY tagged with their real privilege (as
[PE.disposition] now tags them -- see [privilege_cases] above). Every
candidate/privilege pair here is built directly, not routed through
[PE.disposition], so these rows isolate [admit]'s own selection logic
from [privilege_of]'s classification -- the two are proved separately by
design (unlike a test that only proves [admit] admits SOME correct-looking
set without knowing whether it or [privilege_of] supplied the "correct"
part). Checked on slug IDENTITY, not count (the brief: "'two admitted'
proves nothing about *which* two"). *)
(* Class2 dignity, tagged [Ordinary] explicitly (not via [privilege_of]) --
used as the higher-dignity, non-privileged half of every asymmetry pair
below. *)
let ordinary_hi = cand ~rank:V.Class2 "ef-ordinary-hi"
(* Class3 dignity (LOWER than [ordinary_hi]), tagged [Privileged] explicitly
-- pairing a lower-dignity privileged candidate against a higher-dignity
ordinary one is what makes the II-class-Sunday-vs-other-II-class
asymmetry observable: pure dignity and "privilege wins the slot" pick
DIFFERENT winners from this exact pair. *)
let privileged_lo = cand ~rank:V.Class3 "ef-privileged-lo"
(* Class2 dignity (tied with [ordinary_hi], distinguishing rank from
privilege alone), tagged [Privileged] -- the higher-dignity privileged
candidate for the "two privileged due" row. *)
let privileged_hi = cand ~rank:V.Class2 "ef-privileged-hi"
(* Class4, the lowest dignity in play -- the third candidate for the
III/IV-class "at most two" row, so which TWO of three survive is the
thing under test, not merely how many. *)
let ordinary_lowest = cand ~rank:V.Class4 "ef-ordinary-lowest"
(* Class3, tagged [Ordinary] -- fix round 1, F7: the RG 111(b) rank-floor
witness [admit_cases] was missing. [ordinary_hi] above is already Class2,
so every existing II-class-Sunday row here passes whether or not
[admit]'s "de festo II classis" filter is even present -- reverting that
filter would only redden [test_oracle.ml], not this file, which is
exactly the coverage gap the review round found. This candidate is the
ONLY thing due on the Sunday row below, so a version of [admit] without
the rank floor would (wrongly) admit it on pure "best available"
dignity, same as it would have admitted [ordinary_hi]. *)
let ordinary_class3 = cand ~rank:V.Class3 "ef-ordinary-class3"
let observed_class1 = cand "ef-nativity" (* Class1 by [cand]'s own default. *)
let observed_class2_sunday = an_ordinary_sunday (* Class2, slug carries "-sunday". *)
let observed_class2_other = cand ~rank:V.Class2 "ef-other-class2-day" (* Class2, no "-sunday". *)
let observed_class3 = cand ~rank:V.Class3 "ef-some-class3-day"
let slugs_of admitted =
List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) admitted
(* [admit_cases] below (Task B, ef-rg16a) needs a precedence-order [int] on
every comms triple -- {!Precedence.rules.admit}'s new parameter, RG 113
(docs/research/rules-register.md §4, "RG 113"). This table's own header
comment states its purpose: isolate [admit]'s own selection/count logic
from {!PE.band}'s occurrence-table classification, so every candidate
here is built from a synthetic slug ("ef-ordinary-hi" etc.) that matches
none of [band]'s 28 real branches -- calling the REAL [PE.band] on them
would collapse every one to {!PE.unclassified} (the same tied value),
destroying the table's ability to distinguish rows at all. [order_of_rank]
is a TEST-ONLY stand-in, reproducing exactly the relative order the
previous [dignity] function gave (RG 8's rank cardinal, lower first) --
so every existing expectation below still holds unchanged; only the
tuple shape gained this third field. *)
let order_of_rank = function V.Class1 -> 10 | V.Class2 -> 20 | V.Class3 -> 30 | V.Class4 -> 40
let po c p = (c, p, order_of_rank c.P.cel.Cel.rank)
(* Real-data shapes for the fix round's own RG16(a)/[admit] interaction
(below): a Feast of the Lord (subject Lord, Class2, sanctoral) as
[observed], oracle-confirmed real slugs/ranks rather than hand-typed
ones, the same coupling-safety reason [of_temporal] rows elsewhere in
this file use real data. *)
let lord_winner =
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"transfiguration-of-our-lord"
(* Pope Sixtus II et al. (6 August, real slug/rank/status from
data/ef/sanctoral.sexp): Class3, Commemoration_only, tagged [Ordinary]
here directly (as every other [admit_cases] row does, isolating
[admit]'s own selection logic from [privilege_of], per this table's own
header comment). *)
let sixtus =
cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only
~layer:PE.universal_layer "pope-sixtus-ii-felicissimus-and-agapitus-martyrs"
(* (description, observed, temporal, comms, expected). [temporal] --
{!Precedence.rules.admit}'s own new parameter, fix round 1 (RG16(a)
task): the day's temporal-cycle candidate, independent of who is
[observed]. Every row below except the last two repeats [observed] as
[temporal] -- no RG16(a) displacement occurs in those rows (the pattern
[PE.admit]'s pre-fix code implicitly, and wrongly, assumed held
UNIVERSALLY), so the two values coinciding is the faithful shape, not a
simplification that dodges the new parameter. The last two rows are
where they genuinely differ -- see their own comment. *)
(* RG 110 (§4, Caput XIV): candidates built with the REAL trigger/companion
slugs {!PE.rg110_companion_slug} matches on -- unlike every other
[admit_cases] candidate above, which is deliberately built with a
SYNTHETIC slug {!PE.band}'s 28 branches (and this file's own RG 110
lookup) cannot recognise, these three rows need [PE.admit] to recognise
the slug itself, so the real ones are used directly. [~status:
Cel.Commemoration_only] on the companions matches the real data shape
(data/ef/sanctoral.sexp's own [peter]/[paul];
data/ef/adjustments.sexp's own [commemoration-of-st-peter]) even though
[admit] itself never reads [status] -- only [Precedence.resolve]'s own
partition does, upstream of [admit] -- kept for a reader's sake, not a
test requirement. *)
let chair_of_st_peter_synth = cand ~rank:V.Class2 "chair-of-st-peter"
let paul_companion_synth = cand ~rank:V.Class3 ~status:Cel.Commemoration_only "paul"
(* An unrelated third commemoration, sorting AFTER both members of the RG110
pair, so that "spliced immediately after its trigger" and "appended at the
end" give different answers. Added by the fix-round re-review (G1): with
only the pair present the two are indistinguishable, and the re-review
proved an append-at-end variant passed the whole suite green. Class3
Commemoration_only so it cannot win the day; its slug is deliberately not
one {!PE.rg110_companion_slug} recognises. *)
let unrelated_comm_synth =
cand ~rank:V.Class3 ~status:Cel.Commemoration_only "zzz-unrelated-commemoration"
(* Any real Lent/Ember feria's own shape (a Class3, PRIVILEGED, temporal-
origin candidate) -- {!PE.rg110_companion_slug} does not match this slug,
so it is a neutral bystander in [admit]'s own normal (pre-RG110) contest,
exactly like [privileged_lo] elsewhere in this table, just kept
separately named so this block's own rows are self-contained and do not
depend on a binding declared for an unrelated purpose above. Given the
WORSE (higher) precedence int than [paul_companion_synth] would need to
win the day's one "other II class" slot on its own dignity ALONE -- the
whole point of the row below is that RG 110 does not care who wins that
contest; it adds Paul regardless. *)
let lent_feria_synth = (cand ~origin:P.Temporal ~rank:V.Class3 "ef-lent-2-monday-synth", P.Privileged, 180)
(* RG 113 sentence ONE (docs/research/rules-register.md §6.26):
"Commemoratio de Tempore fit primo loco." The commemoration OF THE SEASON
is made in the first place; the second sentence's table order governs the
OTHERS. Every other candidate in this table is [origin = P.Temporal] by
[cand]'s own default, so the rule is an identity on them and no existing
row's expectation moves; these two candidates exist to make a MIXED-origin
row possible, which is the only shape that can witness it.
This file's [admit_cases] is the ONLY place in the whole suite that
asserts commemoration ORDER -- the differential never compares
commemorations, the oracle sorts both sides into a multiset, and
test_golden.ml's own [describe] deliberately sorts its comms field. So a
row here is the only thing standing between this rule and a silent
regression. *)
let sanctoral_hi =
{ (cand ~rank:V.Class2 "ef-sanctoral-hi") with P.origin = P.Sanctoral }
let seasonal_lo = cand ~rank:V.Class3 "ef-seasonal-lo"
let admit_cases =
[ (* RG 111 (§4): "I class: none save one privileged." *)
( "I-class day, only an ordinary commemoration due -> none admitted",
observed_class1, observed_class1,
[ po ordinary_hi P.Ordinary ],
[] );
(* RG 113 s1: the de Tempore commemoration leads, even though the
sanctoral one outranks it on RG 91's table (Class2 beats Class3) and
would therefore come first under sentence TWO alone. Both slots of a
III-class day are filled, so this row tests ORDER, not selection. *)
( "III-class day, seasonal + sanctoral both admitted -> the SEASONAL one \
first (RG 113 s1), despite the sanctoral one's higher table position",
observed_class3, observed_class3,
[ po sanctoral_hi P.Ordinary; po seasonal_lo P.Ordinary ],
[ "ef-seasonal-lo"; "ef-sanctoral-hi" ] );
( "I-class day, ordinary + privileged both due -> only the privileged \
one, regardless of the ordinary one's higher dignity",
observed_class1, observed_class1,
[ po ordinary_hi P.Ordinary; po privileged_lo P.Privileged ],
[ "ef-privileged-lo" ] );
( "I-class day, two privileged due -> only the higher-dignity one (still \
just \"one\")",
observed_class1, observed_class1,
[ po privileged_lo P.Privileged; po privileged_hi P.Privileged ],
[ "ef-privileged-hi" ] );
(* RG 111: "II-class Sundays: one (dropped if a privileged one is due)." *)
( "II-class Sunday, only an ordinary commemoration due -> it is admitted",
observed_class2_sunday, observed_class2_sunday,
[ po ordinary_hi P.Ordinary ],
[ "ef-ordinary-hi" ] );
( "II-class Sunday, ordinary (higher dignity) + privileged (lower \
dignity) both due -> the PRIVILEGED one is admitted, the ordinary \
one dropped despite outranking it",
observed_class2_sunday, observed_class2_sunday,
[ po ordinary_hi P.Ordinary; po privileged_lo P.Privileged ],
[ "ef-privileged-lo" ] );
(* RG 111(b)'s own rank floor ("scilicet DE FESTO II CLASSIS"), fix
round 1 F7: a Class3 ORDINARY candidate -- no privileged rival due,
so the pre-fix-round code's "no privileged? take the best of what's
left" fallback would (wrongly) admit it -- is admitted NOTHING. The
slot is reserved for a II-class candidate specifically; a III-class
ordinary one has no standing for it at all, unlike "other II class"
below, which has no such restriction. *)
( "II-class Sunday, sole candidate is an ORDINARY Class3 (not \
\"de festo II classis\") -> admitted nothing, not the best available",
observed_class2_sunday, observed_class2_sunday,
[ po ordinary_class3 P.Ordinary ],
[] );
(* RG 111: "other II class: one" -- no privilege override, the exact
asymmetry the brief and precedence_ef.ml's own [admit] comment flag:
same candidate pair as the II-class-Sunday row above, OPPOSITE
observed day, OPPOSITE winner. *)
( "other II-class day, only an ordinary commemoration due -> it is \
admitted",
observed_class2_other, observed_class2_other,
[ po ordinary_hi P.Ordinary ],
[ "ef-ordinary-hi" ] );
( "other II-class day, same ordinary+privileged pair as the Sunday row \
above -> the ORDINARY one wins on pure dignity this time, the \
privileged one dropped",
observed_class2_other, observed_class2_other,
[ po ordinary_hi P.Ordinary; po privileged_lo P.Privileged ],
[ "ef-ordinary-hi" ] );
(* RG 111: "III-IV class: at most two" -- three candidates due, top two
by dignity admitted, the third (lowest dignity) dropped. *)
( "III-class day, three commemorations due -> the top two by dignity, \
not merely \"two of them\"",
observed_class3, observed_class3,
[ po ordinary_hi P.Ordinary; po privileged_lo P.Privileged;
po ordinary_lowest P.Ordinary ],
[ "ef-ordinary-hi"; "ef-privileged-lo" ] );
(* Fix round 1, item 2 (RG16(a) task review): the CRITICAL witness for
[~temporal]. RG 16(a)'s own text -- the winning Feast of the Lord
holds the Sunday's place "cum omnibus iuribus et privilegiis" -- means
the day is STILL a "dominica II classis" for RG 111(b)'s own rank
floor, even though [observed] is now the FEAST (Transfiguration, real
slug/rank/subject), not the Sunday. [temporal] (a real Sunday
candidate, {!an_ordinary_sunday}) is what reveals that; nothing in
[observed] does, since its own slug carries no Sunday marker at all.
Oracle-confirmed (missalemeum, 2023-08-06, a Sunday):
"commemorations": [], Sixtus "displaced". Before this fix
[observed_is_sunday] read [observed]'s own slug and got [false] here,
wrongly taking the "other II class: one" branch below and admitting
Sixtus (Class3) despite RG 111(b)'s rank floor. *)
( "RG16(a) interaction: a Feast of the Lord observed on a day whose \
TEMPORAL candidate is a II-class Sunday -> the RG111(b) rank floor \
still applies, admits nothing (Sixtus, Class3, has no standing)",
lord_winner, an_ordinary_sunday,
[ po sixtus P.Ordinary ],
[] );
(* The control, same pair as the row above with [observed] = [temporal]
(no displacement -- an ordinary WEEKDAY, not a Sunday): "other II
class: one", no rank floor, Sixtus IS admitted. Oracle-confirmed
(missalemeum, 2026-08-06, a Thursday): Sixtus in "commemorations".
Proves the DISCRIMINATING factor is [temporal]'s own Sunday-ness, not
merely "was the winner a Feast of the Lord" -- that alone is true of
BOTH rows here, and only one of them excludes Sixtus. *)
( "control: the same Feast of the Lord on an ORDINARY weekday (temporal \
= observed, not a Sunday) -> \"other II class: one\", Sixtus IS \
admitted",
lord_winner, lord_winner,
[ po sixtus P.Ordinary ],
[ "pope-sixtus-ii-felicissimus-and-agapitus-martyrs" ] );
(* RG 110, shape (a)/(b): [chair-of-st-peter] observed, a competing
PRIVILEGED Lent feria also due for the day's single "other II class"
slot (RG 111(c) itself has no privilege-override clause -- unlike the
Sunday branch above -- so pure precedence order decides that slot on
its own, and [lent_feria_synth]'s own 180 beats
[paul_companion_synth]'s 999). Without RG 110, this is exactly
precedence_ef.ml's own measured defect (3,533 of 8,417 domain-wide
years, this task's own report): Paul loses the slot outright and is
never admitted at all. With RG 110, Paul is added ADDITIONALLY,
PREPENDED ahead of the feria -- RG 110(a)/(b)'s own text ("oratio
alterius Apostoli additur ... orationi diei", the companion's oration
added to the DAY's own oration) governs here, not (c)'s "ante omnes
alias commemorationes" (CORRECTED, fix round 1, coordinator finding
F1: this comment previously cited (c) for both shapes uniformly --
see [rg110_additions]'s own comment in precedence_ef.ml for why the
two shapes need, and get, DIFFERENT orderings, not the same one for
different reasons) -- both admitted, not one displacing the other. *)
( "RG110 (a)/(b): chair-of-st-peter observed, Paul rides alongside a \
competing privileged Lent feria, uncapped and prepended",
chair_of_st_peter_synth, chair_of_st_peter_synth,
[ (paul_companion_synth, P.Ordinary, 999); lent_feria_synth ],
[ "paul"; "ef-lent-2-monday-synth" ] );
(* RG 110, shape (c): [chair-of-st-peter] itself LOSES the day outright
(observed is some unrelated II-class Sunday) but is admitted, on its
OWN Class2 dignity, as RG 111(b)'s single ordinary commemoration --
exactly the domain-wide 593-year pattern this task's own report
measures. RG 110(c)'s own text, word for word ("quoties vero oratio
unius Apostoli addenda est ad modum commemorationis, HUIC ORATIONI
ADDITUR ALTERA immediate, ante omnes alias commemorationes"): the
companion (Paul, Class3, no standing of its OWN for this Sunday's
rank-floored slot) rides along anyway, uncapped -- ADDED TO THE
TRIGGER'S own oration ("huic orationi additur altera"), i.e.
FOLLOWING it, not preceding it. CORRECTED, fix round 1 (coordinator
finding F1): this row originally asserted `["paul";
"chair-of-st-peter"]` -- the companion BEFORE the trigger -- citing
the SAME "ante omnes alias commemorationes" clause that actually
governs the PAIR's position relative to OTHER, unrelated
commemorations, not the pair's own internal order. "Huic orationi"
(dative, "to THIS oration") refers back to "oratio unius Apostoli",
the trigger's own oration -- so the companion is added TO it,
following, not leading. Reproduced by mutation: reverting
[rg110_additions]'s shape-(c) branch back to a uniform prepend
reddens exactly this row's own expected order, nothing else. *)
( "RG110 (c): chair-of-st-peter admitted merely AS a commemoration \
(loses the day to an ordinary II-class Sunday), Paul rides along \
AFTER it (RG110(c): \"huic orationi additur altera\")",
cand ~rank:V.Class2 "ef-some-other-sunday", cand ~rank:V.Class2 "ef-some-other-sunday",
(* A third, unrelated candidate is OFFERED here deliberately, and is
correctly NOT admitted -- which is itself the point.
The fix-round re-review asked for a third commemoration so that
"spliced IMMEDIATELY after its own trigger" (RG110(c)'s own word)
could be told apart from "appended at the end of the list": with a
single-member result the two coincide, and it proved an
append-at-end variant passed the whole suite green.
Attempting it establishes something better than the test it asked
for: on this day-shape a third commemoration is RUBRICALLY
IMPOSSIBLE, so the distinction is unobservable BY CONSTRUCTION, not
merely unobserved. Shape (c) requires the trigger to LOSE the day
while still being admitted -- and a Class2 trigger can only lose to
a I-class day (RG 111(a): one privileged commemoration) or a
II-class Sunday (RG 111(b): one, and "scilicet de festo II
classis"). Both caps are ONE. Anything a third candidate could be
is either outranked by the trigger or excluded by the rank floor;
here [zzz-unrelated-commemoration] is Class3 and RG 111(b) drops
it, leaving the pair alone.
That is why the re-review's own domain sweep found shape (c) never
co-occurring with a third commemoration in 8 417 years: not an
accident of the data, a consequence of the rubric.
So RG110(c)'s "ante omnes alias commemorationes" half has no
reachable witness in the EF's real rubrical space, and the
adjacency this row asserts is exercised only against the pair
itself. The append-at-end variant therefore remains
indistinguishable from the splice on any input the engine can
actually construct -- recorded in the register rather than papered
over with a synthetic row that proves a shape no rubric permits. *)
[ po chair_of_st_peter_synth P.Ordinary;
(paul_companion_synth, P.Ordinary, 999);
po unrelated_comm_synth P.Ordinary ],
[ "chair-of-st-peter"; "paul" ] );
(* Defensive: a trigger slug is [observed], but its companion is simply
not present among [comms] at all (a data gap, or -- as this exact
shape was for 30 June before this task's own [Add
commemoration-of-st-peter] directive -- an upstream omission).
{!PE.rg110_find_companion} must return [None] gracefully, not raise;
the day's own (empty, here) normal result is unaffected. *)
( "RG110 defensive: chair-of-st-peter observed, no companion present in \
comms at all -> no crash, nothing added",
chair_of_st_peter_synth, chair_of_st_peter_synth, [], [] )
]
(* Order independence (brief: "the admitted set must not depend on input
order"): the SAME three candidates as the III-class row above, passed in
the reverse order, must still admit the same top two -- exercised on this
row specifically because it is the one where the sort actually has work
to do (three distinct dignities, a real top-2 cut), unlike a
two-candidate row where either order already happens to be sorted. *)
let test_admit_order_independent () =
let comms =
[ po ordinary_hi P.Ordinary; po privileged_lo P.Privileged; po ordinary_lowest P.Ordinary ]
in
let forward = slugs_of (PE.admit ~observed:observed_class3 ~temporal:observed_class3 comms) in
let reversed =
slugs_of (PE.admit ~observed:observed_class3 ~temporal:observed_class3 (List.rev comms))
in
Alcotest.(check (list string)) "reversed input admits the same candidates"
forward reversed
(* RG 113 teeth (Task B, ef-rg16a): two SAME-rank candidates whose slug order
DISAGREES with their band/precedence order -- "aa-worse-precedence" sorts
first alphabetically but is given the WORSE (higher) precedence int,
"zz-better-precedence" sorts last alphabetically but the BETTER (lower)
one. Both are [Class3] (same RG 8 "dignity", so a version of [admit] that
silently reverted to sorting by [rank] then slug -- the exact pre-fix
shape this task replaced -- would pick "aa-worse-precedence" here, the
OPPOSITE of what this asserts. A real RG 91 [band] value never actually
produces this exact pairing against these two synthetic slugs (see
[admit_cases]'s own header on why this table uses synthetic, non-band-
classifiable slugs) -- the precedence ints are supplied directly, the
same [po]/explicit-order convention this file already uses, standing in
for whatever real [band] would compute. This is what proves [admit]
actually consults the passed-in precedence value rather than merely
accepting one as a matter of new type-checking. *)
let order_tiebreak_alpha_first_worse = cand ~rank:V.Class2 "aa-worse-precedence"
let order_tiebreak_alpha_last_better = cand ~rank:V.Class2 "zz-better-precedence"
(* [observed_class2_other]: "other II class: one" admits exactly ONE, by
precedence order alone (no privilege override, no rank floor) -- unlike
the III-class row [test_admit_order_independent] reuses, which admits
TWO out of two offered here and so cannot distinguish "picked by
precedence" from "picked both anyway". *)
let test_admit_uses_precedence_not_slug_when_they_disagree () =
let comms =
[ (order_tiebreak_alpha_first_worse, P.Ordinary, 90);
(order_tiebreak_alpha_last_better, P.Ordinary, 10) ]
in
Alcotest.(check (list string))
"the BETTER-precedence candidate is admitted even though it sorts LAST alphabetically"
[ "zz-better-precedence" ]
(slugs_of (PE.admit ~observed:observed_class2_other ~temporal:observed_class2_other comms))
(* The brief: "a case proving that what the limit drops is reported in
omitted rather than vanishing" -- three end-to-end proofs, wired with the
REAL [PE.band], [PE.disposition] and [PE.admit] together (not a stub, so
[privilege_of]'s real classification is exercised too, not just [admit]'s
selection logic in isolation as above).
[rules] deliberately reused, not rebuilt per test, since it is always the
same three real functions. *)
let real_rules = { P.band = PE.band; disposition = PE.disposition; admit = PE.admit }
(* I-class day, zero admitted: the strongest form of "does not vanish" --
EVERY commemoration due is dropped (RG 111: "none save one privileged",
and the one loser here is ordinary), yet it must still appear in
[omitted], not merely be absent from [commemorations]. *)
let test_i_class_day_drops_into_omitted () =
let date = mk 2026 12 25 in
let day_ctx = ctx date in
let nativity = of_temporal date in
let saint = cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3" in
let resolution = P.resolve real_rules day_ctx ~temporal:nativity ~sanctoral:[ saint ] in
Alcotest.(check (list string)) "nothing admitted on a I-class day with only an ordinary loser due"
[] (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations);
Alcotest.(check (list (pair string string))) "the ordinary loser is reported omitted, not vanished"
[ ("ef-some-saint-3", "omitted: admission limit reached") ]
(List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted)
(* II-class Sunday, two ordinary losers due, RG 111's "one" admits the
higher-dignity one and drops the other into [omitted]. *)
let test_ii_class_sunday_drops_second_loser_into_omitted () =
let date = mk 2025 11 9 (* an ordinary Time-after-Pentecost Sunday, not All Souls-adjacent. *) in
let day_ctx = ctx date in
let sunday =
{ P.cel =
Cel.make ~slug:(S.of_string_exn "ef-time-after-pentecost-sunday-x") ~rank:V.Class2
~colour:Col.Green ~subject:Sub.Temporal ~layer:"temporal" ();
origin = P.Temporal }
in
let saint_a = cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-saint" in
let saint_b = cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3" in
let resolution = P.resolve real_rules day_ctx ~temporal:sunday ~sanctoral:[ saint_a; saint_b ] in
Alcotest.(check (list string)) "only the higher-dignity (Class2) loser is admitted"
[ "ef-some-saint" ]
(List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations);
Alcotest.(check (list (pair string string))) "the lower-dignity loser is reported omitted, not vanished"
[ ("ef-some-saint-3", "omitted: admission limit reached") ]
(List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted)
(* A genuinely privileged commemoration reaching [admit] through the REAL
pipeline (register RG 109(e)): a Lent feria (Class3, temporal-origin)
loses to a universal Class2 sanctoral feast on the same date, and
[PE.disposition] tags it [Privileged] via [privilege_of] -- proving
[privilege_of] and [admit] cooperate correctly end-to-end, not merely in
the hand-tagged unit tests above. *)
let test_privileged_lent_feria_admitted_end_to_end () =
let date = off (-41) (* Lent I Monday, the same date the entry-22 [band] row uses. *) in
let day_ctx = ctx date in
let lent_feria = of_temporal date in
let saint = cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-saint" in
let resolution = P.resolve real_rules day_ctx ~temporal:lent_feria ~sanctoral:[ saint ] in
Alcotest.(check string) "the Lent feast wins the day, not the sanctoral feast's own commemoration"
"ef-some-saint" (S.to_string resolution.P.observed.P.cel.Cel.slug);
Alcotest.(check (list (pair string string))) "the Lent feria is admitted, tagged Privileged"
[ ("ef-lent-1-monday", "Privileged") ]
(List.map
(fun (c, p) -> (S.to_string c.P.cel.Cel.slug, match p with P.Privileged -> "Privileged" | P.Ordinary -> "Ordinary"))
resolution.P.commemorations);
Alcotest.(check int) "nothing omitted" 0 (List.length resolution.P.omitted)
(* Fix round 1 (post-Task-9 review): the II-class-Sunday override (RG 111
"one, dropped if a privileged one is due") reached through the REAL
pipeline, with both a privileged AND an ordinary commemoration due on the
same day -- previously only proven at the isolated [admit] level
([admit_cases]'s hand-tagged rows above). RG 109(a)-(f)'s five reachable
categories are all properties of a TEMPORAL-origin office, and only one
temporal candidate exists per date, so a privileged AND an ordinary
commemoration cannot both be due from the temporal side alone -- but
category (b) is the exception: it is sanctoral and rank-based (a
[Commemoration_only] entry carrying [Class1], the exact shape
[disposition_cases]'s own (b) witness row uses), and a [Commemoration_only]
entry is held out of the band contest entirely ({!Precedence.resolve}),
so it is a loser regardless of what [observed] turns out to be. Paired
with an ordinary sanctoral saint, both lose to an ordinary Class2 Sunday,
giving [observed_is_sunday = true] with one [Privileged] and one
[Ordinary] loser due at once -- no synthetic fixture needed.
NOTE on what this test does and does not prove: the only reachable
witness for RG 109(b) is [Class1] (the highest dignity), and any
sanctoral loser that could ALSO beat this same Sunday by pure dignity
would win the day outright instead of losing to it (any [Feast]-status
[Class1] sanctoral candidate bands at entry 11-13, ahead of an ordinary
Sunday's entry 15) -- so within this specific pipeline shape the
privileged loser is unavoidably also the higher-dignity one, and this
test cannot by itself distinguish "privilege overrides dignity" from
"dignity alone happened to pick the same winner". That distinction is
what [admit_cases]'s hand-tagged rows above prove (a LOWER-dignity
privileged candidate still beats a HIGHER-dignity ordinary one on a
II-class Sunday, the opposite of "other II class"'s own row). This test's
job is narrower and complementary: proving the real pipeline
([PE.band], [PE.disposition], [PE.privilege_of] via [disposition],
[PE.admit] together) actually reaches and exercises the override branch
end-to-end, not merely in isolation. *)
let test_ii_class_sunday_privileged_witness_admitted_end_to_end () =
let date = mk 2026 7 5 (* an ordinary Time-after-Pentecost Sunday. *) in
let day_ctx = ctx date in
let sunday = of_temporal date in
let privileged_witness =
cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~layer:PE.universal_layer
"ef-commemoration-only-b-witness"
(* Class1 by [cand]'s own default -- RG 109(b). *)
in
let ordinary_saint = cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-saint" in
let resolution =
P.resolve real_rules day_ctx ~temporal:sunday ~sanctoral:[ privileged_witness; ordinary_saint ]
in
(* Checked against [sunday]'s own slug, not a hand-typed/guessed literal
(its exact week number is not worth independently re-deriving here):
this asserts identity with the real [Temporal_ef.temporal] candidate,
proving the SUNDAY -- not either sanctoral loser -- is what wins the
day, which the privilege assertions below presuppose. *)
Alcotest.(check string) "the Sunday wins the day, not either sanctoral loser"
(S.to_string sunday.P.cel.Cel.slug) (S.to_string resolution.P.observed.P.cel.Cel.slug);
Alcotest.(check (list (pair string string)))
"only the privileged (Commemoration_only, Class1) witness is admitted"
[ ("ef-commemoration-only-b-witness", "Privileged") ]
(List.map
(fun (c, p) -> (S.to_string c.P.cel.Cel.slug, match p with P.Privileged -> "Privileged" | P.Ordinary -> "Ordinary"))
resolution.P.commemorations);
Alcotest.(check (list (pair string string))) "the ordinary saint is dropped into omitted, not vanished"
[ ("ef-some-saint", "omitted: admission limit reached") ]
(List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted)
(* RG 91 entry 27 / RG 78-79 (Caput IX, "De sancta Maria in sabbato") reached
end-to-end through the REAL pipeline -- this is THE hazard this task's own
brief names explicitly: {!PE.band}'s entry-27 branch has been reachable
only in isolation (the table-driven row above) since it was written, never
through a real [Temporal_ef.temporal] candidate winning an actual day, so
this test is checked for whether it exercises entry 27 for the RIGHT
reason -- proved by the mutation test recorded in the task report
(reverting temporal_ef.ml's own [is_bvm_saturday] branches back to the
generic ferial fallback reddens THIS test, not merely the [band]-table
row, which would still pass unchanged since it is sourced from real
[T.temporal] output either way).
1 August 2026, matching the task brief's own worked example against the
oracle exactly: a Time-after-Pentecost Saturday, otherwise unoccupied but
for Holy Machabees (Commemoration_only, Class3, real data/ef/sanctoral.sexp
entry, 1 August) -- the office wins outright (band 270, ahead of Holy
Machabees's own [unclassified]) and the Commemoration_only loser still
rides along as an ordinary commemoration, the SAME shape the oracle's own
row for this date shows ("V Mass of the B. V. M. -- Salve, Sancta Parens",
commemorating Holy Machabees). *)
let test_bvm_saturday_wins_and_commemorates_end_to_end () =
let date = mk 2026 8 1 in
let day_ctx = ctx date in
let bvm_saturday = of_temporal date in
let holy_machabees =
cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~layer:PE.universal_layer
"holy-machabees"
in
let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ holy_machabees ] in
Alcotest.(check string) "the BVM Saturday office is observed, not Holy Machabees"
"ef-time-after-pentecost-9-saturday" (S.to_string resolution.P.observed.P.cel.Cel.slug);
Alcotest.(check string) "observed rank is class-4, unconditional" "class-4"
(V.rank_to_string resolution.P.observed.P.cel.Cel.rank);
Alcotest.(check string) "observed colour is white, overriding the season's own green" "white"
(Col.to_string resolution.P.observed.P.cel.Cel.colour);
Alcotest.(check bool) "observed subject is Bvm" true (resolution.P.observed.P.cel.Cel.subject = Sub.Bvm);
Alcotest.(check (list (pair string string))) "Holy Machabees still rides along as an ordinary commemoration"
[ ("holy-machabees", "Ordinary") ]
(List.map
(fun (c, p) -> (S.to_string c.P.cel.Cel.slug, match p with P.Privileged -> "Privileged" | P.Ordinary -> "Ordinary"))
resolution.P.commemorations);
Alcotest.(check int) "nothing omitted" 0 (List.length resolution.P.omitted)
(* The other half of the SAME hazard: a real Saturday where RG 78's own
protasis is FALSE (a genuine sanctoral feast wins the day outright), so
the BVM-Saturday-shaped temporal candidate must lose, and be OMITTED under
RG 26 (a temporal Class4 non-vigil loser is never commemorated) exactly
like the plain ferial candidate it replaces always was -- proving [band]/
[disposition] need no special case for this office at all, the design
claim the task report makes explicitly. 12 September 2026 is a Saturday
where `most-holy-name-of-mary` (data/ef/adjustments.sexp's own [Set_subject
Bvm] entry, Class3 Feast) wins outright -- ALSO tagged [subject = Bvm],
the one live data witness that could in principle confuse a
subject-keyed disposition rule with RG 26's own rank-keyed one: this test
proves they do NOT collide (RG 26 fires first, unconditionally, on the
loser's own [rank = Class4], never reaching a subject comparison at all). *)
let test_bvm_saturday_loses_to_a_real_feast_end_to_end () =
let date = mk 2026 9 12 in
let day_ctx = ctx date in
let bvm_saturday = of_temporal date in
let name_of_mary =
cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Bvm ~layer:PE.universal_layer
"most-holy-name-of-mary"
in
let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ name_of_mary ] in
Alcotest.(check string) "Most Holy Name of Mary is observed, not the BVM Saturday office"
"most-holy-name-of-mary" (S.to_string resolution.P.observed.P.cel.Cel.slug);
Alcotest.(check (list string)) "nothing is commemorated" []
(List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations);
Alcotest.(check (list (pair string string)))
"the BVM-Saturday-shaped temporal candidate is omitted under RG26, not commemorated -- no Bvm-vs-Bvm \
confusion with RG112(a)-style subject logic"
[ ("ef-time-after-pentecost-15-saturday", "omitted: yielded to a higher day") ]
(List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted)
(* RG112(d), fix round 1 (coordinator finding F1) -- PE.marian_slugs's own
citation has the full argument. End-to-end through the REAL pipeline
(PE.band, PE.disposition AND PE.admit together): 16 July 2033, a real
otherwise-unoccupied Saturday, with the real data/ef/sanctoral.sexp
[our-lady-of-mt-carmel] entry as the day's only competing candidate.
Before this fix, [resolution.P.commemorations] held
[our-lady-of-mt-carmel:Ordinary] (confirmed live against real 2005/2033
`colitur day` output before this fix existed); after, it is empty and
Mt Carmel is reported omitted, not silently dropped. *)
let test_bvm_saturday_excludes_mt_carmel_end_to_end () =
let date = mk 2033 7 16 in
let day_ctx = ctx date in
let bvm_saturday = of_temporal date in
let mt_carmel =
cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint
~layer:PE.universal_layer "our-lady-of-mt-carmel"
in
let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ mt_carmel ] in
Alcotest.(check string) "the BVM Saturday office is observed"
"ef-time-after-pentecost-5-saturday" (S.to_string resolution.P.observed.P.cel.Cel.slug);
Alcotest.(check (list string)) "Mt Carmel is NOT commemorated (RG112(d))" []
(List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations);
Alcotest.(check (list (pair string string))) "Mt Carmel is reported omitted, not silently dropped"
[ ("our-lady-of-mt-carmel", "omitted: yielded to a higher day") ]
(List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted)
(* RG 16(a) (register §6.0) reached end-to-end through the REAL pipeline
([PE.band], [PE.disposition] AND [PE.admit] together, not any one of
them in isolation as this file's own hand-tagged rows test them
separately): a real civil date this task's own blast-radius measurement
names as a live instance of the bug this branch fixes -- 6 August 2028
is a Sunday (independently checked with `date -d 2028-08-06 +%A`, the
same cross-check discipline [test_golden.ml]'s own header requires),
Time after Pentecost week 9, and the Transfiguration (6 Aug, II class,
"IN TRANSFIGURATIONE D. N. I. C.") falls on it. Before this fix
`colitur day` emitted this Sunday as an admitted
[+ef-time-after-pentecost-sunday-9] commemoration on that date (register
§6.0's own reproduction).
Fix round 1 (item 2): a THIRD candidate, Pope Sixtus II et al. (real
slug/rank/status, 6 August, unrelated to the Sunday), is now also
offered, so this test exercises BOTH fixes at once, the same real shape
[test_golden.ml]'s own 2028-08-06 pin does: [observed] is the
Transfiguration ({!band} entry 14 already did this correctly before this
task); the Sunday is [disposition]-level [Omit] (RG 16(a)); Sixtus is
[disposition]-level [Commemorate], but [admit] then excludes him too --
RG 111(b)'s rank floor, reached only because [admit] reads the day's
Sunday-ness off [~temporal] (the [sunday] candidate passed to
[P.resolve]), not off [observed] (the Transfiguration, whose own slug
carries no Sunday marker). [commemorations] is EMPTY, and BOTH losers
land in [omitted] with their own distinct reasons -- the Sunday via
[disposition]'s own Omit ("omitted: yielded to a higher day"), Sixtus via
[admit]'s cut ("omitted: admission limit reached") -- proving the two
fixes operate through genuinely different mechanisms, not the same one
coincidentally producing the same string. *)
let test_rg16a_lord_feast_suppresses_sunday_end_to_end () =
let date = mk 2028 8 6 in
let day_ctx = ctx date in
let sunday = of_temporal date in
let transfiguration =
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"transfiguration-of-our-lord"
in
let sixtus =
cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~layer:PE.universal_layer
"pope-sixtus-ii-felicissimus-and-agapitus-martyrs"
in
let resolution =
P.resolve real_rules day_ctx ~temporal:sunday ~sanctoral:[ transfiguration; sixtus ]
in
Alcotest.(check string) "the Transfiguration is observed, not the Sunday"
"transfiguration-of-our-lord" (S.to_string resolution.P.observed.P.cel.Cel.slug);
Alcotest.(check int)
"no commemorations at all -- RG16(a)'s own \"nulla fit commemoratio\" AND RG111(b)'s rank floor, both live"
0 (List.length resolution.P.commemorations);
Alcotest.(check (list (pair string string)))
"both losers omitted, via two DIFFERENT mechanisms: the Sunday at disposition-level (RG16a), Sixtus at \
admit's own cut (RG111b)"
[ ("ef-time-after-pentecost-sunday-9", "omitted: yielded to a higher day");
("pope-sixtus-ii-felicissimus-and-agapitus-martyrs", "omitted: admission limit reached") ]
(List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted
|> List.sort compare)
(* End-to-end proof of BOTH fixes this task adds together, {!P.resolve} in
full (band picks the winner, disposition and admit decide the loser's
fate), not just the two isolated functions [cases]/[disposition_cases]
already prove: 13 January 2019, the latest possible date Holy Family can
fall on (Epiphany itself a Sunday) and one of the seven years it
coincides with the fixed Commemoration of the Baptism of the Lord.
{!band}'s [entry_14_movable_band] (RG 91 entry 14, "primum mobilia") must
pick Holy Family as [observed], not the fixed Baptism candidate (which,
absent that fix, would win outright via {!compare_by}'s own alphabetical
tie-break at the shared literal 14 -- backwards from the primary text);
{!PE.disposition}'s new RG 112(a) branch must then Omit the Baptism
entirely, not merely demote it to a commemoration -- confirmed against
BOTH the general rubric (RG 112(a)) and the Holy Family Mass propers' own
more specific 13-January note (this file's own citation on the
RG112(a) [disposition_cases] rows above), and independently against
missalemeum's own JSON for this exact date (register §6.0): title "The
Holy Family: Jesus, Mary & Joseph", commemorations [], the Baptism
listed only under "displaced". *)
let test_holy_family_excludes_baptism_end_to_end () =
let date = T.holy_family_sunday 2019 in
Alcotest.(check string) "sanity: this really is 13 January 2019" "2019-01-13" (D.to_iso8601 date);
let day_ctx = ctx date in
let holy_family = of_temporal date in
let baptism =
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"commemoration-of-the-baptism-of-the-lord"
in
let resolution = P.resolve real_rules day_ctx ~temporal:holy_family ~sanctoral:[ baptism ] in
Alcotest.(check string) "Holy Family is observed, not the fixed Baptism (RG91 e14: movable first)"
"ef-time-after-epiphany-sunday-1" (S.to_string resolution.P.observed.P.cel.Cel.slug);
Alcotest.(check string) "II class" "class-2" (V.rank_to_string resolution.P.observed.P.cel.Cel.rank);
Alcotest.(check int)
"no commemorations at all -- RG112(a)'s own exclusion, not merely dropped by an admission count"
0 (List.length resolution.P.commemorations);
Alcotest.(check (list string)) "the Baptism is OMITTED, not merely uncommemorated"
[ "commemoration-of-the-baptism-of-the-lord" ]
(List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.omitted)
(* Fix round 1 (coordinator finding 1), end-to-end proof of the
[entry_14_movable_band] fix itself: a genuine I-class candidate landing
inside Holy Family's own 7-13 January window (a synthetic proper/indult
feast, standing in for the diocesan-overlay shape {!band}'s own new
comment names as the live risk) must be OBSERVED and must NOT transfer
-- reproducing, at the [Precedence.resolve] level, the exact regression
the coordinator demonstrated against the pre-fix [-14] sentinel ("the
I-class feast displaced and transferred to the next day"). With the fix,
{!band}'s entry 11 (110) beats {!PE.entry_14_movable_band} (139)
outright, so the I-class candidate wins the day, and Holy Family itself
-- losing, is_temporal, its own slug carrying the "-sunday" marker --
falls to RG 109(a)'s ordinary privileged-Sunday-commemoration path
(disposition's final catch-all, [privilege_of]'s (a) branch), the same
treatment ANY genuine feast outranking an ordinary Sunday gets: neither
omitted nor transferred, a Privileged commemoration. *)
let test_class1_feast_inside_holy_familys_window_end_to_end () =
let date = mk 2026 1 11 in
Alcotest.(check string) "sanity: this is Holy Family's own date in 2026" "2026-01-11" (D.to_iso8601 date);
let day_ctx = ctx date in
let holy_family = of_temporal date in
let synthetic_class1 =
cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-synthetic-class1-probe"
in
let resolution = P.resolve real_rules day_ctx ~temporal:holy_family ~sanctoral:[ synthetic_class1 ] in
Alcotest.(check string) "the I-class candidate is observed, NOT Holy Family (RG91 e11 beats e14-movable)"
"ef-synthetic-class1-probe" (S.to_string resolution.P.observed.P.cel.Cel.slug);
Alcotest.(check (list string)) "nothing transfers -- the pre-fix bug moved the I-class feast to the next day"
[] (List.map (fun c -> S.to_string c.P.cel.Cel.slug) resolution.P.deferred);
(match resolution.P.commemorations with
| [ (c, P.Privileged) ] ->
Alcotest.(check string)
"Holy Family itself is privileged-commemorated (RG109(a), a Sunday-shaped office \
outranked by a genuine feast), neither omitted nor transferred"
"ef-time-after-epiphany-sunday-1" (S.to_string c.P.cel.Cel.slug)
| other ->
Alcotest.failf "expected exactly one Privileged commemoration (Holy Family), got %d"
(List.length other))
(* Most Holy Name of Jesus (RG 17(a)) -- the SAME two proofs immediately
above, restated for Holy Name instead of Holy Family. UNLIKE Holy Family,
there is no LIVE collision to exercise here (this file's own [cases]/
[disposition_cases] rows never construct one, because
data/ef/sanctoral.sexp has nothing fixed-date 2-5 January besides
[telesphorus-pope-and-martyr] (5 Jan, Commemoration_only, so it can never
contest [observed] at all -- {!P.resolve}'s own partition holds it out of
the band contest entirely) -- register §6.2's own account: "no live
collision exists for it in this codebase's current data". These two
tests are the SYNTHETIC proof the task brief asked for instead: that
{!band}'s [entry_14_movable_band]/RG 112(a) mechanism, ALREADY proved
above for Holy Family, governs Holy Name identically, since neither
{!band} nor {!disposition} key on anything Holy-Family-specific (only on
[subject]/[rank]/[origin], which [temporal_ef.ml]'s fix now sets
identically for both feasts) -- not a live-data regression test, a
mechanism-generalises-correctly one, the same distinction this file's own
header draws between a table row and an end-to-end proof.
Run against BOTH of RG 17(a)'s own shapes -- the Sunday (2026-01-04) and
the 2 January fallback (2029-01-02, {!T.holy_name_fallback_date}) --
because [temporal_ef.ml]'s own fallback-branch comment claims both band
identically at {!PE.entry_14_movable_band} (ONE feast, not two
differently-classified ones); a table proving the SAME two outcomes hold
for both dates is direct evidence for that specific claim, not merely
for the Sunday shape colitur already had a name for before this task. *)
let holy_name_probe_dates =
[ ("Sunday shape (2026-01-04)", mk 2026 1 4); ("fallback shape (2029-01-02)", T.holy_name_fallback_date 2029) ]
let test_holy_name_excludes_synthetic_lord_end_to_end () =
List.iter
(fun (label, date) ->
let holy_name = of_temporal date in
Alcotest.(check bool) (label ^ ": sanity -- temporal_ef really did tag this Lord") true
(holy_name.P.cel.Cel.subject = Sub.Lord);
let day_ctx = ctx date in
let synthetic_lord =
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"ef-synthetic-fixed-lord-probe"
in
let resolution = P.resolve real_rules day_ctx ~temporal:holy_name ~sanctoral:[ synthetic_lord ] in
Alcotest.(check string) (label ^ ": Holy Name is observed (RG91 e14: movable first)")
(S.to_string holy_name.P.cel.Cel.slug) (S.to_string resolution.P.observed.P.cel.Cel.slug);
Alcotest.(check int) (label ^ ": no commemorations at all -- RG112(a)'s own exclusion") 0
(List.length resolution.P.commemorations);
Alcotest.(check (list string)) (label ^ ": the synthetic Lord candidate is OMITTED, not commemorated")
[ "ef-synthetic-fixed-lord-probe" ]
(List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.omitted))
holy_name_probe_dates
(* A REAL asymmetry this test found between the two shapes, kept explicit
rather than papered over by forcing one uniform expectation on both --
RG 109(a)'s privilege is textually "of a SUNDAY" (docs/research/rules-
register.md §4), read here via {!PE.is_sunday_slug}'s own substring test:
the Sunday shape's slug ("ef-holy-name-sunday") carries the marker and
the fallback shape's ("ef-holy-name") genuinely does not, because 2
January genuinely is not a Sunday. So a losing Holy-Name-Sunday is
PRIVILEGED (RG109(a)) and survives RG111(a)'s "I class: none save one
privileged" cap when it loses to a higher day; a losing Holy-Name-
fallback has no RG109(a)-(f) category to claim (not a Sunday, not I
class, none of (c)-(f) either) and so is only ORDINARY -- which
RG111(a)'s own cap admits NONE of on a I-class day. Both outcomes are
individually correct readings of RG 109/111's own closed lists, not a
defect in either shape -- first observed as a test failure while writing
this pair (the original version of this test asserted one Privileged
commemoration for BOTH shapes uniformly and reddened here), traced to
this citation rather than forced to pass. *)
let test_class1_feast_inside_holy_name_window_end_to_end () =
List.iter
(fun (label, date, expect_privileged_commemoration) ->
let holy_name = of_temporal date in
let day_ctx = ctx date in
let synthetic_class1 =
cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-synthetic-class1-probe-hn"
in
let resolution = P.resolve real_rules day_ctx ~temporal:holy_name ~sanctoral:[ synthetic_class1 ] in
Alcotest.(check string) (label ^ ": the I-class candidate is observed, NOT Holy Name (RG91 e11 beats e14-movable)")
"ef-synthetic-class1-probe-hn" (S.to_string resolution.P.observed.P.cel.Cel.slug);
Alcotest.(check (list string)) (label ^ ": nothing transfers") []
(List.map (fun c -> S.to_string c.P.cel.Cel.slug) resolution.P.deferred);
if expect_privileged_commemoration then
match resolution.P.commemorations with
| [ (c, P.Privileged) ] ->
Alcotest.(check string)
(label ^ ": Holy Name itself is privileged-commemorated (RG109(a)), neither omitted nor transferred")
(S.to_string holy_name.P.cel.Cel.slug) (S.to_string c.P.cel.Cel.slug)
| other ->
Alcotest.failf "%s: expected exactly one Privileged commemoration (Holy Name), got %d" label
(List.length other)
else
Alcotest.(check int)
(label ^ ": RG111(a) admits none -- Holy Name's own commemoration here is Ordinary \
(no RG109(a)-(f) category applies to a non-Sunday loser), and a I-class day admits \
only a Privileged one")
0 (List.length resolution.P.commemorations))
[ ("Sunday shape (2026-01-04)", mk 2026 1 4, true);
("fallback shape (2029-01-02)", T.holy_name_fallback_date 2029, false) ]
(* Completes Task 7's carried fix (RG 91 entry 8, §4): on a real Sunday
landing on 2 November, All Souls does not merely lose (that was Task 7's
[band] fix, proved by [test_all_souls_yields_to_sunday] above) -- it must
be TRANSFERRED, not commemorated and not omitted. All Souls is I class
(RG 91 entry 8's own [rank] field, untouched by the Sunday-exception band
bump -- see precedence_ef.ml's comment on entry 8), so RG 95's rank
condition alone should route it to [Transfer]. *)
let test_all_souls_disposition_is_transfer () =
let sunday = an_ordinary_sunday in
let all_souls = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls" in
Alcotest.(check string) "All Souls loses to a Sunday and transfers"
"Transfer"
(string_of_disposition (PE.disposition ~winner:sunday ~loser:all_souls))
(* The same fact, proved end-to-end through [Precedence.resolve] with the
REAL [PE.band] and REAL [PE.disposition] wired together (Task 7's own
integration test above still stubs [disposition] to a constant [Omit],
which is exactly what this task must not leave true) -- All Souls must
land in [deferred], not [commemorations] or [omitted]. WHERE it is placed
(3 November, RG 96) is [Rite.transfer_target]'s job, out of this task's
scope; this only proves [resolve] hands it to the transfer path at all. *)
let test_all_souls_transfers_end_to_end () =
let date = mk 2025 11 2 in
let day_ctx = ctx date in
let sunday =
{ P.cel =
Cel.make ~slug:(S.of_string_exn "ef-time-after-pentecost-sunday-x") ~rank:V.Class2
~colour:Col.Green ~subject:Sub.Temporal ~layer:"temporal" ();
origin = P.Temporal }
in
let all_souls = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls" in
let rules =
{ P.band = PE.band; disposition = PE.disposition;
admit = (fun ~observed:_ ~temporal:_ cs -> List.map (fun (c, p, (_ : int)) -> (c, p)) cs) }
in
let resolution = P.resolve rules day_ctx ~temporal:sunday ~sanctoral:[ all_souls ] in
Alcotest.(check (list string)) "All Souls is deferred (transferred), not omitted or commemorated"
[ "ef-all-souls" ]
(List.map (fun c -> S.to_string c.P.cel.Cel.slug) resolution.P.deferred);
Alcotest.(check int) "nothing commemorated" 0 (List.length resolution.P.commemorations);
Alcotest.(check int) "nothing omitted" 0 (List.length resolution.P.omitted)
(* Task 11: [PE.transfer_target] -- RG 96 ("the next following day that is
not I or II class") plus its Annunciation exception. [occupant] is a
synthetic callback ({!Colitur_kernel.Rite.t.transfer_target}'s own
[occupant] parameter), not a real [Calendar]-driven one -- the CLI's own
end-to-end proof (colitur day, All Souls landing on 3 Nov 2025 and the
Annunciation landing on 5 Apr 2027, see test/cli.t and the task report)
is what wires this against real data; these rows isolate the search
function itself. *)
(* [blocked] returns Class1 (blocking) for exactly the listed dates, Class4
(not blocking) everywhere else -- enough to exercise [is_blocking]'s own
two-way test (RG 96 speaks of I OR II class; Class1 alone is enough to
prove the blocking side, [test_transfer_target_terminates...] below adds
nothing by varying it further). *)
let occupant_blocking_on blocked_dates (d : D.t) : V.rank Cel.t =
let blocking = List.exists (fun bd -> D.compare bd d = 0) blocked_dates in
Cel.make ~slug:(S.of_string_exn "occupant") ~rank:(if blocking then V.Class1 else V.Class4)
~colour:Col.Green ~layer:"synthetic" ()
let occupant_always_blocking (_ : D.t) : V.rank Cel.t =
Cel.make ~slug:(S.of_string_exn "occupant") ~rank:V.Class1 ~colour:Col.Green ~layer:"synthetic" ()
(* General RG 96 search: two consecutive blocked days past [origin], proving
the search walks past MORE than one ineligible day rather than only
trying [origin + 1] and stopping (the same shape Calendar's own
synthetic fixture pins for the abstraction -- this pins it for the real
EF search function). *)
let test_transfer_target_general_multi_step_search () =
let origin = mk 2026 1 10 in
let occupant = occupant_blocking_on [ mk 2026 1 11; mk 2026 1 12 ] in
let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-some-i-class-feast" in
let target = PE.transfer_target c origin occupant in
Alcotest.(check string) "lands on the first day past the blocked run"
"2026-01-13" (D.to_iso8601 target)
(* Coordinator review (fix round 1): RG 96 Attamen (a) (register-transcribed,
primary-source-verified) makes the Annunciation exception CONDITIONAL on
the general RG 96 walk carrying the feast past Easter Sunday -- NOT
unconditional as the first transcription had it. The occupant here blocks
every day from [origin + 1] through the day after Easter (26 March - 6
April 2026 inclusive), so the GENERAL target itself would land at 7
April -- after Easter (5 April) -- which is exactly the trigger
condition, not merely "the Annunciation is impeded at all". *)
let test_transfer_target_annunciation_starts_at_monday_after_low_sunday () =
let origin = mk 2026 3 25 in
let easter_2026 = Comp.gregorian_easter 2026 in
let blocked_through_day_after_easter =
let rec range a b = if D.compare a b > 0 then [] else a :: range (D.add_days a 1) b in
range (D.add_days origin 1) (D.add_days easter_2026 1)
in
let occupant = occupant_blocking_on blocked_through_day_after_easter in
let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer PE.annunciation_slug in
let target = PE.transfer_target c origin occupant in
let monday_after_low_sunday = D.add_days easter_2026 8 in
Alcotest.(check string) "lands on the Monday after Low Sunday (Easter + 8), the general \
walk having crossed Easter itself"
(D.to_iso8601 monday_after_low_sunday) (D.to_iso8601 target);
Alcotest.(check bool) "NOT the general target (2 days after Easter, discriminates the branch)"
true
(D.compare target (D.add_days easter_2026 2) <> 0)
(* RG 96's own qualifier on the exception -- "searching onward from there
only if that day is itself blocked" (rite.mli) -- is [search_from]'s
ORDINARY behaviour, not a second mechanism: same blocked run as above
(forcing the general target past Easter, so the exception fires), PLUS
the Monday after Low Sunday itself blocked, confirming the search
continues exactly one more day from there. *)
let test_transfer_target_annunciation_searches_onward_if_blocked () =
let origin = mk 2026 3 25 in
let easter_2026 = Comp.gregorian_easter 2026 in
let monday_after_low_sunday = D.add_days easter_2026 8 in
let blocked =
let rec range a b = if D.compare a b > 0 then [] else a :: range (D.add_days a 1) b in
range (D.add_days origin 1) (D.add_days easter_2026 1) @ [ monday_after_low_sunday ]
in
let occupant = occupant_blocking_on blocked in
let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer PE.annunciation_slug in
let target = PE.transfer_target c origin occupant in
Alcotest.(check string) "searches onward one more day when that Monday is itself blocked"
(D.to_iso8601 (D.add_days monday_after_low_sunday 1)) (D.to_iso8601 target)
(* THE REGRESSION PIN (coordinator review): the bug an unconditional
exception produced. 25 March 2057 is Lent III Sunday (I class, RG 91
entry 6), impeding the Annunciation; 26 March 2057 is an ordinary Lent
feria (III class, well before Easter, 22 April 2057) -- the general RG
96 target. The general target does NOT fall after Easter, so the
exception must NOT fire: the Annunciation lands on 26 March, not 13
April (Easter + 8), which is what the unconditional reading produced
(verified by reverting the fix and re-running this exact test -- see the
task report's mutation record). Uses the REAL [Temporal_ef.temporal] as
[occupant] (not a synthetic stand-in), the same coupling-safety
convention [of_temporal]'s callers use elsewhere in this file, so this
is also effectively an end-to-end check of the real 2057 calendar
shape, not just the search's own logic in isolation. *)
let test_transfer_target_annunciation_not_overridden_when_general_target_precedes_easter () =
let origin = mk 2057 3 25 in
Alcotest.(check string) "25 March 2057 is a Sunday (Lent III)" "sunday"
(D.weekday_to_string (D.weekday origin));
let occupant d = (T.temporal d).Colitur_kernel.Temporal.office in
let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer PE.annunciation_slug in
let target = PE.transfer_target c origin occupant in
Alcotest.(check string) "lands on 26 March 2057 (the general RG96 target), NOT the \
Annunciation exception's Monday after Low Sunday"
"2057-03-26" (D.to_iso8601 target)
(* rite.mli's own obligations on [transfer_target] (Task 11 brief): the call
must TERMINATE and its result must be STRICTLY AFTER [origin], even for a
rite/data shape this function cannot have anticipated -- an occupant that
reports every single day as blocking, forever. Calendar's own round guard
(max_transfer_rounds) does not cover this: it bounds ROUNDS across a
whole year, not the internal walk one call to this function makes (see
precedence_ef.ml's own comment on [search_from] and [max_search_days]).
Deliberately NOT pinning the exact returned date against the private
[max_search_days] constant -- that would coalesce a behavioural contract
(terminates, makes forward progress) with an internal tuning value this
function is free to change; a generous, test-owned ceiling (1000 days,
comfortably past any realistic bound) is enough to prove termination is
genuine and not merely "didn't hang during this particular run". *)
let test_transfer_target_terminates_under_pathological_occupant () =
let origin = mk 2026 1 1 in
let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-pathological-case" in
let target = PE.transfer_target c origin occupant_always_blocking in
Alcotest.(check bool) "strictly after origin" true (D.compare target origin > 0);
Alcotest.(check bool)
"terminates within a generous bound (proves the internal search is bounded, not merely lucky)"
true
(D.compare target (D.add_days origin 1000) <= 0)
(* Coordinator review: [search_from] must not probe [occupant] past
{!Date}'s own domain ceiling (31 December 9999). A SYNTHETIC occupant
(like [occupant_always_blocking] above) can never actually discriminate
this: it never calls [Computus.gregorian_easter] itself, so it cannot
raise regardless of whether the domain guard exists -- a test built on
one would only prove [search_from]'s unrelated step bound, not this fix.
[occupant] here is instead the REAL [Temporal_ef.temporal] (no sanctoral
layer needed: 29-31 Dec are ALREADY II class via [named]'s own Nativity-
octave-day entries, so three real, unbroken blocking days already sit at
the very end of the domain) -- exactly the shape that raises without the
fix: 1 January of civil year 10000 is next, and [Computus.gregorian_easter
10000] does [Date.make ~year:10000 ...] and [failwith]s (the .ml's own
[domain_max_date] comment; also how the reviewer reproduced the bug
through the project's own overlay mechanism -- see the task report for
that end-to-end reproduction). Mutation-verified: reverting the domain
guard makes this test error with exactly that uncaught [Failure], not
merely fail an assertion (see the task report). *)
let test_transfer_target_does_not_raise_at_domain_ceiling () =
let origin = mk 9999 12 28 in
let occupant d = (T.temporal d).Colitur_kernel.Temporal.office in
let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-domain-ceiling-case" in
let target = PE.transfer_target c origin occupant in
Alcotest.(check bool) "past 31 December 9999 (the guard engaged; nothing admissible remained \
in-domain, so the search gave up at the ceiling rather than crashing)"
true
(D.compare target (mk 9999 12 31) > 0)
(* RG 80 (ef-major-litanies task, NEW) -- {!PE.major_litanies_slug}'s own
citation has the full text. [occupant_always_blocking] is deliberately
PATHOLOGICAL (every day reports I-class, forever) -- for every OTHER
candidate this would send [search_from] to its own 400-day structural
ceiling ({!test_transfer_target_terminates_under_pathological_occupant}
above proves exactly that shape). Using it here is the mutation proof
this branch's own comment promises ("running search_from here would be
ACTIVELY WRONG"): if the Litanies' own early-return branch were ever
deleted or bypassed, this test would immediately land somewhere near
[origin + 1000], not Easter+2, and fail loudly -- a MUCH stronger
witness than a cooperative occupant could give, which would pass either
way by accident (Easter+2 is itself never blocking under a realistic
occupant, so a real [Temporal_ef.temporal] could not distinguish "no
search happened" from "a search happened and immediately succeeded"). *)
let test_transfer_target_major_litanies_easter_sunday_shape () =
let easter_2038 = Comp.gregorian_easter 2038 in
Alcotest.(check string) "2038: Easter Sunday IS 25 April (this file's own worked example)" "2038-04-25"
(D.to_iso8601 easter_2038);
let origin = easter_2038 in
let c =
cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer
PE.major_litanies_slug
in
let target = PE.transfer_target c origin occupant_always_blocking in
Alcotest.(check string) "lands on Easter+2 regardless of the pathological occupant (no RG96 search at all)"
(D.to_iso8601 (D.add_days easter_2038 2)) (D.to_iso8601 target);
Alcotest.(check bool) "strictly after origin (rite.mli's own obligation)" true (D.compare target origin > 0)
let test_transfer_target_major_litanies_easter_monday_shape () =
let easter_2011 = Comp.gregorian_easter 2011 in
Alcotest.(check string) "2011: Easter Sunday is 24 April, so 25 April is Easter Monday" "2011-04-24"
(D.to_iso8601 easter_2011);
let origin = D.add_days easter_2011 1 (* 25 April 2011, Easter Monday *) in
let c =
cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~rank:V.Class4 ~layer:PE.universal_layer
PE.major_litanies_slug
in
let target = PE.transfer_target c origin occupant_always_blocking in
Alcotest.(check string) "ALSO lands on Easter+2 (not Easter+3 -- both RG80 trigger shapes converge on the \
SAME offset), regardless of the pathological occupant"
(D.to_iso8601 (D.add_days easter_2011 2)) (D.to_iso8601 target);
Alcotest.(check bool) "strictly after origin (rite.mli's own obligation)" true (D.compare target origin > 0)
let suite =
( "Precedence_ef",
List.map
(fun (desc, date, c, expect) ->
Alcotest.test_case desc `Quick (fun () ->
Alcotest.(check int) desc expect (PE.band (ctx date) c)))
cases
@ [ Alcotest.test_case "8 All Souls yields to a Sunday (resolve-level)" `Quick
test_all_souls_yields_to_sunday ]
@ List.map
(fun (desc, winner, loser, expect) ->
Alcotest.test_case desc `Quick (fun () ->
Alcotest.(check string) desc expect
(string_of_disposition (PE.disposition ~winner ~loser))))
disposition_cases
@ List.map
(fun (desc, winner, loser, expect) ->
Alcotest.test_case desc `Quick (fun () ->
Alcotest.(check string) desc expect
(string_of_disposition (PE.disposition ~winner ~loser))))
privilege_cases
@ [ Alcotest.test_case "All Souls disposition is Transfer" `Quick
test_all_souls_disposition_is_transfer;
Alcotest.test_case "All Souls transfers end-to-end (resolve, real rules)" `Quick
test_all_souls_transfers_end_to_end ]
@ List.map
(fun (desc, observed, temporal, comms, expect) ->
Alcotest.test_case desc `Quick (fun () ->
Alcotest.(check (list string)) desc expect
(slugs_of (PE.admit ~observed ~temporal comms))))
admit_cases
@ [ Alcotest.test_case "admit is order-independent (III-class, 3 candidates)" `Quick
test_admit_order_independent;
Alcotest.test_case
"RG113: admit picks by precedence order, not slug, when they disagree" `Quick
test_admit_uses_precedence_not_slug_when_they_disagree;
Alcotest.test_case "I-class day: full drop reported in omitted, not vanished" `Quick
test_i_class_day_drops_into_omitted;
Alcotest.test_case "II-class Sunday: second loser dropped into omitted" `Quick
test_ii_class_sunday_drops_second_loser_into_omitted;
Alcotest.test_case "RG109(e) Lent feria privileged end-to-end" `Quick
test_privileged_lent_feria_admitted_end_to_end;
Alcotest.test_case
"II-class Sunday override: RG109(b) witness admitted over an ordinary saint, end-to-end"
`Quick test_ii_class_sunday_privileged_witness_admitted_end_to_end;
Alcotest.test_case
"RG91 e27/RG78: the BVM Saturday office wins and commemorates Holy Machabees, end-to-end \
(2026-08-01)"
`Quick test_bvm_saturday_wins_and_commemorates_end_to_end;
Alcotest.test_case
"RG91 e27/RG78/RG26: the BVM Saturday office loses to a real feast and is omitted, end-to-end \
(2026-09-12)"
`Quick test_bvm_saturday_loses_to_a_real_feast_end_to_end;
Alcotest.test_case
"RG112(d), fix round 1 (F1): the BVM Saturday office excludes Mt Carmel, end-to-end \
(2033-07-16)"
`Quick test_bvm_saturday_excludes_mt_carmel_end_to_end;
Alcotest.test_case
"RG16(a): a Feast of the Lord suppresses the Sunday's own commemoration entirely, \
end-to-end (2028-08-06)"
`Quick test_rg16a_lord_feast_suppresses_sunday_end_to_end;
Alcotest.test_case
"RG112(a) + RG91 e14 \"primum mobilia\": Holy Family excludes the Baptism of the Lord \
entirely, end-to-end (13 Jan 2019)"
`Quick test_holy_family_excludes_baptism_end_to_end;
Alcotest.test_case
"fix round 1, finding 1: a synthetic I-class candidate inside Holy Family's own window is \
observed and does not transfer (2026-01-11)"
`Quick test_class1_feast_inside_holy_familys_window_end_to_end;
Alcotest.test_case
"RG112(a) + RG91 e14 \"primum mobilia\": Holy Name of Jesus excludes a synthetic fixed Lord \
candidate entirely, both shapes (Sunday + fallback)"
`Quick test_holy_name_excludes_synthetic_lord_end_to_end;
Alcotest.test_case
"a synthetic I-class candidate inside Holy Name's own window is observed and does not \
transfer, both shapes (Sunday + fallback)"
`Quick test_class1_feast_inside_holy_name_window_end_to_end;
Alcotest.test_case "transfer_target: general RG96 search walks past more than one blocked day"
`Quick test_transfer_target_general_multi_step_search;
Alcotest.test_case "transfer_target: Annunciation exception starts at Monday after Low Sunday"
`Quick test_transfer_target_annunciation_starts_at_monday_after_low_sunday;
Alcotest.test_case
"transfer_target: Annunciation exception searches onward if that Monday is blocked" `Quick
test_transfer_target_annunciation_searches_onward_if_blocked;
Alcotest.test_case
"transfer_target: Annunciation NOT overridden when the general target precedes Easter \
(2057 regression)"
`Quick test_transfer_target_annunciation_not_overridden_when_general_target_precedes_easter;
Alcotest.test_case "transfer_target: terminates and stays forward under a pathological occupant"
`Quick test_transfer_target_terminates_under_pathological_occupant;
Alcotest.test_case "transfer_target: does not raise probing past the domain ceiling" `Quick
test_transfer_target_does_not_raise_at_domain_ceiling;
Alcotest.test_case
"transfer_target: RG80, Easter-Sunday shape -- lands on Easter+2 with NO search, even under a \
pathological occupant"
`Quick test_transfer_target_major_litanies_easter_sunday_shape;
Alcotest.test_case
"transfer_target: RG80, Easter-Monday shape -- ALSO lands on Easter+2, not Easter+3" `Quick
test_transfer_target_major_litanies_easter_monday_shape ] )
|