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
|
(* EF (1962) temporal cycle. Every boundary and rank rule cites its Rubricae
Generales paragraph; see docs/research/rules-register.md §3-§4. *)
open Colitur_kernel
open Vocab_ef
let mk y m d =
match Date.make ~year:y ~month:m ~day:d with
| Ok t -> t
| Error e -> failwith ("temporal_ef: " ^ e)
let weekday_index d =
match Date.weekday d with
| Date.Sun -> 0 | Date.Mon -> 1 | Date.Tue -> 2 | Date.Wed -> 3
| Date.Thu -> 4 | Date.Fri -> 5 | Date.Sat -> 6
(* The Sunday on or before [d]. *)
let sunday_on_or_before d = Date.add_days d (-(weekday_index d))
(* RG 20 (Caput III, "De Dominicis"), primary-source-verified (final fix
wave): "Dominica I Adventus ea est, quae cadit die 30 novembris vel est
ipsi proximior" -- Advent I Sunday is that which falls on 30 November or
is nearest to it. CORRECTED citation: this comment previously cited RG
71 for this placement rule -- WRONG, RG 71 (cited on [season] below)
states only Advent's own season BOUNDARY ("a I Vesperis dominicae I
Adventus..."), not which Sunday opens it; the register's own RG 71 entry
is a boundary citation, and the only "nearest 30 November" text there
before this fix was UNLYC nn. 39-42, the MODERN form's rule, not this
one's. Equivalently the fourth Sunday before Christmas, i.e. three weeks
before the last Sunday on or before 24 December. *)
let advent_start y = Date.add_days (sunday_on_or_before (mk y 12 24)) (-21)
let year_start = advent_start
let before a b = Date.compare a b < 0
let on_or_after a b = Date.compare a b >= 0
(* RG 71-77. Tested in chronological order within the civil year. *)
let season d =
let y = Date.year d in
let easter = Computus.gregorian_easter y in
let advent_this = advent_start y in
let christmas_this = mk y 12 25 in
let jan14 = mk y 1 14 in
let septuagesima_sunday = Date.add_days easter (-63) in
let ash_wednesday = Date.add_days easter (-46) in
let passion_sunday = Date.add_days easter (-14) in
let paschal_end = Date.add_days easter 55 in
if on_or_after d advent_this && before d christmas_this then Advent (* RG 71 *)
else if on_or_after d christmas_this then Christmastide (* RG 72-73: 25-31 Dec *)
else if before d jan14 then Christmastide (* RG 72-73: 1-13 Jan inclusive *)
else if before d septuagesima_sunday then Time_after_epiphany (* RG 77: from 14 Jan *)
else if before d ash_wednesday then Septuagesima (* RG 73 *)
else if before d passion_sunday then Lent (* RG 74 *)
else if before d easter then Passiontide (* RG 75; Holy Saturday included *)
else if Date.compare d paschal_end <= 0 then Paschaltide (* RG 76 *)
else Time_after_pentecost (* RG 77 *)
(* RG 17(d) (Caput III, "De Dominicis"), PRIMARY-SOURCE-VERIFIED (final fix
wave, closing the item register §6 previously carried as "oracle-backed,
not yet primary-verified"): "festum D. N. Iesu Christi Regis, celebrandum
dominica ultima mensis octobris" -- the feast of Our Lord Jesus Christ
the King is to be celebrated on the LAST SUNDAY OF OCTOBER. NOT the OF's
last Sunday before Advent -- a genuine EF/OF divergence, not merely a
citation gap. *)
let christ_the_king y = sunday_on_or_before (mk y 10 31)
(* RG 17(b) (Caput III, "De Dominicis"), primary text, verified against BOTH
photographic scans (missale-romanum-1962.pdf and "Missale Romanum
1962_text.pdf" -- the electronic transcription, 1962-06-23,_SS_Ioannes
_XXIII,_Missale_Romanum,_LT.pdf, carries the SAME text here, so this one
paragraph is not itself a transcription-vs-scan divergence): "17.
Dominica excludit, per se, assignationem perpetuam festorum. Excipiuntur:
a) festum Ssmi Nominis Iesu, celebrandum dominica quae occurrit a die 2
ad 5 ianuarii (secus die 2 ianuarii); b) festum S. Familiae Iesu, Mariae,
Ioseph, celebrandum dominica prima post Epiphaniam; ... Haec festa locum
tenent dominicae occurrentis cum omnibus iuribus et privilegiis: de
dominica, proinde, nulla fit commemoratio" -- a Sunday of itself excludes
the perpetual assignment of feasts to it; EXCEPTED: (a) the Most Holy
Name of Jesus, celebrated on the Sunday falling 2-5 January (otherwise 2
January); (b) the HOLY FAMILY of Jesus, Mary and Joseph, celebrated on
the FIRST SUNDAY AFTER EPIPHANY; ... these feasts hold the place of the
occurring Sunday with ALL its rights and privileges: of the Sunday,
therefore, NO commemoration is made -- the identical "cum omnibus
iuribus et privilegiis: nulla fit commemoratio" formula RG 16(a) already
uses for a FIXED-date Lord feast impeding a Sunday (register §6.0),
stated here for the Sunday-ASSIGNED feasts RG 17 itself lists by letter.
UNLIKE (a)'s own narrow 2-5 January window (which CAN be empty of a
Sunday -- the calendarium's own January table carries an explicit "vel ea
deficiente, die 2 ianuarii" fallback for it, scan-verified), (b)'s 7-13
January window can NEVER be empty: [sunday_on_or_before (mk y 1 6)] is,
by construction, at most 6 days before 6 January, so adding 7 always
lands in [7, 13] regardless of which weekday 6 January falls on (every
one of the 7 possible cases is exercised by test_temporal_ef.ml's own
[test_holy_family]). The calendarium's own text for (b), both in RG 17
itself and in its January table entry ("Dominica I post Epiphaniam:
S. Familiae, Iesu, Mariae, Ioseph, II classis"), carries no fallback
clause of any kind -- consistent with there being no gap for one to
cover.
Formula IDENTICAL to [week_origin]'s own [Time_after_epiphany] case below
(the first Sunday after Epiphany) -- not re-derived a second time, just
named here for its own citation and so [temporal] can test a specific
date against it without reaching into [week_origin]'s implementation. *)
let holy_family_sunday y = Date.add_days (sunday_on_or_before (mk y 1 6)) 7
(* RG 17(a) (Caput III, "De Dominicis"; full text on [holy_family_sunday]'s
own citation above): "festum Ss.mi Nominis Iesu, celebrandum dominica quae
occurrit a die 2 ad 5 ianuarii (secus die 2 ianuarii)" -- the feast of the
Most Holy Name of Jesus, celebrated on the Sunday falling 2-5 January,
OTHERWISE [on] 2 January [itself]. [None] here means no such Sunday exists
that year -- [holy_name_fallback_date]'s own fallback branch is for
exactly that case; see its own citation for why the window CAN be empty
(unlike (b)'s 7-13 January window, which never is).
Formula: the Sunday on or before 5 January is in [2,5] iff it is not
before 2 January -- the window is only 4 days wide, so there is at most
one Sunday in it either way. *)
let holy_name_sunday y =
let s = sunday_on_or_before (mk y 1 5) in
if on_or_after s (mk y 1 2) then Some s else None
(* RG 17(a)'s own "secus die 2 ianuarii" clause, and the calendarium's
January table, BOTH photographic scans, word for word (docs/research/
rules-register.md; also confirmed in the Mass propers' own heading for
this feast, both scans, Caput "Sanctissimi Nominis Iesu": "DOMINICA a die
2 ad diem 5 ianuarii occurrenti, VEL, SI HAEC DEFECERIT, DIE 2 IANUARII" --
the SUNDAY occurring 2-5 January, OR, IF THIS IS LACKING, [on] 2 January):
*"Dominica inter octavam Nativitatis Domini et Epiphaniam, vel, EA
DEFICIENTE, die 2 ianuarii: Sanctissimi Nominis Iesu, II classis"* -- the
Sunday between the Octave of the Nativity and Epiphany, or, THAT FAILING,
2 January. Unlike [holy_family_sunday]'s 7-13 January window (which can
never be empty of a Sunday), this 2-5 January window CAN be, and is in
3,619 of the 8,417 years in [1583, 9999] (a plain weekday check,
independently cross-checked against `date -d`) -- [temporal]'s own
dispatch applies this fallback date, tagged exactly like the Sunday shape
(subject Lord, the same Latin name), only when [holy_name_sunday y] is
[None] for that date's own civil year. *)
let holy_name_fallback_date y = mk y 1 2
(* The ONE Latin title both of RG 17(a)'s two shapes (the Sunday and the 2
January fallback) carry -- ONE feast, RG 91's own "primum mobilia, deinde
fixa" language notwithstanding (see [temporal]'s own fallback-branch
comment for why both shapes band identically at entry 14's MOVABLE half,
not split across the two halves by year). Bound once so the two [temporal]
branches that use it cannot silently drift apart, the same discipline
[holy_family_sunday] already gets from being named instead of re-derived.
Both photographic scans, word for word, twice over (the calendarium's own
January table AND the Mass propers' own running header/heading, this
file's own citations on [holy_name_sunday]/[holy_name_fallback_date]
above): "Sanctissimi Nominis Iesu". No English name, deliberately -- the
SAME "would read the oracle it is compared against" reasoning
[holy_family_sunday]'s own [names] comment below gives for Holy Family. *)
let holy_name_names =
Colitur_kernel.Names.of_list [ (Colitur_kernel.Lang.of_string_exn "la", "Sanctissimi Nominis Iesu") ]
(* RG 91 entry 2 (Sacred Triduum) ranks Holy Thursday/Good Friday/Holy
Saturday above even entry 7's own I-class ferias (this file's own comment
on [privileged_feria]), but [temporal] constructs no proper NAMED office
for any of the three (ef-triduum-litanies task, Gap 1) -- they fall
through to the generic Passiontide ferial branch below like any other day
of Holy Week. The slug STAYS that generic key: RG 91 entry 2 is already
identified structurally, off the day's own Easter offset and rank
(Precedence_ef.band's own entry-2 branch), never off the slug, so
renaming it would touch precedence for no reason and would break the
lectio differential's own slug mapping for these three lectionary keys
(the same reasoning [holy_family_sunday]'s own citation above gives for
keeping "ef-time-after-epiphany-sunday-1"). What was actually missing is
IDENTITY alone, closed the same way RG 17(b)/(a) (Holy Family/Holy Name,
above) already were: Latin, not English, in [names] -- zero circularity
(an English name would mean reading missalemeum's own title text, the
oracle this exact axis is compared against, to decide colitur's own
"ground truth").
Both photographic scans, word for word, corroborated by the electronic
transcription's own table-of-contents-style listing at the identical
three headings (docs/research/rules-register.md; three-way agreement, no
scan-vs-transcription conflict to adjudicate here -- these three headings
sit OUTSIDE the Mass-propers body text the transcription is documented
elsewhere as missing almost all of, in a title/running-header line it
carries just the same): "FERIA QUINTA IN CENA DOMINI" / running header
"Feria V in Cena Domini" (Holy Thursday); "FERIA SEXTA / IN PASSIONE ET
MORTE DOMINI" (Good Friday -- NOT the informal "Feria VI Parasceve" the
transcription uses in passing elsewhere in an unrelated rubric; this is
the Missal's own Mass-propers section title, confirmed as the running
header on every page of that Office in both scans); "SABBATO SANCTO"
(Holy Saturday, both scans, with "I classis" printed directly beneath the
heading).
[subject = Lord] is also set (Holy Family/Holy Name needed it only
because Precedence_ef.band's entry 14 reads [subject] to outrank a
colliding fixed feast; entry 2 here needs no such thing --
Precedence_ef.band's own entry-2 branch tests only [rank] and the Easter
offset). Tagged anyway because it is simply true (the Last Supper, the
Passion and Death, the entombment are textbook mysteries of the Lord) and
safe: Precedence_ef.disposition's RG 112(a) branch (a mystery of the Lord
excluding a commemoration of ANOTHER mystery of the same Divine Person)
only fires when BOTH sides of an occurrence are tagged [Lord], and no
[Lord]-subject entry in data/ef/sanctoral.sexp has a fixed civil date
inside Holy Week's own movable range (earliest 19 March, latest 24 April;
the register's own subject audit lists exactly six [Lord]-tagged
entries, all fixed in January, February, JULY, August, September or
November -- the Precious Blood, 1 July, was omitted from this list until
the fix-round review counted six entries against five months)
-- and any [Class1] sanctoral entry that DOES land there (e.g. a
transferred Annunciation) reaches Precedence_ef.disposition's EARLIER,
subject-blind "I class, not a Sunday -> Transfer" branch first
regardless, so RG 112(a)'s branch is never reached for it either way.
Measured, not merely argued: this task's own full 1583-9999 blast-radius
sweep (git archive, pre- vs post-change) shows zero
[observed]/[commemorations]/[transferred_*] difference traceable to this
tag anywhere in the domain -- see the task report. *)
(* SOURCE NOTE on the three literals below (corrected, fix-round review):
each is the Missal's own RUNNING HEADER for that Office, which is the
convention every other Latin name in this file already follows
([holy_family_sunday], [holy_name_sunday]). Counted across both
photographic scans:
"Feria V in Cena Domini" heading FERIA QUINTA IN CENA
DOMINI; running header 1:1
"Feria VI in Passione et Morte Domini" heading FERIA SEXTA / IN
PASSIONE ET MORTE DOMINI;
running header 1:1. NOT
"Feria VI Parasceve", which
occurs ZERO times as a title in
either scan -- every occurrence
of Parasceve is inside the
Johannine Passion text. RG
132(a) writes "feriae VI in
Passione et Morte Domini".
"Sabbato sancto" heading SABBATO SANCTO (1x);
running header "Sabbato sancto"
28x / 30x.
The last was previously shipped as "Sabbato Sancto", a third casing
attested in NEITHER scan -- it appears only in the electronic
transcription's own table of contents, i.e. the source this project's
methodology rule deprecates -- while the comment called it "both
photographic scans, word for word". Recased to the running-header form
the other names use. *)
let triduum_names = function
| -3 -> Some (Colitur_kernel.Names.of_list [ (Colitur_kernel.Lang.of_string_exn "la", "Feria V in Cena Domini") ])
| -2 ->
Some
(Colitur_kernel.Names.of_list
[ (Colitur_kernel.Lang.of_string_exn "la", "Feria VI in Passione et Morte Domini") ])
| -1 -> Some (Colitur_kernel.Names.of_list [ (Colitur_kernel.Lang.of_string_exn "la", "Sabbato sancto") ])
| _ -> None
let same a b = Date.compare a b = 0
(* Named temporal days: the I-class feasts of the Lord (RG 91 entries 1 and 3),
the Vigil and Octave Day of the Nativity and the Vigil of Pentecost (RG
28-34, RG 91 entries 5 and 9), the I-class Sundays of Passiontide and Low
Sunday (RG 91 entry 6), Ash Wednesday (RG 91 entry 7), and the days within
the Octave of the Nativity (RG 63-70, RG 91 entry 17).
Returns (season, slug, colour, rank). Deliberately NOT week: an earlier
version carried an explicit week option here, hand-set on some branches
(Passion/Palm Sunday, Easter, Low Sunday, Pentecost and its Vigil, Christ
the King) and left at [None] on others (Ascension and its Vigil, Corpus
Christi, Sacred Heart) even though those sit inside a numbered season run
just the same -- a manual-convention bug the guarding property could not
even detect (see test_temporal_ef.ml's history). [temporal] now calls
[week] itself for every day, named or not, which makes "a named day inside
a run carries that run's week" hold by construction instead of by
remembering to set it here. *)
let named d =
let y = Date.year d in
let easter = Computus.gregorian_easter y in
let off n = Date.add_days easter n in
let m = Date.month d and dd = Date.day d in
if m = 12 && dd = 25 then Some (Christmastide, "ef-nativity", Colour.White, Class1)
else if m = 12 && dd = 24 then
(* RG 91 entry 5: the Vigil of the Nativity is I class. lectio has no slug
for it, so this key has no lectionary entry until Plan 4 fills it
(CORRECTED, final fix wave, item 7 -- this is the lectionary/reading-
citations bootstrap, Plan 4, not the sanctoral one, Plan 3, which
already shipped in this branch). *)
Some (Advent, "ef-nativity-vigil", Colour.Violet, Class1)
else if m = 12 && dd >= 26 && dd <= 31 && Date.weekday d <> Date.Sun then
(* Days within the Octave of the Nativity, RG 67: "Dies infra octavam sunt
II classis". The octave runs 25 December to 1 January, so 26-31 are its
days 2-7.
CORRECTED 2026-08-18: this covered 29-31 ONLY, on the reasoning that
"26-28 Dec are Stephen, John and the Innocents, hence sanctoral". Their
OFFICE is sanctoral -- those three II-class feasts win the day -- but
they remain DAYS WITHIN THE OCTAVE, and the calendarium directs a
commemoration of the octave under each, verbatim (LT.txt:5454-5459):
26 S. STEPHANI PROTOMARTYRIS, II classis. Com. octavae Nativitatis.
27 S. IOANNIS AP. ET EV., II classis. Com. octavae Nativitatis.
28 Ss. INNOCENTIUM Mm., II classis. Com. octavae Nativitatis.
Leaving 26-28 as generic Class4 ferias meant there was no octave-day
candidate for the feast to be commemorated OVER, so colitur emitted no
commemoration at all there -- the gap test_oracle.ml's own M11 records,
and the one entry in that file adjudicated against colitur rather than
the oracle. RG 109(c) makes "de diebus infra octavam Nativitatis
Domini" PRIVILEGED, so once the candidate exists RG 111(c)'s single
slot on a II-class day goes to it.
The Sunday guard is RG 69, not an implementation detail: "De dominica
infra octavam Nativitatis Domini, quae scilicet a die 26 ad 31
decembris occurrit, SEMPER fit Officium cum commemoratione festi forte
occurrentis" -- a Sunday falling 26-31 December keeps its OWN office
and commemorates the feast, the reverse of the other days. That Sunday
is built below; overriding it here would invert RG 69. *)
Some (Christmastide, Printf.sprintf "ef-nativity-octave-day-%d" (dd - 24), Colour.White, Class2)
else if m = 1 && dd = 1 then
(* RG 91 entry 5: 1 Jan is the Octave Day of the Nativity, the same table
entry as the Nativity vigil above. *)
Some (Christmastide, "ef-circumcision", Colour.White, Class1)
else if m = 1 && dd = 6 then Some (Christmastide, "ef-epiphany", Colour.White, Class1)
else if same d (off (-46)) then
Some (Lent, "ef-ash-wednesday", Colour.Violet, Class1) (* RG 91 entry 7 *)
else if same d (off (-14)) then
Some (Passiontide, "ef-passion-sunday", Colour.Violet, Class1) (* RG 91 entry 6 *)
else if same d (off (-7)) then
Some (Passiontide, "ef-palm-sunday", Colour.Violet, Class1) (* RG 91 entry 6 *)
else if same d easter then Some (Paschaltide, "ef-easter-sunday", Colour.White, Class1)
else if same d (off 7) then
Some (Paschaltide, "ef-low-sunday", Colour.White, Class1) (* RG 91 entry 6 *)
else if same d (off 38) then
(* RG 91 entry 21: II-class vigil. It is also Rogation Wednesday; [named]
emits the higher-ranked vigil (entry 21 outranks any Rogation-day
ferial rank). CORRECTED (final fix wave, item 7): this comment
previously said the Rogation commemoration itself "waits for RG
108-111" -- the precedence framework and RG 108-111 both exist now
(this branch), but no candidate for the Rogation Wednesday's own
observance is constructed here or anywhere else, so there is nothing
for RG 108-111 to admit; see the fuller comment on the Rogation
branch further down in [temporal] for the current, still-real gap. *)
Some (Paschaltide, "ef-ascension-vigil", Colour.White, Class2)
else if same d (off 39) then Some (Paschaltide, "ef-ascension", Colour.White, Class1)
else if same d (off 48) then Some (Paschaltide, "ef-pentecost-vigil", Colour.Red, Class1) (* RG 91 entry 9 *)
else if same d (off 49) then Some (Paschaltide, "ef-pentecost", Colour.Red, Class1)
else if same d (off 56) then Some (Time_after_pentecost, "ef-trinity", Colour.White, Class1)
else if same d (off 60) then Some (Time_after_pentecost, "ef-corpus-christi", Colour.White, Class1)
else if same d (off 68) then Some (Time_after_pentecost, "ef-sacred-heart", Colour.White, Class1)
else if same d (christ_the_king y) then Some (Time_after_pentecost, "ef-christ-the-king", Colour.White, Class1)
else None
let days_between a b = Date.to_rata b - Date.to_rata a
(* Floor division. OCaml's [/] truncates toward zero, so a date before a
season's week origin would round up into week 1 instead of falling out of the
numbering: Ash Wednesday is 4 days before the Lent I origin, and -4/7 = 0
would make it week 1. *)
let floor_div a b = if a >= 0 then a / b else ((a + 1) / b) - 1
(* The Sunday on which week 1 of a season begins. Every origin is a Sunday, so
week numbers are constant Sunday-to-Saturday.
Christmastide has no numbered weeks. Time after Epiphany counts from the
first Sunday after Epiphany -- which itself falls 7-13 January and is
therefore inside Christmastide (RG 72-73), so the season's own days start
part-way through week 1. Time after Pentecost counts from Pentecost, making
Trinity Sunday the first Sunday after Pentecost. *)
let week_origin s y =
let easter = Computus.gregorian_easter y in
match s with
| Advent -> Some (advent_start y)
| Christmastide -> None
| Time_after_epiphany -> Some (Date.add_days (sunday_on_or_before (mk y 1 6)) 7)
| Septuagesima -> Some (Date.add_days easter (-63))
| Lent -> Some (Date.add_days easter (-42)) (* Lent I Sunday *)
| Passiontide -> Some (Date.add_days easter (-14))
| Paschaltide -> Some easter
| Time_after_pentecost -> Some (Date.add_days easter 49) (* Pentecost *)
(* [week] is total and defined for every date, named or not: a named day
inside a numbered season run carries that run's week by calling this same
function, not by a separately hand-set value (see [named]'s docstring
above). It naturally returns [None] for Christmastide (no season-wide
numbering) and for the handful of proper-Mass days between Ash Wednesday
and Lent I that precede any run's origin. *)
let week d =
let s = season d in
match week_origin s (Date.year d) with
| None -> None
| Some origin ->
let n = floor_div (days_between origin d) 7 in
let n = match s with Time_after_pentecost -> n | _ -> n + 1 in
if n < 1 then None else Some n
(* Christmastide has no numbered weeks ([week_origin] returns [None]), so the
generic <season>-<week>-<weekday> ferial fallback below would collapse
every feria to literal week "0" -- and because colitur's Christmastide
spans 25 Dec - 13 Jan (RG 72-73, the deliberate divergence from lectio),
the same weekday recurs two or three times across that span, producing
duplicate slugs within a single liturgical year (register finding 1).
Four sub-stretches, each given a key that cannot collide with the others:
- 26-28 Dec (between the Nativity and its Octave days, which are named
above): lectio has no narrower key here either, so this keeps its
existing "ef-christmas-0-<weekday>" key unchanged -- nothing to lose by
changing it, and nothing gained.
- 2-5 Jan (between the Octave Day and Epiphany): lectio *also* collapses
this to "ef-christmas-0-<weekday>", indistinguishable from the stretch
above in lectio's own data. colitur cannot preserve a distinction lectio
doesn't make, so this becomes "ef-christmas-1-<weekday>" -- a
colitur-only key and a lectionary gap for the Plan 4 bootstrap to fill
(CORRECTED, final fix wave, item 7: the lectionary bootstrap is Plan 4,
not Plan 3 -- see Slug.ml's own corrected comment), exactly like the
Nativity vigil and octave-day keys above.
- 7-13 Jan, split in two by the *actual* first-Sunday-after-Epiphany
origin ([week_origin Time_after_epiphany], which by construction always
falls somewhere in this window -- see that function's own comment):
- from the origin Sunday through 13 Jan: this genuinely is week 1 of
Time after Epiphany, just still inside Christmastide by season
(RG 72-73). lectio keys it "ef-time-after-epiphany-1-<weekday>", the
same key [sunday_slug] already gives the Sunday in this window --
and it is the *same computation* the ordinary Time-after-Epiphany
ferial fallback below will give the rest of that same
Sunday-to-Saturday week once the season turns on 14 Jan, so this
cannot collide with it (a fixed 7-day week has each weekday once).
- 7 Jan through the day *before* the origin (0-6 days, only present
when Epiphany does not fall on a Saturday): these genuinely precede
week 1 -- treating them as week 1 too, as a naive calendar-range
read of lectio's behaviour would, collides with the days named just
above, because they are exactly 7 days before them for whichever
weekdays they cover (verified empirically: reusing "week 1" here
produced duplicates in most years, not merely an edge case). No
lectio key to preserve either way, so this is its own colitur-only
"ef-christmas-2-<weekday>" -- a further lectionary gap. *)
let christmastide_feria_slug d =
let y = Date.year d in
let m = Date.month d and dd = Date.day d in
let w = Date.weekday_to_string (Date.weekday d) in
if m = 12 && dd >= 26 && dd <= 28 then Some (Printf.sprintf "ef-christmas-0-%s" w)
else if m = 1 && dd >= 2 && dd <= 5 then Some (Printf.sprintf "ef-christmas-1-%s" w)
else if m = 1 && dd >= 7 && dd <= 13 then
match week_origin Time_after_epiphany y with
| Some origin when Date.compare d origin >= 0 -> Some (Printf.sprintf "ef-time-after-epiphany-1-%s" w)
| _ -> Some (Printf.sprintf "ef-christmas-2-%s" w)
else None
(* Sunday slugs. These are lectionary keys: they use [season_slug_word], and for
Christmastide they keep lectio's keys even though colitur's season differs
(spec §4.4 -- slugs are opaque keys, not truth). *)
let sunday_slug d =
if Date.weekday d <> Date.Sun then None
else
let y = Date.year d in
let m = Date.month d and dd = Date.day d in
let s = season d in
match s with
| Christmastide ->
if m = 12 && dd >= 26 then Some "ef-christmas-sunday-0"
else if m = 1 && dd >= 7 && dd <= 13 then
(* 1st Sunday after Epiphany (Holy Family). Season is Christmastide per
RG 72-73; the key stays lectio's. *)
Some "ef-time-after-epiphany-sunday-1"
else if m = 1 && dd >= 2 && dd <= 5 then
(* Most Holy Name of Jesus. Colitur slug -- lectionary gap; confirm the
placement against MR1962 while coding (register §6). *)
Some "ef-holy-name-sunday"
else None
| Time_after_pentecost -> (
(* Reuse [week] rather than recomputing the Pentecost-relative week
number locally, so the two can never drift apart (see
test_week_sunday_slug_agree). Only the last Sunday and the resumed
tail are genuinely special. *)
match week d with
| None -> None
| Some n ->
let last_sunday = Date.add_days (advent_start y) (-7) in
if same d last_sunday then
(* The last Sunday before Advent always keeps the 24th (Last) Mass. *)
Some "ef-time-after-pentecost-sunday-24"
else if n > 23 then
(* Surplus Sundays resume the Sundays after Epiphany that Septuagesima
cut short -- the highest-numbered ones, so the 6th sits just before
the Last. *)
let total = match week last_sunday with Some t -> t | None -> n in
Some (Printf.sprintf "ef-time-after-epiphany-sunday-%d" (n - total + 7))
else Some (Printf.sprintf "ef-time-after-pentecost-sunday-%d" n))
| _ -> (
match week d with
| Some n -> Some (Printf.sprintf "ef-%s-sunday-%d" (season_slug_word s) n)
| None -> None)
let id = "ef"
(* The third Sunday of September: the Ember week's anchor.
[cited] PRIMARY-SOURCE-VERIFIED (register §3a): MR1962, "De anno et eius
partibus", under the heading "Quatuor Tempora" (not a numbered RG
paragraph, which is why an earlier paragraph-number search missed it):
"Quatuor Tempora celebrantur quarta et sexta feria ac sabbato post
tertiam dominicam Adventus, post primam dominicam Quadragesimae, post
dominicam Pentecostes, post dominicam tertiam septembris."
-- the Ember Days are kept on the Wednesday, Friday and Saturday after
Advent III, after Lent I, after Pentecost, [and] after the third Sunday
of September -- confirming all four of this module's anchors, including
this specific contested one. This specific date-derivation rule was one
of the more contested points in the 1962 calendar: pre-1955 practice
tied the September Ember days to the week following the Exaltation of
the Holy Cross (14 Sept) instead. The two rules only disagree when 1
September is a Monday -- 2025 is such a year, and confirms the
third-Sunday reading empirically too (24/26/27 September against an
independent oracle, vs the Holy-Cross rule's 17/19/20). An earlier
version of this comment said the scan contained no numbered-paragraph
statement of either rule and left the citation at the rank rules only
(RG 91 entries 18/22) -- WRONG, corrected once the nominative heading
"Quatuor Tempora" was found rather than the genitive "Quatuor Temporum"
the original search used; register §3a records the correction, because a
false "not in the source" note is worse than no note. *)
let third_sunday_of_september y =
let sep1 = mk y 9 1 in
let first_sunday = Date.add_days sep1 ((7 - weekday_index sep1) mod 7) in
Date.add_days first_sunday 14
(* Ember days: Wednesday, Friday and Saturday after the anchoring Sunday.
RG 91 entry 18 makes the Advent, Lent and September sets II class; entry 22
excepts the Lenten set from the III-class Lenten ferias. The Whitsun set
falls inside the I-class Pentecost octave and takes its rank.
The September and Advent sets match lectio's own Ember SLUG spelling
directly. The Lent and Whitsun (Pentecost) sets do not -- "ef-lent-ember-*"
and "ef-pentecost-ember-*" are colitur-only slug KEYS, never lectio's own
naming for the identical civil days (Lent: "ef-lent-1-<weekday>";
Whitsun: "ef-easter-8-<weekday>"). CORRECTED (task 8 fix round 1,
coordinator review, Critical 2): this comment previously said "lectio
has no Ember slug for either" and framed BOTH as "a lectionary gap for
the Plan 4 bootstrap to fill" -- true of the SLUG naming, misleading
about the underlying READING DATA. lectio's own tridentine-lectionary.ini
carries real citations for both sets, just filed under the non-Ember
names above -- tools/bootstrap_lectionary.ml's own [colitur_keys]
renames both onto colitur's own slugs (the Whitsun rename shipped with
Plan 4; the Lent one was missed for one fix round -- both engines
independently fell through to the wrong ferial-resumption answer there,
so even the differential could not see the gap until it was found;
data/ef/expected-divergences.sexp's own C22 has the full account). *)
let ember d =
let y = Date.year d in
let easter = Computus.gregorian_easter y in
let sets =
[ (third_sunday_of_september y, "september", Class2, Colour.Violet);
(Date.add_days (advent_start y) 14, "advent", Class2, Colour.Violet);
(Date.add_days easter (-42), "lent", Class2, Colour.Violet);
(Date.add_days easter 49, "pentecost", Class1, Colour.Red) ]
in
List.find_map
(fun (anchor, name, rank, colour) ->
let day_of = function 3 -> Some "wed" | 5 -> Some "fri" | 6 -> Some "sat" | _ -> None in
let n = days_between anchor d in
if n >= 3 && n <= 6 then
match day_of n with
| Some w -> Some (Printf.sprintf "ef-%s-ember-%s" name w, rank, colour)
| None -> None
else None)
sets
(* RG 91 entry 7: Ash Wednesday (named above) and Monday-Wednesday of Holy
Week are I-class ferias -- the primary text reads "feria IV cinerum et II,
III et IV Hebdomadae sanctae", i.e. explicitly stops at Wednesday
(PRIMARY-SOURCE-VERIFIED, final fix wave: confirmed word for word
against the scan). Thursday to Saturday of Holy Week are the Sacred
Triduum, RG 91 entry 2 -- ranked even above entry 7, not a mere feria --
but the Sacred Triduum has NO PROPER OFFICE of its own in this codebase
(CORRECTED, final fix wave, item 7: this comment previously said their
"own named offices are a Plan 3 sanctoral addition"; WRONG on two
counts -- Plan 3 shipped, in this branch, without adding them, AND a
proper office for I-class FERIAS was never a sanctoral matter to begin
with, RG 21's own definition of "feria" excludes Sundays/feasts, not the
other way round). `Temporal_ef.temporal 2026-04-02/03/04` (Holy
Thursday/Good Friday/Holy Saturday) still resolve today to the ordinary
Passiontide ferial fallback's own generic slugs,
"ef-passiontide-2-{thursday,friday,saturday}" -- register §6 records
this as its own open item now. This gives them the same I-class rank
via the generic ferial path regardless. RG 91 entry 10: the weekdays
within the privileged Octaves of Easter and Pentecost are I class too. *)
let privileged_feria d =
let easter = Computus.gregorian_easter (Date.year d) in
let n = days_between easter d in
(n >= -6 && n <= -1) || (n >= 1 && n <= 6) || (n >= 50 && n <= 55)
(* RG 117 enumerates the five colours (white, red, green, violet, black);
RG 127 assigns green and RG 128 violet to the seasons de Tempore below. RG
119 (register §3b, primary-source-verified 2026-08-11 -- this comment was
stale until Task 16 noticed the correction had not been copied down here):
white "a festo Nativitatis Domini usque ad expletum tempus Epiphaniae"
and "a Missa Vigiliae paschalis usque ad Missam vigiliae Pentecostis
exclusive" -- exactly Christmastide and Paschaltide below. *)
let season_colour = function
| Advent | Septuagesima | Lent | Passiontide -> Colour.Violet (* RG 128 *)
| Christmastide | Paschaltide -> Colour.White (* RG 119 *)
| Time_after_epiphany | Time_after_pentecost -> Colour.Green (* RG 127 *)
(* Gaudete (Advent III) and Laetare (Lent IV) are rose: RG 131, "may be used...
for the Office and Mass of that Sunday only" -- an indult over the
season's violet, not a season colour of its own. *)
let is_rose_sunday d s =
let y = Date.year d in
match s with
| Advent -> same d (Date.add_days (advent_start y) 14)
| Lent -> same d (Date.add_days (Computus.gregorian_easter y) (-21))
| _ -> false
(* RG 91 entry 28, "Feriae IV classis", is an unqualified catch-all: any feria
not placed by a more specific entry above defaults to IV class. That is
what a per annum or Septuagesima feria falls back to here -- and also an
ordinary Paschaltide weekday (e.g. a Rogation day) outside the privileged
octave, since the table has no entry of its own for Paschaltide ferias. *)
let ferial_rank d s =
if privileged_feria d then Class1
else
match s with
| Advent -> if Date.month d = 12 && Date.day d >= 17 then Class2 (* RG 91 e18 *) else Class3 (* e25 *)
| Lent | Passiontide -> Class3 (* RG 91 e22 *)
| _ -> Class4 (* RG 91 e28 *)
let weekday_word d = Date.weekday_to_string (Date.weekday d)
(* RG 91 entry 27, "Officium sanctae Mariae in sabbato" -- the votive Office of
the BVM on Saturday. Caput IX of the Rubricae Generales, both photographic
scans and the electronic transcription, word for word (docs/research/
rules-register.md §4/§6; no scan-vs-transcription conflict -- RG 78/79 are
General Rubrics prose, not the Mass-propers body text the transcription is
documented elsewhere as missing almost all of):
"78. In sabbatis, in quibus occurrit Officium de feria IV classis, fit
de sancta Maria in sabbato.
79. Officium sanctae Mariae in sabbato incipit a Matutino et explicit
post Nonam."
-- "On Saturdays on which the Office of a IV-class feria occurs, [the
Office] is made of Holy Mary on Saturday [instead]. The Office of Holy
Mary on Saturday begins at Matins and ends after None." RG 78's own
protasis is exactly "otherwise unoccupied IV-class Saturday": WHICH
Saturdays qualify is not a season list to hand-maintain -- it is simply
every Saturday whose temporal candidate would otherwise be
{!Vocab_ef.Class4} (RG 91 entry 28's own unqualified ferial catch-all,
[ferial_rank] above), from ANY season that ferial rank reaches
(Septuagesima, Time after Epiphany, Time after Pentecost, and ordinary
Paschaltide Saturdays outside the privileged Easter/Pentecost octaves, and
the Christmastide ferial stretches [christmastide_feria_slug] builds --
Advent/Lent/Passiontide Saturdays are already Class2/Class3, higher than
IV class, so RG 78's protasis never fires there, and Ember/Rogation/
privileged-octave Saturdays are excluded the same way). Nothing else is
needed to decide "otherwise unoccupied": {!Precedence_ef.band}'s own
entry-27 branch already reads [rank = Class4 && weekday = Sat] on the
TEMPORAL candidate unconditionally (312 966 times across the domain, per
the task report) and only ever WINS the day (becomes [observed]) when
nothing of better table position contests it -- exactly RG 78's own
condition, decided by the existing occurrence machinery, not re-derived
here. When a real sanctoral feast DOES win such a Saturday outright, RG
78's protasis is false for that day (the IV-class Office never "occurs"
there to begin with), and this candidate simply loses/omits exactly as
the plain ferial candidate it replaces already did (RG 26, {!disposition}
in precedence_ef.ml) -- verified: no change to {!Precedence_ef.band},
{!Precedence_ef.disposition} or {!Precedence_ef.admit} was needed for
this, since RANK stays {!Vocab_ef.Class4} either way and none of those
three functions reads slug/colour/name to decide who wins, what a loser's
fate is, or how many commemorations are admitted.
Rank: IV class, RG 91's own table position, unconditional ("Dies
liturgici IV classis: 27. Officium sanctae Mariae in sabbato. 28. Feriae
IV classis." -- both scans, register §4).
Colour: white, ALWAYS, regardless of season. CORRECTED, fix round 1
(coordinator finding F6): RG 120(b) alone is a stretch as the PRIMARY
citation -- its own text reads "in Officio et Missa DE FESTIS" (of
FEASTS), and this Office is not itself a festum (RG 91's own table
position, entry 27, sits outside the Festa rows 11-13/16/19/20/23/24
entirely). The tighter chain, found on the same scan: RG 431(e) ("VIII -- De diversis
Missae partibus", subsection "C) De hymno Gloria in excelsis" -- NOT
"Caput XX", which does not exist: the Rubricae Generales' Caput series
ends at XIX. Corrected by the fix-round re-review; the paragraph number,
letter and subsection title were right, only the containing division was
invented) is the Missal's OWN classification of
this exact Mass, word for word: "431. Hymnus Gloria in excelsis
dicitur: ... e) in Missis votivis IV classis de Angelis, quocumque die,
et de B. Maria Virg. QUAE IN SABBATO CELEBRANTUR" -- the Gloria is said
"in IV-class VOTIVE MASSES of the Angels, on any day, and OF THE BVM
WHICH ARE CELEBRATED ON SATURDAY" -- the Missal's own words classify
this Mass as a "Missa votiva IV classis... de B. Maria Virg.", not a
festum. RG 121(a) (immediately after RG 120, same "De colore albo"
section), both photographic scans: "121. Colorem album requirunt
Missae votivae: a) quae respondent festis, de quibus numero
praecedenti" -- white colour is required by VOTIVE MASSES which
"correspond to" (respondent) the feasts named in the preceding
paragraph [120] -- 120(b)'s own "B. Mariae Virg." among them. The chain
-- RG 431(e) (this Mass IS a IV-class votive Mass of the BVM) -> RG
121(a) (a votive Mass corresponding to a 120-listed feast-type takes
that colour) -> RG 120(b) (BVM feasts are white) -- reaches the
identical conclusion (white) through the category the Missal's own text
actually places this Mass in (votive), not by treating it as a festum
RG 91's own table structure says it is not. NOT RG 119 (the "de
Tempore" white rule for Christmastide/Paschaltide) or RG 127/128
(season green/violet) either way: this Office is never "de Tempore" for
colour purposes, so it overrides [season_colour] unconditionally,
including on Septuagesima (violet) and Time-after-Epiphany/-Pentecost
(green) Saturdays -- confirmed against the oracle (missalemeum, 1
August 2026, a Time-after-Pentecost Saturday: white, not the season's
green). The Missal's own "Missae de sancta Maria in sabbato" section
(both scans) carries no further per-Mass colour override the way some
OTHER votive Masses do elsewhere in the same scan ("In hac Missa
adhibetur color albus") -- it needs none, the chain above already
settles it unconditionally for every occurrence.
Name: "Officium sanctae Mariae in sabbato" -- RG 91 entry 27's own table
title AND RG 79's own heading, both photographic scans and the electronic
transcription, word for word. Latin only, not English -- the same
zero-circularity discipline {!holy_family_sunday}/{!holy_name_names}/
{!triduum_names} already established: an English name would mean reading
missalemeum's own title text, the oracle this axis is compared against,
to decide colitur's own "ground truth" name.
Subject: [Bvm] -- simply true (RG 91 entry 27's own title names the BVM
directly) and currently inert for precedence purposes: neither
{!Precedence_ef.band}, {!Precedence_ef.disposition} nor
{!Precedence_ef.admit} tests [subject = Bvm] anywhere today (only [=
Lord] is ever read) -- the same "tagged anyway because it is simply true"
reasoning {!triduum_names}'s own [subject = Lord] tag already used for
the identical shape. Checked for a live interaction with the one other
[Bvm]-tagged entry this codebase's data carries
(data/ef/adjustments.sexp's `most-holy-name-of-mary`, 12 September,
Class3 Feast, not Commemoration_only): whenever 12 September falls on a
Saturday it wins outright over any Class4 candidate ({!Precedence_ef.band}
entry 24 beats entry 27 on table order alone), so this Office and that
entry can never coexist as a winner/commemorated-loser pair -- no live
RG 112(d) witness is created by this tag.
Slug: UNCHANGED -- deliberately reuses whatever
[christmastide_feria_slug]/the generic <season>-<week>-<weekday> ferial
fallback would already have produced for this date (e.g.
"ef-septuagesima-2-saturday", "ef-time-after-pentecost-8-saturday"), not
a new bespoke key. Two independent reasons, not one:
(1) PRECEDENT: the SAME shape as the Sacred Triduum ({!triduum_names}'s
own citation above) -- "the slug deliberately STAYS the generic ...
key... identified structurally... never off the slug" -- and
{!Precedence_ef.band}'s own entry-27 branch already reads [rank = Class4
&& weekday = Sat] on the temporal candidate structurally, never its slug,
so nothing needs a bespoke key to find this office.
(2) A bespoke season-independent slug would have been actively WRONG:
[Colitur_kernel.Validate]'s own ["slugs"] check (validate.ml) asserts
every OBSERVED slug is sighted at most once per liturgical year, verified
with ZERO exceptions across the whole 1583-9999 domain before this task
(register/CLAUDE.md's own carried item 4) -- a single uniform
"ef-bvm-saturday" would be sighted 4-9 times in most liturgical years
(every otherwise-unoccupied Saturday), breaking that invariant for real,
not merely in theory (confirmed by writing it that way first: [Validate]'s
own landmark-years test failed immediately, 1583 alone sighting it on 5
dates). Reusing the season-keyed ferial slug keeps the invariant intact
with no change to [Validate] at all, since season+week+weekday is already
guaranteed unique within a liturgical year by construction. It also
dovetails with the FIVE numbered "Missae de sancta Maria in sabbato" the
Missal's own Common of the BVM prints immediately afterward -- a heading
("MISSAE DE S. MARIA IN SABBATO QUAE DICI ETIAM POSSUNT ALIIS DIEBUS UT
VOTIVAE DE B. MARIA VIRGINE, IUXTA RUBRICAS VEL, EX INDULTO, PRO TEMPORUM
DIVERSITATE") whose SUBSTANCE both scans corroborate but whose exact
wording is NOT "word for word" identical on both -- CORRECTED, fix round
1 (coordinator finding F5b): both scans' own OCR is noisy here, and the
two additionally use different abbreviations for each other ("S. MARIA"
vs "SANCTA MARIA", "B. MARIA VIRGINE" vs "BEATA MARIA VIRGINE"), not
merely different OCR artifacts of one underlying text -- the task
report has the full raw-OCR comparison. Followed by five numbered
Masses, each with its own season heading, clean and consistent on both
scans: I "Tempore Adventus", II "A Nativitate Domini usque ad
Purificationem", III "A die 3 Februarii usque ad feriam IV Hebdomadae
sanctae", IV "Tempore Paschali", V "A festo Ss. Trinitatis usque ad
sabbatum ante dominicam I Adventus" -- matching the oracle's own "V Mass
of the B. V. M." title on a Time-after-Pentecost Saturday: a
season-keyed slug leaves room for a future lectionary
bootstrap (Plan 4) to map each one onto its corresponding numbered Mass's
propers, the same way [sunday_slug]'s own season-keyed Sunday slugs
already anchor the ordinary lectionary. RG 309(a)'s own words -- "Missae
quae pro sancta Maria in sabbato, IUXTA TEMPORUM DIVERSITATEM, in Missali
assignantur" -- confirm this is a MASS-TEXT selection detail (which
PROPERS are said), not an OFFICE-identity one; colitur computes no
citations/readings at all yet ([citations] is empty on every day) --
deliberately not modelled further here, recorded per the task report. The
Missal's own five-season partition for the Mass numbering does not even
align with this rite's own RG 71-77 season boundaries (its own "III"
spans Septuagesima, all of Lent and most of Passiontide as ONE bracket),
one further reason not to try to derive a "Mass number" field from
[Vocab_ef.season] here. *)
let bvm_saturday_names =
Colitur_kernel.Names.of_list
[ (Colitur_kernel.Lang.of_string_exn "la", "Officium sanctae Mariae in sabbato") ]
let is_bvm_saturday d (rank : Vocab_ef.rank) = Date.weekday d = Date.Sat && rank = Class4
let temporal d =
let y = Date.year d in
let easter = Computus.gregorian_easter y in
let s = season d in
let weekday = Date.weekday d in
let build ?(subject = Colitur_kernel.Subject.Temporal) ?(names = Colitur_kernel.Names.empty) ~season
~slug ~colour ~rank ~week () =
let office =
Colitur_kernel.Celebration.make ~slug:(Slug.of_string_exn slug) ~rank ~colour ~subject ~names
~layer:"temporal" ()
in
{ Colitur_kernel.Temporal.season; week; weekday; office }
in
match named d with
| Some (season, slug, colour, rank) -> build ~season ~slug ~colour ~rank ~week:(week d) ()
| None -> (
(* Rogations (the Minor Litanies only -- RG 87, Monday and Tuesday
before Ascension). The Major Litanies (25 April, RG 80) are a fixed
date and are STILL not computed (CORRECTED, final fix wave, item 7:
this comment previously said "they arrive with Plan 3's sanctoral"
-- Plan 3 shipped, in this branch, without them; register §6 tracks
this as a plain open item, with no plan committed to build it yet).
The Wednesday here is the Ascension vigil (see Task 11), which
happens to also fall on Rogation Wednesday -- the vigil (higher
RG 91 entry) is what [named] emits for that date; the Rogation
Wednesday's own commemoration is not separately constructed (a real
gap, not a forward dependency: the precedence framework and RG
108-111 both exist now, but nothing wires a Rogation-Wednesday
candidate into the contest for this specific date the way Monday
and Tuesday get one below). RG 88: "de Litaniis minoribus nihil fit
in Officio" -- the Office (hence the day's rank) is unchanged by
the Rogation; only the Mass is proper. No RG 91 table entry
elevates these days, so they
take the ordinary ferial rank of their season via [ferial_rank]
rather than a fixed class. lectio has no Rogation slug at all, so
"ef-rogation-monday"/"-tuesday" are colitur-only keys and a
lectionary gap for Plan 4 (CORRECTED, final fix wave, item 7 --
the lectionary bootstrap is Plan 4, not Plan 3), like the Ember
and Nativity-vigil keys above.
COLOUR (L6, data/ef/expected-divergences-lms.sexp, CLOSED by this
fix): this branch used to hardcode [Colour.Violet] with no
citation at all. RG 119 (Caput XVIII, "De Colore Paramentorum"):
white is used "in Officio et Missa de Tempore ... a Missa Vigiliae
paschalis usque ad Missam vigiliae Pentecostes exclusive" -- the
de-Tempore Office AND Mass are white from the Paschal Vigil Mass
until the Pentecost Vigil Mass exclusive, and days 36/37 (Rogation
Monday/Tuesday) sit squarely inside that window. RG 128's own
exhaustive four-case list for violet (Advent I-Christmas Eve;
Septuagesima-Paschal Vigil; the September Ember ferias; vigils of
II/III class OUTSIDE Paschaltide) names Rogation days nowhere, and
its vigil clause is inapplicable twice over -- these are not
vigils, and they ARE inside Paschaltide. RG 88 ("de Litaniis
minoribus nihil fit in Officio, sed tantum in Missa") is why: the
violet belongs to the ROGATION MASS/procession alone, a distinct
votive text this engine does not model as a separate Mass (the
same per-action colour nuance temporal_ef.ml's own RG 126/RG 128
Good-Friday-Communion citations already flag as unmodelled
elsewhere) -- the day's own OFFICE, which is what [colour] here
expresses, stays [season_colour s] like any other Paschaltide
feria. Witnessed directly: LMS 2023-2024, 2024-05-06 (Rogation
Monday), "FERIA IV Cl W" (white), "Mass of 5th Sunday after
Easter". *)
let rogation = days_between easter d in
if rogation = 36 || rogation = 37 then
build ~season:s
~slug:(if rogation = 36 then "ef-rogation-monday" else "ef-rogation-tuesday")
~colour:(season_colour s) ~rank:(ferial_rank d s) ~week:(week d) ()
else
match ember d with
| Some (slug, rank, colour) -> build ~season:s ~slug ~colour ~rank ~week:(week d) ()
| None -> (
match sunday_slug d with
| Some slug ->
let colour = if is_rose_sunday d s then Colour.Rose else season_colour s in
(* RG 11-12: Sundays of Advent, Lent, Passiontide, Easter, Low
Sunday and Pentecost are I class; all others II. The I-class
ones are already named above -- Passiontide has only two
Sundays and both are named, so no Passiontide Sunday ever
reaches this fallback -- leaving II class here except the
remaining Advent and Lent Sundays. *)
let rank = match s with Advent | Lent -> Class1 | _ -> Class2 in
(* RG 17(b) (this file's own [holy_family_sunday], full
citation there): the ONE Sunday a year this branch must
NOT leave [subject = Temporal] (this function's own
default) -- the Holy Family, whose slug/rank/colour are
otherwise EXACTLY what this branch already computes for
the 7-13 January Sunday (an accident this task's own
oracle-strengthening work exposed: rank/colour alone
could never distinguish "Holy Family" from "an ordinary,
unnamed Sunday", since they happen to coincide). Every
OTHER Sunday this branch ever builds -- including the
narrower Holy Name of Jesus window, RG 17(a), still
unbuilt as its own named day, register §6 -- keeps
[Temporal] and no name, unaffected.
[names] -- fix round 1 (coordinator finding 3): a LATIN
name, not English. The oracle's own observed-identity
axis (test_oracle.ml) reads only [en], so this is
deliberately invisible to it -- setting an ENGLISH name
here would mean reading missalemeum's own title text
(the oracle this exact axis is compared against) to
decide colitur's own "ground truth" name, precisely the
"expected value promoted from actual output" vacuity
flavour this project's own review process watches for.
Latin has no such circularity: the calendarium's own
January table, both photographic scans, word for word:
"Dominica I post Epiphaniam: S. Familiae, Iesu, Mariae,
Ioseph, II classis" -- and the Mass propers' own heading
(also both scans): "SANCTAE FAMILIAE IESU, MARIAE,
IOSEPH, II classis", the exact string used here, the
same genitive-title convention test_names.ml's own
worked example already uses for Easter ("Dominica
Resurrectionis"). Every other temporal-cycle candidate
still carries no name at all (register §6.2's own open
item on Holy Name of Jesus, RG 17(a)) -- this is a
targeted addition for the one day this task built, not a
claim that the gap is closed generally. *)
(* RG 17(a) (this file's own [holy_name_sunday], full citation
there): the SECOND Sunday a year this branch must not
leave [subject = Temporal] -- added alongside Holy
Family's own check above, not replacing it (the two
windows, 2-5 and 7-13 January, never overlap, so at most
one of the two conditions is ever true for a given [d]).
Guarded on [holy_name_sunday y = Some d] rather than only
"is [d] in [2,5] January": every OTHER Sunday in that
range must stay unnamed, and in a year where a DIFFERENT
day of the window is the one true Sunday, [d] itself is
never reached by [sunday_slug] as a Sunday to begin with
(weekday alone already excludes it) -- but writing the
check this way, against the SAME independently-computed
date [holy_name_sunday] returns rather than a bare month/
day range, is what test_temporal_ef.ml's own anchor test
below cross-checks against.
ADDED, fix round 1 (coordinator finding F4): the Sunday
shape's "takes the Sunday's own place, no commemoration of
it" treatment is settled at RG LEVEL, not only in Holy
Name's own Mass propers (this file's own [holy_name_sunday]
citation already has that quote) -- RG 17's own CLOSING
paragraph, immediately after its own lettered list
(a)-(e), scan-verified: "Haec festa locum tenent dominicae
occurrentis cum omnibus iuribus et privilegiis; de
dominica, proinde, nulla fit commemoratio" -- "these
feasts" (plural, covering the WHOLE list (a)-(e), Holy Name
included) "hold the place of the occurring Sunday with all
rights and privileges; of the Sunday, therefore, no
commemoration is made". A second, independent primary
source for the same conclusion the propers-level quote
already gives, not a new claim.
FRAGILE DERIVATION, noted per fix round 1 (coordinator
finding F5): the mechanism that actually grants a LOSING
Holy-Name-Sunday its RG 109(a) privilege when outranked
(Precedence_ef.privilege_of's own (a) branch, {!Precedence
_ef.is_sunday_slug}) reads it off the "-sunday" SUBSTRING
in the slug ("ef-holy-name-sunday") -- a naming convention,
not a citation of RG 17's own text quoted immediately
above, which is the actual warrant ("these feasts hold the
place of the occurring Sunday WITH ALL ITS RIGHTS AND
PRIVILEGES" -- RG 109(a)'s own "of a Sunday" privilege is
one of those rights). Right answer, fragile path: a future
rename of this slug family would silently drop the
privilege with no compiler or test failure pointing here.
No live witness exists to test it either way (no
fixed-date candidate ever outranks Holy Name in today's
data -- test_precedence_ef.ml's own synthetic
[test_class1_feast_inside_holy_name_window_end_to_end]
proves the SYNTHETIC case only). *)
let subject, names =
if same d (holy_family_sunday y) then
( Colitur_kernel.Subject.Lord,
Colitur_kernel.Names.of_list
[ (Colitur_kernel.Lang.of_string_exn "la", "Sanctae Familiae Iesu, Mariae, Ioseph") ] )
else if holy_name_sunday y = Some d then (Colitur_kernel.Subject.Lord, holy_name_names)
else (Colitur_kernel.Subject.Temporal, Colitur_kernel.Names.empty)
in
build ~subject ~names ~season:s ~slug ~colour ~rank ~week:(week d) ()
| None ->
(* RG 17(a)'s own fallback, "secus die 2 ianuarii" (full
citation on [holy_name_fallback_date] above): ONLY when
[holy_name_sunday y] is [None] for [d]'s own civil year --
i.e. 2-5 January genuinely has no Sunday that year -- does
2 January itself carry the feast; every other year, 2
January is an ordinary Christmastide feria, exactly what
[christmastide_feria_slug] below already computes for it.
Checked here, ahead of that generic fallback, the same way
[named]'s own fixed dates are checked ahead of everything
ferial -- NOT folded into [named] itself, because unlike
every one of [named]'s ~20 entries this one is
CONDITIONAL on a per-year fact ([holy_name_sunday y] =
[None]) that [named]'s bare [Date.t -> ... option] shape
has no way to express without threading [subject] through
every one of its other branches too -- the same reason
Holy Family, immediately above, is not one of [named]'s
outputs either (that branch's own comment).
Season Christmastide (RG 72-73: 2 January is always within
it); rank/colour/subject/names identical to the Sunday
shape immediately above -- ONE feast, {!holy_name_names}'s
own comment on why both shapes share it. Band-classified
at entry 14's MOVABLE half regardless of which shape fired
this year (Precedence_ef.band; both shapes are built
through this same [temporal] function, hence [origin =
Temporal] either way) -- RG 91's "primum mobilia, deinde
fixa" split is a property of the TABLE ENTRY a feast
occupies, and RG 17(a) names ONE feast with a fallback
clause, not two differently-classified feasts that happen
to share a Mass formulary; the Mass propers' own heading
(docs/research/rules-register.md) states both shapes under
the identical title for exactly this reason. *)
if same d (holy_name_fallback_date y) && holy_name_sunday y = None then
build ~subject:Colitur_kernel.Subject.Lord ~names:holy_name_names ~season:Christmastide
~slug:"ef-holy-name" ~colour:Colour.White ~rank:Class2 ~week:(week d) ()
else (
match christmastide_feria_slug d with
| Some slug ->
(* Every Christmastide feria this branch builds is
Class4 ([ferial_rank]'s own catch-all -- Christmastide
is never Advent/Lent/Passiontide), so
[is_bvm_saturday]'s rank test is always true here; the
weekday is the only real condition. See
[bvm_saturday_names]'s own citation above for the full
argument (rank/colour/name/subject) -- the SLUG stays
[slug], the same one this branch would otherwise have
built, per that same citation's own "Slug" paragraph
(not re-quoted at this function's two call sites). *)
if is_bvm_saturday d Class4 then
build ~subject:Colitur_kernel.Subject.Bvm ~names:bvm_saturday_names ~season:s ~slug
~colour:Colour.White ~rank:Class4 ~week:(week d) ()
else
build ~season:s ~slug ~colour:(season_colour s) ~rank:(ferial_rank d s)
~week:(week d) ()
| None ->
(* The days between Ash Wednesday and Lent I have proper
Masses and belong to no numbered week. *)
let after_ashes = days_between easter d in
if after_ashes >= -45 && after_ashes <= -43 then
build ~season:s
~slug:(Printf.sprintf "ef-lent-after-ashes-%s" (weekday_word d))
~colour:Colour.Violet ~rank:Class3 ~week:None ()
else
let week_n = week d in
let slug =
Printf.sprintf "ef-%s-%d-%s" (season_slug_word s)
(Option.value week_n ~default:0) (weekday_word d)
in
if is_bvm_saturday d (ferial_rank d s) then
(* RG 78 (Caput IX, "De sancta Maria in sabbato") --
see [bvm_saturday_names]'s own citation above for
the full argument. This is the ordinary-season
half of the two call sites: Septuagesima, Time
after Epiphany, Time after Pentecost, and
ordinary Paschaltide Saturdays outside the
privileged Easter/Pentecost octaves are exactly
the seasons [ferial_rank] gives Class4 outside
Christmastide (the other call site, above). The
SLUG stays [slug], just computed above, the same
one this branch would otherwise have built. *)
build ~subject:Colitur_kernel.Subject.Bvm ~names:bvm_saturday_names ~season:s ~slug
~colour:Colour.White ~rank:Class4 ~week:week_n ()
else
let colour =
(* The Pentecost octave weekdays are red, not Paschaltide's white. *)
if days_between easter d >= 50 && days_between easter d <= 55 then Colour.Red
(* RG 128(b) (docs/research/rules-register.md §3b), primary
text: "...a dominica in Septuagesima usque ad Vigiliam
paschalem, EXCEPTIS: ... MISSA SIVE CHRISMATIS SIVE IN
CENA DOMINI FERIA V HEBDOMADAE SANCTAE; ..." -- violet
runs Septuagesima to the Easter Vigil EXCEPT (among
others) "the Mass, whether of the Chrism or in Cena
Domini [Holy Thursday], on Thursday of Holy Week" --
named as a WHOLE-MASS exception (unlike Palm Sunday's
"blessing and procession of palms", which the SAME
sentence carves out as only PART of that day, register
§3b's own RG126 note on the not-yet-modelled per-action
nuance), so this is a clean whole-day colour fact, not
a per-action one the day/colour model cannot express.
RG 122, fix round 1 (F9), states the same fact
affirmatively rather than as an exception to RG 128's
violet: "Demum adhibetur color albus, feria V
Hebdomadae sanctae, in Missa Chrismatis et in Missa in
Cena Domini" -- white is used, finally [among the
White section's own list], on Thursday of Holy Week,
in the Mass of Chrism and in the Mass in Cena Domini.
Task 16, found via the missalemeum oracle comparison:
every other Triduum day's oracle colour SET includes
violet as one option (Good Friday "bv", Holy Saturday
"vw" -- RG 132's black is a separate, ALREADY-flagged
gap, register §3b, not touched here), but Holy
Thursday's is white ALONE -- confirming this specific
day, and only this one, needs the exception coded. *)
else if days_between easter d = -3 then Colour.White
(* RG 128(b)'s THIRD named exception, the same primary
sentence quoted above: "...Actione liturgica feria VI
in Passione et Morte Domini usque ad Communionem
exclusive; ..." -- violet does NOT run through the Good
Friday liturgical action, and RG 132 assigns black
there. Recorded in register §3b as an acknowledged gap
since Task 16 (the comment above says so in as many
words) and closed here on two independent 1962-scoped
witnesses, neither in the Divinum Officium lineage:
O'Connell, *The Celebration of Mass* 4th ed. (1964),
revised to the Codex Rubricarum (1960) and the 1962
Missal, §5 -- "Black, symbolising mourning, is used on
Good Friday (violet for the Communion rite)" -- and a
published 1962 Ordo (register §6.21, §6.22).
Like Palm Sunday's palms this rubric is per-ACTION
("usque ad Communionem exclusive") and the day/colour
model emits ONE colour; black is the day's principal
one -- the same judgement missalemeum's own colour SET
makes by ordering Good Friday "bv", black first. The
residual per-action nuance (violet from Communion) is
the SAME already-acknowledged modelling gap RG 126's
palms carry, not a new one. *)
else if days_between easter d = -2 then Colour.Black
else season_colour s
in
(* [triduum_names]'s own citation above: identity only,
for the three days of the Sacred Triduum -- every
other ferial day here stays unnamed, subject
Temporal, as before. *)
let subject, names =
match triduum_names (days_between easter d) with
| Some names -> (Colitur_kernel.Subject.Lord, names)
| None -> (Colitur_kernel.Subject.Temporal, Colitur_kernel.Names.empty)
in
build ~subject ~names ~season:s ~slug ~colour ~rank:(ferial_rank d s) ~week:week_n ())))
(* Independent restatement of [named]'s fixed and Easter-relative dates,
paired with the slug each should carry, for civil year [y]. Deliberately
NOT derived from [named] itself -- consumed by [Validate]'s anchor-
agreement check (design spec §5.7), which exists precisely to catch an
accidental single-site drift (e.g. Ascension's [off 39] silently becoming
[off 40]) that both sides moving together would hide.
[holy_family_sunday] is NOT one of [named]'s own outputs (its own
citation above explains why -- [temporal] applies it as a targeted
[subject] override inside [sunday_slug]'s branch, not through [named]'s
4-tuple), but it is exactly the same kind of independently-computed
anchor this list exists to guard -- an accidental drift in
[holy_family_sunday]'s own [+7] would silently move Holy Family without
this guard catching it, same as any other entry here.
Holy Name of Jesus (RG 17(a)) is the SAME shape, restated per-year rather
than as a single fixed pair: exactly ONE of its two slugs exists for any
given [y] ([holy_name_sunday y] is [Some] xor [None]), so exactly one
entry -- "ef-holy-name-sunday" at that Sunday, or "ef-holy-name" at
[holy_name_fallback_date y] -- is appended below, matching whichever
shape [temporal] itself will actually build that year. An erosion of
EITHER branch is still caught: {!Validate.run} calls [anchors] for both
the civil year a date falls in and the one before it (a liturgical year
straddles two), so across any two consecutive years both shapes are
exercised regardless of which one civil year [y] itself happens to
land on. *)
let anchors y =
let easter = Computus.gregorian_easter y in
let off n = Date.add_days easter n in
(* Days within the Octave, 26-31 December, but ONLY those that are not
Sundays. RG 69 gives a Sunday occurring 26-31 December its OWN office
("semper fit Officium cum commemoratione festi forte occurrentis"), so
in such a year that date carries the Sunday-within-the-octave slug, and
asserting an octave-day anchor there would assert the rubric's own
opposite. This list named 29/30/31 unconditionally until 2026-08-18 --
wrong whenever one fell on a Sunday, corrected alongside [named]'s own
Sunday guard and confirmed against the oracle, which shows "Sunday in
the Octave of Christmas" on 2035-12-30 where colitur used to show a
feria. 26-28 join for the first time: they are days within the octave
too (RG 67), which is what M11's missing commemoration turned on. *)
List.filter_map
(fun dd ->
let d = mk y 12 dd in
if Date.weekday d = Date.Sun then None
else Some (Printf.sprintf "ef-nativity-octave-day-%d" (dd - 24), d))
[ 26; 27; 28; 29; 30; 31 ]
@ [ ("ef-nativity", mk y 12 25);
("ef-nativity-vigil", mk y 12 24);
("ef-circumcision", mk y 1 1);
("ef-epiphany", mk y 1 6);
("ef-time-after-epiphany-sunday-1", holy_family_sunday y);
("ef-ash-wednesday", off (-46));
("ef-passion-sunday", off (-14));
("ef-palm-sunday", off (-7));
("ef-easter-sunday", off 0);
("ef-low-sunday", off 7);
("ef-ascension-vigil", off 38);
("ef-ascension", off 39);
("ef-pentecost-vigil", off 48);
("ef-pentecost", off 49);
("ef-trinity", off 56);
("ef-corpus-christi", off 60);
("ef-sacred-heart", off 68);
("ef-christ-the-king", christ_the_king y) ]
@ (match holy_name_sunday y with
| Some d -> [ ("ef-holy-name-sunday", d) ]
| None -> [ ("ef-holy-name", holy_name_fallback_date y) ])
(* The Missale Romanum's own CALENDARIUM table, February, footnote
(docs/research/LT.txt:5011-5014, scan-corroborated at
docs/research/scan2.txt:3050-3058, where OCR mangles it to
"bi&sextili"/"Matthue"/"Cabrielis" -- transcription and scan agree, so
this rests on two witnesses of the same document):
"In anno bissextili mensis februarius est dierum 29, et festum S.
Matthiae celebratur die 25 februarii, ac festum S. Gabrielis a
Virgine perdolente 28 februarii, et bis dicitur sexto calendas, id
est die 24 et die 25; et littera dominicalis, quae assumpta fuit in
mense ianuario, mutetur in praecedentem..."
In a leap year February has 29 days, St Matthias is kept on 25 February
(not 24), St Gabriel of Our Lady of Sorrows on 28 February (not 27), and
the sixth kalends of March is said TWICE -- "bis dicitur sexto
calendas... die 24 et die 25". That last clause names the MECHANISM, not
two independent exceptions: the Roman calendar's intercalary day is
inserted by DOUBLING the sixth kalends (civil 24 February in a common
year), not by appending a 29th day at the month's end. Reckoned by
kalends position: 24 Feb = VI Kal. Mart., 25 Feb = V Kal., 26 Feb = IV
Kal., 27 Feb = III Kal., 28 Feb = pridie Kal. In a leap year VI Kal.
itself falls on TWO consecutive civil days (24th and 25th, "bis"), and
every kalends position after it is pushed one civil day later as a
result: V Kal. from 25th to 26th, IV Kal. from 26th to 27th, III Kal.
(Gabriel) from 27th to 28th, pridie Kal. from 28th to 29th. A feast fixed
at VI Kal. itself (Matthias) is kept on the SECOND occurrence, not the
first -- 25 February carries it, 24 February carries no fixed office at
all that year (confirmed against the LMS Ordo witness,
data/ef/expected-divergences-lms.sexp entry L3: 24 February 2024 is
titled plainly "EMBER SATURDAY of LENT", no Matthias anywhere near it).
IMPLEMENTED AS THE GENERAL MECHANISM, not as "shift these two named
feasts": only three sanctoral entries exist in 23-29 February on shipped
data (peter-damien the 23rd, before the doubled kalends and therefore
unaffected; matthias the 24th; gabriel-of-our-lady-of-sorrows the 27th),
so "shift the two named feasts" and "shift every fixed entry from 24
February" are indistinguishable on universal data -- they produce
identical output. They differ only for a locally-supplied entry, e.g. an
[--overlay] patronal feast fixed at 26 February: under the mechanism
reading it shifts to 27 February in a leap year (IV Kal. -> the civil day
IV Kal. falls on that year), exactly as a diocesan calendar compiled
against the same kalends convention would expect. The rubric's own text
states a MECHANISM ("bis dicitur sexto calendas"), not a closed list of
two saints, and this project has already been bitten once by a rule
implemented against the shipped data's coincidental shape rather than the
rubric itself (RG 16(a), CLAUDE.md's own carried lesson) -- so the general
reading is the one implemented here.
Threaded through {!Rite.t}'s [fixed_key] field, read only by
{!Colitur_kernel.Layer.on_date}'s FIXED half: the kernel stays
rite-agnostic (a Byzantine or other non-Roman rite supplies nothing and
is unaffected), and the MOVABLE half ({!Date_spec.Easter_offset},
{!Date_spec.Nth_weekday}) is untouched -- nothing in the footnote
concerns Easter-relative dates. *)
let bissextile_fixed_key (d : Date.t) : (int * int) option =
let m = Date.month d and day = Date.day d in
if m = 2 && Date.is_leap (Date.year d) then
if day = 24 then None (* the FIRST VI Kal.: no fixed entry lands here in a leap year *)
else if day >= 25 && day <= 29 then Some (2, day - 1) (* the SECOND VI Kal. onward, one day later *)
else Some (m, day)
else Some (m, day)
(* Compile-time check that this module satisfies the kernel's rite contract. *)
module _ : Colitur_kernel.Temporal.RITE = struct
let id = id
type season = Vocab_ef.season
type rank = Vocab_ef.rank
let vocab = Vocab_ef.vocab
let year_start = year_start
let temporal = temporal
end
|