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
|
(* Task 6 (2026-08-21-colitur-celebrant-rubrics-phase1) built this layer
against ONE LMS Ordo edition (2024-2025). The Witnesses task
(2026-08-22, same phase) extends it to THREE editions -- 2023-2024,
2024-2025, 2025-2026 -- each compared in its OWN window, counted and
reported SEPARATELY, never merged: a divergence appearing in one year
and not another is informative on its own, and merging would hide it.
[make_suite] below is the shared machinery Task 6 built as a single
flat file; every per-year fact that Task 6 hardcoded (window dates,
fixture SHA-256, the "only Good Friday has no Creed marker" pin, the
formulary-bucket totals, the BVM-Saturday population) is now a
parameter or a value RE-DERIVED from the rubric independently per
window, not copied forward -- see each call site below for the
per-edition numbers, which do differ (397/399/400 rows; not every
edition even has the same II-class "sharp pairing" saint available,
see [fixture_probes] in tools/extract_lms_ordo.ml's own header for
2025-2026's own Joachim-is-impeded finding).
WHY THIS LAYER EXISTS, AND WHAT IT IS INDEPENDENT OF: layers 3-4
(test_differential.ml, test_oracle.ml) are ONE LINEAGE running Divinum
Officium -> missalemeum -> lectio -> colitur (CLAUDE.md's "know what
each layer cannot see" section); layer 5 (test_golden.ml) pins colitur's
own output against itself. This Ordo is compiled independently by the
Latin Mass Society (a printed book, England & Wales diocesan propers)
from the 1962 Missal/1960 rubrics directly -- it never passed through
Divinum Officium, missalemeum or lectio at any point, so an error
inherited by that whole lineage (colitur's own bootstrap included) is
visible here for the first time. Three editions of the SAME publisher's
book are still only ONE compiler's judgement (see docs/research/ordo/
PROVENANCE-ordo-corpus.md's own "what this corpus still cannot show" --
genuine cross-compiler independence needs the FIUV layer, test_fiuv_
ordo.ml, built separately in this same task).
SCOPE, decided after running the comparison against real data (not
guessed in advance -- see each fixture's own CHARACTERISATION section):
- The CREED (RG 475-476, {!Rite_ef.Rubrics_ef.creed}): compared on
every day, full 1:1, in every edition's own window. Exactly one day
per window is EXCLUDED, not silently skipped: that window's own
Good Friday (RE-DERIVED per window from {!Colitur_kernel.Computus.
gregorian_easter}, never hardcoded to a specific date) has no Mass
in the 1955-restored Holy Week, so the Ordo prints no Gl/Cr line at
all there -- [test_creed_coverage] asserts this is the ONLY such
day per window.
- The BVM-SATURDAY SEASONAL SELECTION (RG 309(a), "iuxta temporum
diversitatem", {!Rite_ef.Lectionary_ef.bvm_saturday_citations}):
the Ordo's own roman numeral (I-V) names WHICH of the five seasonal
"Missae de sancta Maria in sabbato" is said, so it validates
colitur's SEASONAL SELECTION, not merely that some BVM Mass was
chosen. Checked against every {!Colitur_kernel.Mass_formulary.
Votive} day inside each edition's own window.
- The FORMULARY OVERRIDE (does the day say its own Mass, or a
borrowed one) -- compared for THREE of {!Colitur_kernel.
Mass_formulary.source}'s five constructors ([Proper]/
[Preceding_sunday]/[Common]), on the same [Own_slug]-excluded,
[Votive]-covered-separately basis Task 6 established (see that
task's own report for the full reasoning: 11 of [Own_slug]'s
real days show an Ordo override anyway, a data-representation
ambiguity in colitur's own lectionary, not a rubric question the
Ordo could adjudicate).
GLORIA is extracted into every fixture (a genuine, independently useful
field for the still-unbuilt Phase 2 Gloria rule) but NOT compared here:
colitur has no {!Rite_ef.Rubrics_ef}-level Gloria predicate yet.
DIOCESAN SCOPE: colitur computes the UNIVERSAL General Roman Calendar
only (no diocesan overlay loaded by the differential/oracle harnesses
anywhere in this suite) -- every LMS edition is an England & Wales
diocesan Ordo, and each fixture's own provenance header quantifies how
many of its days carry at least one diocesan variant. Every comparison
in this file reads ONLY each fixture's own universal-block fields. *)
module Cal = Colitur_kernel.Calendar
module Layer = Colitur_kernel.Layer
module Overlay = Colitur_kernel.Overlay
module LD = Colitur_kernel.Liturgical_day
module Date = Colitur_kernel.Date
module Computus = Colitur_kernel.Computus
module MF = Colitur_kernel.Mass_formulary
module V = Rite_ef.Vocab_ef
module TE = Rite_ef.Temporal_ef
let sanctoral_path = "../data/ef/sanctoral.sexp"
let adjustments_path = "../data/ef/adjustments.sexp"
(* Duplicated, not shared, from test_oracle.ml/test_differential.ml's own
identical function -- neither file exposes an .mli the other could
depend on, and this is three lines (test_oracle.ml's own comment). *)
let sha256_of_file path =
let tmp = Filename.temp_file "colitur_lms_ordo_sha256" ".txt" in
Fun.protect
~finally:(fun () -> try Sys.remove tmp with Sys_error _ -> ())
(fun () ->
let cmd = Printf.sprintf "sha256sum %s > %s" (Filename.quote path) (Filename.quote tmp) in
let rc = Sys.command cmd in
if rc <> 0 then Alcotest.failf "sha256sum exited %d for %s (is it on PATH?)" rc path;
let ic = open_in tmp in
let line =
try input_line ic
with End_of_file ->
close_in ic;
Alcotest.failf "sha256sum produced no output for %s" path
in
close_in ic;
match String.index_opt line ' ' with
| Some i -> String.sub line 0 i
| None -> Alcotest.failf "unexpected sha256sum output for %s: %S" path line)
let real_layer () =
let layer =
match Layer.load V.rank_of_sexp sanctoral_path with
| Ok l -> l
| Error e -> Alcotest.failf "%s: failed to load: %s" sanctoral_path e
in
let overlay =
match Overlay.load V.rank_of_sexp adjustments_path with
| Ok o -> o
| Error e -> Alcotest.failf "%s: failed to load: %s" adjustments_path e
in
let layer, diagnostics = Overlay.apply layer overlay in
Alcotest.(check (list string)) "the committed overlay applies cleanly, no diagnostics" []
(List.map Overlay.diagnostic_to_string diagnostics);
layer
let real_lectionary () =
match Colitur_kernel.Lectionary.load "../data/ef/lectionary.sexp" with
| Ok l -> l
| Error e -> Alcotest.failf "../data/ef/lectionary.sexp: failed to load: %s" e
let real_commons () =
match Rite_ef.Lectionary_ef.Commons.load "../data/ef/commons.sexp" with
| Ok c -> c
| Error e -> Alcotest.failf "../data/ef/commons.sexp: failed to load: %s" e
(* ---------------------------------------------------------------------- *)
(* The Ordo side: the sexp row, mirrored (not shared -- see this file's *)
(* own header) from tools/extract_lms_ordo.ml's own [row]. *)
(* ---------------------------------------------------------------------- *)
open Sexplib0.Sexp_conv
type ordo_row = {
date : string;
weekday : string;
title : string;
formulary_override : string option;
bvm_numeral : string option;
gloria : bool option;
creed : bool option;
has_diocesan_variant : bool;
}
[@@deriving sexp]
let ordo_rows fixture_path =
let sexp =
try Sexplib.Sexp.load_sexp fixture_path
with e -> Alcotest.failf "%s: failed to load: %s" fixture_path (Printexc.to_string e)
in
list_of_sexp ordo_row_of_sexp sexp
(* ---------------------------------------------------------------------- *)
(* The colitur side, over the SAME window a given fixture covers. Mirrors *)
(* test_oracle.ml's own [colitur_rows]: the resolution walk starts a civil *)
(* year EARLY (and ends one late) because a liturgical year is *)
(* Advent-anchored and straddles two civil years -- generalised from Task *)
(* 6's single hardcoded [2023..2025] to a [~year_lo]/[~year_hi] parameter *)
(* per window, since the three editions' own civil-year spans differ. *)
(* ---------------------------------------------------------------------- *)
type colitur_row = {
c_date : string;
c_season : V.season;
c_formulary : MF.t option;
c_creed : bool;
c_gloria : bool;
c_rank : V.rank;
c_slug : string;
c_has_commemoration : bool;
(** whether {!Colitur_kernel.Liturgical_day.t.commemorations} is
non-empty -- needed by the Gloria comparison's own L5 shape
(RG 431(b)/n.302(b)), not by anything Task 6 built. *)
}
let colitur_rows ~year_lo ~year_hi ~window_first ~window_last =
let layer = real_layer () in
let rite = Rite_ef.context ~lectionary:(real_lectionary ()) ~commons:(real_commons ()) in
let by_rata : (int, (V.season, V.rank) LD.t) Hashtbl.t = Hashtbl.create 800 in
for y = year_lo to year_hi do
let days = Cal.year rite layer y in
Array.iter (fun (d : (V.season, V.rank) LD.t) -> Hashtbl.replace by_rata (Date.to_rata d.LD.date) d) days
done;
let mk s =
match Date.of_iso8601 s with Ok d -> d | Error e -> Alcotest.failf "%s: not a valid ISO-8601 date: %s" s e
in
let rows = ref [] in
let d = ref (mk window_first) in
let stop = mk window_last in
while Date.compare !d stop <= 0 do
(match Hashtbl.find_opt by_rata (Date.to_rata !d) with
| Some day ->
rows :=
{ c_date = Date.to_iso8601 day.LD.date; c_season = day.LD.temporal.Colitur_kernel.Temporal.season;
c_formulary = day.LD.formulary; c_creed = day.LD.creed; c_gloria = day.LD.gloria;
c_rank = day.LD.observed.Colitur_kernel.Celebration.rank;
c_slug = Colitur_kernel.Slug.to_string day.LD.observed.Colitur_kernel.Celebration.slug;
c_has_commemoration = day.LD.commemorations <> [] }
:: !rows
| None -> Alcotest.failf "no colitur day resolved for %s" (Date.to_iso8601 !d));
d := Date.add_days !d 1
done;
List.rev !rows
(* ---------------------------------------------------------------------- *)
(* RG 309(a), "iuxta temporum diversitatem" -- INDEPENDENTLY re-derived *)
(* from the rubric, not called from Lectionary_ef.bvm_saturday_citations. *)
(* ---------------------------------------------------------------------- *)
let expected_bvm_numeral season ~month ~day =
match (season : V.season) with
| V.Advent -> "I"
| V.Christmastide -> "II"
| V.Time_after_epiphany when month = 1 || (month = 2 && day <= 2) -> "II"
| V.Time_after_epiphany | V.Septuagesima | V.Lent | V.Passiontide -> "III"
| V.Paschaltide -> "IV"
| V.Time_after_pentecost -> "V"
(* ---------------------------------------------------------------------- *)
(* data/ef/expected-divergences-lms.sexp -- same shape as *)
(* test_oracle.ml's own [allow_entry]/[load_allow_list]. ONE allow-list *)
(* file for all three editions (they are one lineage, the same publisher *)
(* and compiler) -- an entry's own [note] names which edition(s) it fired *)
(* on when that matters. *)
(* ---------------------------------------------------------------------- *)
type allow_entry = { id : string; citation : string; verdict : string; note : string; expected_rows : int }
[@@deriving sexp]
let load_allow_list allow_list_path =
let sexps =
try Sexplib.Sexp.load_sexps allow_list_path
with e -> Alcotest.failf "%s: failed to load: %s" allow_list_path (Printexc.to_string e)
in
List.map allow_entry_of_sexp sexps
(* ---------------------------------------------------------------------- *)
(* Formulary override, restricted to Proper/Common/Preceding_sunday -- *)
(* see this file's own header for why Own_slug is excluded, and Votive is *)
(* covered by the BVM checks instead. Unchanged from Task 6 except for *)
(* being read per-window rather than off a single hardcoded fixture. *)
(* ---------------------------------------------------------------------- *)
let ordinal n =
let suffix =
if n mod 100 >= 11 && n mod 100 <= 13 then "th"
else match n mod 10 with 1 -> "st" | 2 -> "nd" | 3 -> "rd" | _ -> "th"
in
Printf.sprintf "%d%s" n suffix
let parse_sunday_slug slug =
let prefix = "ef-" in
let plen = String.length prefix in
if String.length slug <= plen || String.sub slug 0 plen <> prefix then None
else
let body = String.sub slug plen (String.length slug - plen) in
let marker = "-sunday-" in
let mlen = String.length marker and blen = String.length body in
let rec find_last i best = if i + mlen > blen then best
else find_last (i + 1) (if String.sub body i mlen = marker then Some i else best)
in
match find_last 0 None with
| None -> None
| Some i -> (
let word = String.sub body 0 i in
let num_str = String.sub body (i + mlen) (blen - i - mlen) in
match int_of_string_opt num_str with Some n -> Some (word, n) | None -> None)
(* [~season] disambiguates ONE genuinely ambiguous case, found extending
this suite to the 2023-2024 window (2024-11-05..08): colitur's own
"resumed-Sunday tail" (CLAUDE.md's own term) REUSES an Epiphany Sunday's
own slug ([ef-time-after-epiphany-sunday-4]) for a feria whose REAL
calendar date falls in November, inside Time after Pentecost -- Easter
was late enough in 2024 that not every Epiphany Sunday got "used" before
Septuagesima cut the season short, so the leftover ones resume near the
END of the year instead. The Ordo's own prose distinguishes the two
occurrences of the SAME slug by wording alone: "4th Sunday after the
Epiphany" in January, "4th Sunday REMAINING after the Epiphany" in
November (verified directly against the raw dump). [c_season] is the
feria's own actual calendar season -- [Time_after_epiphany] for the
January occurrence, [Time_after_pentecost] for the November one -- so
comparing it against the WORD parsed out of the resumed slug ("time-
after-epiphany" either way) is exactly the signal needed, with no new
colitur-side field required. *)
let expected_preceding_sunday_override ~(season : V.season) (word : string) (n : int) =
match (word, n) with
| "advent", 1 -> Some "Advent Sunday"
| "advent", 2 -> Some "2nd Sunday of Advent"
| "advent", 3 -> Some "3rd Sunday of Advent (Gaudete Sunday)"
| "advent", 4 -> Some "4th Sunday of Advent"
| "septuagesima", 1 -> Some "Septuagesima Sunday"
| "septuagesima", 2 -> Some "Sexagesima Sunday"
| "septuagesima", 3 -> Some "Quinquagesima Sunday"
| "time-after-epiphany", n when season = V.Time_after_epiphany ->
Some (Printf.sprintf "%s Sunday after the Epiphany" (ordinal n))
| "time-after-epiphany", n -> Some (Printf.sprintf "%s Sunday remaining after the Epiphany" (ordinal n))
| "time-after-pentecost", 24 -> Some "24th & Last Sunday After Pentecost"
| "time-after-pentecost", n -> Some (Printf.sprintf "%s Sunday after Pentecost" (ordinal n))
(* [n >= 3], no upper bound: Task 6's original [n <= 5] excluded n=6/7 on
the (overfit, 2024-2025-window-only) assumption that EVERY such day is
Ascension-adjacent. Extending to 2023-2024/2025-2026 found a REAL
counter-example -- Rogation Monday (RG 87-89), which also resumes
week 6's own Sunday but falls BEFORE Ascension, not after, and the
Ordo prints the ordinary "5th Sunday after Easter" there, not "Mass
of the Ascension". The caller now decides Ascension-adjacency by the
day's own DATE against {!Computus.ascension}, not by [n] -- so this
mapping can be trusted for n=6/7 whenever the caller does not first
divert to [ascension_week_override]. *)
| "easter", n when n >= 3 -> Some (Printf.sprintf "%s Sunday after Easter" (ordinal (n - 1)))
| _ -> None
let expected_named_sunday_override slug ~year =
if String.equal slug "ef-christ-the-king" then
match TE.week (TE.christ_the_king year) with
| Some n -> Some (Printf.sprintf "%s Sunday after Pentecost" (ordinal n))
| None -> None
(* [ef-low-sunday]: a SECOND named-Sunday population reaching
[Preceding_sunday], found the same way [ef-christ-the-king] was --
absent from the 2024-2025 window Task 6 built this mapping against
(every Low-Sunday-week feria that year had its own proper), present
in both 2023-2024 (9-12 April 2024) and 2025-2026. The Ordo's own
text is the plain, unnumbered "Mass of Low Sunday". *)
else if String.equal slug "ef-low-sunday" then Some "Low Sunday"
else None
(* See Task 6's own [ascension_week_override] citation (test_lms_ordo.ml
history, data/ef/expected-divergences-lms.sexp's own now-CLOSED L2,
celebrant-rubrics-phase1 Bug 2, 2026-08-22): STRUCTURAL, not a fixture
artefact -- the Ascension is a fixed Easter+39, so every feria whose own
[Preceding_sunday] fallback would otherwise resolve to a week-6/7 Sunday
falls, in SOME years, between Ascension Thursday and the following
Sunday. Extending this suite past the single 2024-2025 window sharpened
the finding: it is specifically the days AFTER Ascension (not merely
"any week-6/7 Preceding_sunday day") that get this treatment -- see
[expected_preceding_sunday_override]'s own citation for the Rogation
Monday counter-example that exposed the difference. [is_after_ascension]
below is the corrected predicate: the day's own DATE compared against
{!Computus.ascension} for its own year, not [n].
FIXED (celebrant-rubrics-phase1, Bug 2): data/ef/lectionary.sexp's new
[ascension_week_entries] now answers these six ferial slugs directly at
step 2 (Own_slug), so [check_formulary_overrides]'s own [Preceding_sunday]
branch below never reaches them any more -- moved to a dedicated
[Own_slug] branch in that same function, which still asserts the Ordo's
override reads exactly [ascension_week_override], now as a genuine
confirmation that colitur's OWN citation matches (previously this
predicate only served to EXCLUDE the population from the general
Preceding_sunday mapping, because colitur had no correct answer to
compare). This constant and [is_after_ascension] itself are still load-
bearing for that confirmation -- kept, not deleted, the same "the
regression net survives its own fix" discipline
data/ef/expected-divergences-lms.sexp's own L6 note states for
[is_l6_rogation_colour]. *)
let ascension_week_override = "Mass of the Ascension"
(* The exact six slugs data/ef/lectionary.sexp's own [ascension_week_entries]
(tools/bootstrap_lectionary.ml) now answers at step 2 -- gated on SLUG
IDENTITY, not merely [is_after_ascension date], because that date range
also covers "Dominica post Ascensionem" and the Vigil of Pentecost, both
genuinely [Own_slug] and both carrying their OWN, different citation, not
the Ascension's (caught live: an earlier, date-only version of this gate
wrongly flagged both as missing an Ordo override before this list was
added). *)
let ascension_week_slugs =
[ "ef-easter-6-friday"; "ef-easter-7-monday"; "ef-easter-7-tuesday"; "ef-easter-7-wednesday";
"ef-easter-7-thursday"; "ef-easter-7-friday"
]
(* Bounded ABOVE by Pentecost, not merely "later than Ascension" -- a bare
lower bound is wrong for any date past Ascension in the SAME civil year
(every date from June to December satisfies "> Ascension", which a
first version of this predicate did and it manufactured 8 false
failures across the 2024-2025/2025-2026 windows, in July/November/
December, caught immediately by re-running against those two
previously-clean windows). Task 6's own original population (Friday
after Ascension, then the following Monday/Tuesday) already showed the
window extends PAST the immediately-following Sunday, not merely up to
it, so Pentecost -- the next fixed liturgical anchor, Easter+49 -- is
the natural, provable upper bound: nothing in this stretch has its own
proper once Ascension's octave-like privilege takes over, and Pentecost
itself always does (RG 91 entry 3). *)
let is_after_ascension date =
let year = Date.year date in
Date.compare date (Computus.ascension year) > 0 && Date.compare date (Computus.pentecost year) < 0
type formulary_counts = {
fc_bad : string list;
fc_proper : int;
fc_common : int;
fc_preceding_sunday : int;
fc_ascension_week : int;
}
let check_formulary_overrides ordo colitur =
let pairs = List.combine ordo colitur in
let proper = ref 0 and common = ref 0 and preceding_sunday = ref 0 and ascension_week = ref 0 in
let bad = ref [] in
List.iter
(fun ((o : ordo_row), (c : colitur_row)) ->
match c.c_formulary with
| Some { MF.via = MF.Proper; _ } ->
incr proper;
if o.formulary_override <> None then
bad := Printf.sprintf "%s: Proper day but Ordo shows override %s" o.date (Option.get o.formulary_override)
:: !bad
| Some { MF.via = MF.Common; _ } ->
incr common;
if o.formulary_override <> None then
bad := Printf.sprintf "%s: Common day but Ordo shows override %s" o.date (Option.get o.formulary_override)
:: !bad
| Some { MF.via = MF.Preceding_sunday; said } -> (
incr preceding_sunday;
let slug =
match said with
| Some s -> Colitur_kernel.Slug.to_string s
| None -> Alcotest.failf "%s: Preceding_sunday day with said = None (should be impossible)" o.date
in
let ordo_says = Printf.sprintf "Mass of %s" in
let year = int_of_string (String.sub o.date 0 4) in
let date = match Date.of_iso8601 o.date with Ok d -> d | Error e -> Alcotest.failf "%s: %s" o.date e in
if is_after_ascension date then (
match o.formulary_override with
| Some got when String.equal got ascension_week_override -> incr ascension_week
| Some got ->
bad :=
Printf.sprintf "%s: colitur resumed %S (after Ascension), Ordo shows %S, not the adjudicated %S"
o.date slug got ascension_week_override
:: !bad
| None ->
bad := Printf.sprintf "%s: colitur resumed %S (after Ascension), Ordo shows no override" o.date slug :: !bad)
else
let expected =
match parse_sunday_slug slug with
| Some (word, n) -> expected_preceding_sunday_override ~season:c.c_season word n
| None -> expected_named_sunday_override slug ~year
in
match expected with
| Some expected -> (
let expected_line = ordo_says expected in
match o.formulary_override with
| Some got when String.equal got expected_line -> ()
| Some got -> bad := Printf.sprintf "%s: colitur resumed %S, Ordo says %S" o.date expected_line got :: !bad
| None -> bad := Printf.sprintf "%s: colitur resumed %S, Ordo shows no override" o.date expected_line :: !bad)
| None -> (
match o.formulary_override with
| Some got when String.equal got ascension_week_override -> incr ascension_week
| Some got ->
bad :=
Printf.sprintf "%s: unmapped (colitur resumed %S) and Ordo shows %S, not the adjudicated %S" o.date
slug got ascension_week_override
:: !bad
| None ->
bad := Printf.sprintf "%s: unmapped (colitur resumed %S) and Ordo shows no override" o.date slug :: !bad))
| Some { MF.via = MF.Own_slug; said } -> (
(* celebrant-rubrics-phase1, Bug 2 fix: the six Ascension-week
ferias moved from [Preceding_sunday] (the branch above, which
used to special-case them via [is_after_ascension]) to
[Own_slug] -- data/ef/lectionary.sexp's own
[ascension_week_entries] now answers at step 2, before step 3
is ever reached. Checked HERE, not merely left to fall through
silently the way every OTHER [Own_slug] day already does
(RG 299's ordinary case, a real proper Mass, needs no Ordo
cross-check because nothing about it is adjudicated): this is
the one [Own_slug] population this suite has an OUTSIDE
witness for, and losing that check when the fix landed would
have been a silent regression net, not merely dead code -- the
Ordo's own override text must still read "Mass of the
Ascension" on exactly these days. *)
let slug = match said with Some s -> Colitur_kernel.Slug.to_string s | None -> "" in
if List.mem slug ascension_week_slugs then
match o.formulary_override with
| Some got when String.equal got ascension_week_override -> incr ascension_week
| Some got ->
bad :=
Printf.sprintf "%s: colitur resolved %S via Own_slug (after Ascension), Ordo shows %S, not %S"
o.date slug got ascension_week_override
:: !bad
| None ->
bad := Printf.sprintf "%s: colitur resolved %S via Own_slug (after Ascension), Ordo shows no override"
o.date slug :: !bad)
| Some { MF.via = MF.Votive; _ } | None -> ())
pairs;
{ fc_bad = List.rev !bad; fc_proper = !proper; fc_common = !common; fc_preceding_sunday = !preceding_sunday;
fc_ascension_week = !ascension_week }
(* ---------------------------------------------------------------------- *)
(* [make_suite]: one edition's worth of tests, parameterised. Every count *)
(* below is a genuine per-edition PIN (measured once against real data, *)
(* not guessed), the same discipline every other golden/oracle fixture in *)
(* this suite already uses -- a change to any of them is a real finding, *)
(* not noise to silence. *)
(* ---------------------------------------------------------------------- *)
(* No Str/regex (frozen deps) -- the same hand-rolled substring test every
other file in this codebase reaches for (rubrics_ef.ml's own
[contains_substring], precedence_ef.ml's [contains_substring]). Local to
this file, not shared, on the same "no common .mli to hang it from"
footing those other copies already document. *)
let contains_substring s ~needle =
let ls = String.length s and ln = String.length needle in
let rec at i = i + ln <= ls && (String.sub s i ln = needle || at (i + 1)) in
ln = 0 || at 0
let make_suite ~label ~fixture_path ~fixture_sha256 ~window_first ~window_last ~year_lo ~year_hi ~allow_list_path
~expected_rows ~expected_bvm_votive ~expected_proper ~expected_common ~expected_preceding_sunday
~expected_ascension_week ~expected_gloria_l5 ~expected_gloria_l6 =
let test_fixture_checksum () =
Alcotest.(check string) "fixture SHA-256 matches its provenance note" fixture_sha256
(sha256_of_file fixture_path)
in
let test_dates_align () =
let ordo = ordo_rows fixture_path in
let colitur = colitur_rows ~year_lo ~year_hi ~window_first ~window_last in
Alcotest.(check int) (Printf.sprintf "the Ordo fixture has %d rows (%s..%s)" expected_rows window_first window_last)
expected_rows (List.length ordo);
Alcotest.(check int) "colitur resolved the same number of days" (List.length ordo) (List.length colitur);
(match ordo with o :: _ -> Alcotest.(check string) "first date" window_first o.date | [] -> Alcotest.fail "empty");
(match List.rev ordo with
| o :: _ -> Alcotest.(check string) "last date" window_last o.date
| [] -> Alcotest.fail "empty");
let mismatched =
List.filter_map
(fun (o, c) -> if String.equal o.date c.c_date then None else Some (o.date, c.c_date))
(List.combine ordo colitur)
in
Alcotest.(check (list (pair string string))) "no misaligned dates" [] mismatched
in
(* This window's own Good Friday(s) (Easter-2, RE-DERIVED here from
{!Computus.gregorian_easter} for every civil year the window touches,
never hardcoded) is the only day expected to carry no Ordo Creed
marker at all -- Good Friday has no Mass in the 1955-restored Holy
Week. Exactly one such date falls inside any one edition's ~13-month
window (its OTHER Easter, a year apart, falls outside it), but this
is computed, not assumed: a window that unexpectedly contained zero or
two would be a real finding this test would catch. *)
let window_good_fridays () =
let first_year = int_of_string (String.sub window_first 0 4) in
let last_year = int_of_string (String.sub window_last 0 4) in
let candidates =
List.concat_map
(fun y ->
let gf = Date.add_days (Computus.gregorian_easter y) (-2) in
[ Date.to_iso8601 gf ])
(List.init (last_year - first_year + 1) (fun i -> first_year + i))
in
List.filter (fun d -> String.compare d window_first >= 0 && String.compare d window_last <= 0) candidates
in
let test_creed_coverage () =
let ordo = ordo_rows fixture_path in
let no_creed = List.filter (fun o -> o.creed = None) ordo in
let expected = window_good_fridays () in
Alcotest.(check (list string)) "only this window's own Good Friday has no Ordo Creed marker" expected
(List.map (fun o -> o.date) no_creed)
in
let describe_creed_mismatch (o : ordo_row) (c : colitur_row) =
Printf.sprintf "%s %S: colitur creed=%b, Ordo creed=%b (formulary=%s)" o.date o.title c.c_creed
(Option.get o.creed)
(match c.c_formulary with
| Some { MF.said = Some s; _ } -> Colitur_kernel.Slug.to_string s
| Some { MF.said = None; via = MF.Votive } -> "votive (said unnamed in the data)"
| Some { MF.said = None; _ } -> "NONE (said, unexpectedly outside Votive)"
| None -> "NONE")
in
(* Which allow-list id, if any, explains a Creed mismatch on this date.
Date-keyed (not merely "any mismatch is fine") deliberately: an id
that stops firing, or fires on a NEW date it was never adjudicated
for, must be visible as a real change, not silently absorbed by a
loose "some entry explains something" check. Task 6's own version of
this function declared [by_id]/[explained_counts] but never actually
populated the latter (the mechanism this comment sits in) -- L1 was
already CLOSED by the time that task shipped, so it had nothing real
to explain and the gap went unexercised. L4 (Witnesses task,
2026-08-22, 2023-2024-only) is the first live test of it. L3's own
"2024-02-24" branch (bissextile-shift task, 2026-08-22) is REMOVED,
not left dead: the fix makes that date's Creed match outright, so the
branch would never fire again, and a stale date->id mapping for a
now-closed entry is exactly the kind of thing this file's own
"must be visible as a real change" discipline (this comment's own
opening sentence) argues against leaving in place. *)
let allow_list_id_for_date d = if String.equal d "2023-12-24" then Some "L4" else None in
let test_creed_matches_or_is_explained () =
let ordo = ordo_rows fixture_path in
let colitur = colitur_rows ~year_lo ~year_hi ~window_first ~window_last in
let allow_list = load_allow_list allow_list_path in
let by_id = List.map (fun e -> (e.id, e)) allow_list in
let unexplained = ref [] in
let explained_counts = Hashtbl.create 8 in
List.iter2
(fun (o : ordo_row) (c : colitur_row) ->
if not (String.equal o.date c.c_date) then Alcotest.failf "misaligned: ordo %s vs colitur %s" o.date c.c_date;
match o.creed with
| None -> ()
| Some ocreed ->
if Bool.equal ocreed c.c_creed then ()
else (
match allow_list_id_for_date o.date with
| Some id -> Hashtbl.replace explained_counts id (1 + try Hashtbl.find explained_counts id with Not_found -> 0)
| None -> unexplained := describe_creed_mismatch o c :: !unexplained))
ordo colitur;
Alcotest.(check (list string)) (Printf.sprintf "[%s] every Creed mismatch is named in the allow-list -- none unexplained" label)
[] (List.rev !unexplained);
Hashtbl.iter
(fun id n ->
match List.assoc_opt id by_id with
| None -> Alcotest.failf "allow-list entry %s used by the comparator but not declared in %s" id allow_list_path
| Some e ->
Alcotest.(check int) (Printf.sprintf "%s: expected_rows matches the actual count" id) e.expected_rows n)
explained_counts
in
(* ---- Gloria (RG 431-432, {!Rite_ef.Rubrics_ef.gloria}) -- Phase 2 of
this task. Same shape as the Creed comparison immediately above:
coverage (which dates carry no Ordo "Gl"/"No Gl" marker at all) is
checked separately from the value comparison, and both are
date-keyed against their own allow-list ids, never a loose "some
divergence is fine" check. *)
let test_gloria_coverage () =
let ordo = ordo_rows fixture_path in
let no_gloria = List.filter (fun o -> o.gloria = None) ordo in
let expected = window_good_fridays () in
Alcotest.(check (list string)) "only this window's own Good Friday has no Ordo Gloria marker" expected
(List.map (fun o -> o.date) no_gloria)
in
let describe_gloria_mismatch (o : ordo_row) (c : colitur_row) =
Printf.sprintf "%s %S: colitur gloria=%b, Ordo gloria=%b (rank=%s commemoration=%b formulary=%s)" o.date o.title
c.c_gloria (Option.get o.gloria) (V.rank_to_string c.c_rank) c.c_has_commemoration
(match c.c_formulary with
| Some { MF.said = Some s; _ } -> Colitur_kernel.Slug.to_string s
| Some { MF.said = None; via = MF.Votive } -> "votive (said unnamed in the data)"
| Some { MF.said = None; _ } -> "NONE (said, unexpectedly outside Votive)"
| None -> "NONE")
in
(* L5 is the one REAL, STRUCTURAL shape still open (see
data/ef/expected-divergences-lms.sexp's own L5 for the full
citation) -- not a fixed date list, a property of the day itself,
because it recurs every year the underlying condition holds, not on a
fixed calendar date the way L4 above does. Matched by PREDICATE, not
by date, for the same reason L2's own note gives for why it is
prose-only rather than wired through the shared [expected_rows]
mechanism: the count varies window to window (L5: 6/5/6), so a single
static count cannot check it, and [make_suite] threads the per-window
expected total directly (~expected_gloria_l5 below) instead.
L6 is CLOSED (celebrant-rubrics-phase1, Bug 1 fix, this task):
{!Rite_ef.Temporal_ef.temporal}'s Rogation Monday/Tuesday branch no
longer hardcodes [Colour.Violet] -- see temporal_ef.ml's own citation
on that branch and data/ef/expected-divergences-lms.sexp's L6 entry
for the fix and its measured blast radius. [is_l6_rogation_colour] and
~expected_gloria_l6 are kept, at 0 in every window, as the regression
net the same way L3's own bissextile fix keeps a predicate whose count
is now provably zero rather than deleting it. *)
let is_l5_lenten_commemoration (o : ordo_row) (c : colitur_row) =
(* NOT a Gloria defect (CORRECTED, l5-f3-readjudication task,
2026-08-22 -- data/ef/expected-divergences-lms.sexp's own L5 has
the full account, including the RG 431(b)/n.302(b) hypothesis this
comment used to state as settled and RG 303(b)'s own "dici potest
tantum si occurrit dies liturgicus IV classis", which prohibits
302(b) on exactly these THIRD-class privileged ferias). Every
instance is a privileged Lenten/Passiontide feria (Class3, violet
-- {!TE}'s own [ferial_rank], RG25) carrying exactly one
commemoration of an impeded Class3 saint, colitur reading
[gloria]=false (the ferial Mass, correctly, per the 1962 core)
where the Ordo reads [true] (the LMS Ordo follows the 2020 decree
Cum Sanctissima, colitur excludes it by binding design, CLAUDE.md
decision 2) -- an OCCURRENCE difference between two different,
both genuine, calendars, not a rule dispute. Predicate kept AS A
MATCHER (the 16-date population is unchanged); only the citation
explaining WHY it is allow-listed was wrong. *)
(not c.c_gloria) && Option.value o.gloria ~default:false && c.c_has_commemoration && c.c_rank = V.Class3
in
let is_l6_rogation_colour (o : ordo_row) (c : colitur_row) =
(* CLOSED (celebrant-rubrics-phase1, Bug 1 fix). Was root-caused to a
DIFFERENT, pre-existing bug this comparison merely surfaced --
{!Rite_ef.Temporal_ef.temporal}'s own Rogation Monday/Tuesday branch
used to hardcode [Colour.Violet] with no RG citation at all, where
the Ordo shows this exact date "FERIA IV Cl W" (white, matching
Paschaltide's own [season_colour] and RG88's "nihil fit in Officio"
-- the Office, hence its colour, is unchanged by the Rogation, only
the Mass TEXT is proper) -- so [gloria]'s own 432(b) violet guard
wrongly fired. Now fixed at the source (temporal_ef.ml); this
predicate and its own ~expected_gloria_l6 count (0 in every window)
are kept as the regression net, not deleted. *)
(not c.c_gloria) && Option.value o.gloria ~default:false
&& (contains_substring c.c_slug ~needle:"rogation-monday" || contains_substring c.c_slug ~needle:"rogation-tuesday")
in
let test_gloria_matches_or_is_explained ~expected_gloria_l5 ~expected_gloria_l6 () =
let ordo = ordo_rows fixture_path in
let colitur = colitur_rows ~year_lo ~year_hi ~window_first ~window_last in
let unexplained = ref [] in
let l5_count = ref 0 and l6_count = ref 0 in
List.iter2
(fun (o : ordo_row) (c : colitur_row) ->
if not (String.equal o.date c.c_date) then Alcotest.failf "misaligned: ordo %s vs colitur %s" o.date c.c_date;
match o.gloria with
| None -> ()
| Some ogloria ->
if Bool.equal ogloria c.c_gloria then ()
else if is_l5_lenten_commemoration o c then incr l5_count
else if is_l6_rogation_colour o c then incr l6_count
else unexplained := describe_gloria_mismatch o c :: !unexplained)
ordo colitur;
Alcotest.(check (list string)) (Printf.sprintf "[%s] every Gloria mismatch is named in the allow-list -- none unexplained" label)
[] (List.rev !unexplained);
Alcotest.(check int) (Printf.sprintf "[%s] L5 (Lenten privileged feria + commemoration) count" label)
expected_gloria_l5 !l5_count;
Alcotest.(check int) (Printf.sprintf "[%s] L6 (Rogation Monday/Tuesday colour bug) count" label) expected_gloria_l6
!l6_count
in
let test_bvm_seasonal_selection () =
let ordo = ordo_rows fixture_path in
let colitur = colitur_rows ~year_lo ~year_hi ~window_first ~window_last in
let votive_pairs =
List.filter_map
(fun (o, c) -> match c.c_formulary with Some { MF.via = MF.Votive; _ } -> Some (o, c) | _ -> None)
(List.combine ordo colitur)
in
Alcotest.(check int) (Printf.sprintf "%d BVM-Saturday (Votive) days fall inside this window" expected_bvm_votive)
expected_bvm_votive (List.length votive_pairs);
let mismatches =
List.filter_map
(fun (o, c) ->
let expected = expected_bvm_numeral c.c_season ~month:(int_of_string (String.sub c.c_date 5 2))
~day:(int_of_string (String.sub c.c_date 8 2)) in
match o.bvm_numeral with
| Some got when String.equal got expected -> None
| Some got -> Some (Printf.sprintf "%s: Ordo says %s, expected %s (season-derived)" o.date got expected)
| None ->
(* One SOURCE-TEXT anomaly, found extending this suite to
2023-2024: 2024-02-03 is the ONLY BVM-Saturday day in the
2023-2024 fixture (of 12) whose Ordo entry carries no
roman-numeral "<N> Mass of BVM" line at all. Verified
directly against the raw pdftotext dump (not inferred):
every OTHER BVM-Saturday day in this edition prints "Sat
<n> CELEBRATION of the BVM IV Cl W" immediately followed
by its own "<N> Mass of BVM" line; this one date's block
goes straight from the title line to "(Blessing of
throats in honour of St Blaise may be given on this
day)" -- the numeral line the Ordo's own layout convention
would otherwise print is simply MISSING from the printed
text, a publisher-side omission this session cannot
correct, not a colitur defect (colitur's own side is
independently right here: St Blaise is scan-verified
"Com." -- a bare commemoration, not a feast -- in the
calendarium itself, docs/research/LT.txt:4978, so RG 91's
table has no row for him and the BVM-Saturday office
correctly wins the day outright, exactly as colitur
computes). Excluded by name, not silently: a future
change to this population (a different date, or a SECOND
gap) would still be caught below. *)
if String.equal fixture_path "fixtures/lms-ordo-2023-2024.sexp" && String.equal o.date "2024-02-03" then
None
else Some (Printf.sprintf "%s: colitur says Votive but the Ordo shows no BVM numeral" o.date))
votive_pairs
in
Alcotest.(check (list string)) "every BVM-Saturday numeral matches its season -- none unexplained" [] mismatches
in
let test_bvm_numeral_implies_votive () =
let ordo = ordo_rows fixture_path in
let colitur = colitur_rows ~year_lo ~year_hi ~window_first ~window_last in
let mismatches =
List.filter_map
(fun (o, c) ->
match (o.bvm_numeral, c.c_formulary) with
| Some n, Some { MF.via = MF.Votive; _ } -> ignore n; None
| Some n, other ->
Some
(Printf.sprintf "%s: Ordo shows %S Mass of BVM but colitur's via is %s" o.date n
(match other with Some f -> MF.source_to_string f.MF.via | None -> "NONE"))
| None, _ -> None)
(List.combine ordo colitur)
in
Alcotest.(check (list string)) "every Ordo BVM numeral day is a colitur Votive day" [] mismatches
in
let test_formulary_override_matches () =
let ordo = ordo_rows fixture_path in
let colitur = colitur_rows ~year_lo ~year_hi ~window_first ~window_last in
let r = check_formulary_overrides ordo colitur in
Alcotest.(check (list string)) "every formulary-override mismatch is explained -- none unexplained" [] r.fc_bad;
Alcotest.(check int) (Printf.sprintf "%d Proper days in the window" expected_proper) expected_proper r.fc_proper;
Alcotest.(check int) (Printf.sprintf "%d Common days in the window" expected_common) expected_common r.fc_common;
Alcotest.(check int) (Printf.sprintf "%d Preceding_sunday days in the window" expected_preceding_sunday)
expected_preceding_sunday r.fc_preceding_sunday;
Alcotest.(check int)
(Printf.sprintf "%d Ascension-week days resolve via Own_slug and match the Ordo's own override, no more, no fewer"
expected_ascension_week)
expected_ascension_week r.fc_ascension_week
in
let test_via_distribution_totals () =
let colitur = colitur_rows ~year_lo ~year_hi ~window_first ~window_last in
let via_name c = match c.c_formulary with None -> "none" | Some f -> MF.source_to_string f.MF.via in
let counts = Hashtbl.create 8 in
List.iter
(fun c -> Hashtbl.replace counts (via_name c) (1 + try Hashtbl.find counts (via_name c) with Not_found -> 0))
colitur;
let total = Hashtbl.fold (fun _ n acc -> acc + n) counts 0 in
Alcotest.(check int) "every via bucket sums to the full window" expected_rows total
in
( label,
[ Alcotest.test_case "fixture SHA-256 matches its provenance note" `Quick test_fixture_checksum;
Alcotest.test_case "streams are N rows each, dates aligned 1:1" `Quick test_dates_align;
Alcotest.test_case "only this window's own Good Friday has no Ordo Creed marker" `Quick test_creed_coverage;
Alcotest.test_case "every Creed difference is named in the cited allow-list -- none unexplained" `Quick
test_creed_matches_or_is_explained;
Alcotest.test_case "only this window's own Good Friday has no Ordo Gloria marker" `Quick
test_gloria_coverage;
Alcotest.test_case "every Gloria difference is named in the cited allow-list -- none unexplained" `Quick
(test_gloria_matches_or_is_explained ~expected_gloria_l5 ~expected_gloria_l6);
Alcotest.test_case "every BVM-Saturday numeral matches its season" `Quick test_bvm_seasonal_selection;
Alcotest.test_case "every Ordo BVM numeral day is a colitur Votive day" `Quick
test_bvm_numeral_implies_votive;
Alcotest.test_case "formulary override STRING matches on Proper/Common/Preceding_sunday days" `Quick
test_formulary_override_matches;
Alcotest.test_case "the via distribution sums to the full window" `Quick test_via_distribution_totals
] )
let allow_list_path = "../data/ef/expected-divergences-lms.sexp"
(* Guards against a stale allow-list entry (declared in the sexp file, but
the OCaml-side [allow_list_id_for_date] predicate above no longer maps
any date to it, or never did) -- the per-window suites above only prove
an id that DOES fire has the right count; nothing in them can notice an
id that never fires in ANY of the three windows, since each suite only
ever looks at its own explained_counts. Fixed to the two ids this task
added (L3, L4) -- L1 is CLOSED/removed and L2 is deliberately prose-only
(see the sexp file's own header on why), so neither is expected here. *)
(* L3 CLOSED (bissextile-shift task, 2026-08-22): removed from the active
sexp record set -- see its own now-prose closure note in
data/ef/expected-divergences-lms.sexp for the fix and the measured
blast radius. Only L4 remains active. *)
let test_allow_list_ids_are_exactly_l4 () =
let ids = List.sort String.compare (List.map (fun e -> e.id) (load_allow_list allow_list_path)) in
Alcotest.(check (list string)) "the LMS allow-list declares exactly L4" [ "L4" ] ids
let suite_allow_list = ("lms-ordo-allow-list", [ Alcotest.test_case "declares exactly L4" `Quick test_allow_list_ids_are_exactly_l4 ])
let suite_2023_2024 =
make_suite ~label:"lms-ordo-2023-2024" ~fixture_path:"fixtures/lms-ordo-2023-2024.sexp"
~fixture_sha256:"c8d9d4b790f6b4438e932f70dad8ebe9a0316662f1eb02c26248e04c7be1a62b" ~window_first:"2023-12-01"
~window_last:"2024-12-31" ~year_lo:2022 ~year_hi:2025 ~allow_list_path ~expected_rows:397
(* expected_proper 182 -> 181 (bissextile-shift task, 2026-08-22): 24
February 2024, previously Matthias's own Proper Mass, is now the
Ember Saturday of Lent's own temporal proper -- Own_slug-sourced, not
Proper, so it leaves this bucket's population rather than merely
changing which slug's proper it is (see this file's own header on why
Own_slug is excluded). Matthias himself moves to 25 February, an
I-class Sunday that admits him not even as a commemoration (RG16(a)),
so his own Mass is not said anywhere in this window any more. *)
(* expected_preceding_sunday 61 -> 60 (celebrant-rubrics-phase1, Bug 2
fix, 2026-08-22): the window's own single Ascension-week day
(2024-05-06 was BEFORE Ascension that year and never counted here in
the first place -- see [ascension_week_override]'s own citation; the
real Ascension-week day this window carries is elsewhere in May)
now resolves via Own_slug (step 2, data/ef/lectionary.sexp's new
[ascension_week_entries]), not Preceding_sunday -- it leaves this
bucket's population the same way a Proper day already does, while
expected_ascension_week itself is UNCHANGED (1): the same day is
still counted, just via check_formulary_overrides' own new
Own_slug branch instead of its Preceding_sunday one. *)
~expected_bvm_votive:12 ~expected_proper:181 ~expected_common:2 ~expected_preceding_sunday:60
~expected_ascension_week:1 ~expected_gloria_l5:5 ~expected_gloria_l6:0
let suite_2024_2025 =
make_suite ~label:"lms-ordo-2024-2025" ~fixture_path:"fixtures/lms-ordo-2024-2025.sexp"
~fixture_sha256:"da817b75c5bf40ed3be1d5f6890b199705e02ce4d42111253ab8547bccabc3f7" ~window_first:"2024-11-27"
~window_last:"2025-12-31" ~year_lo:2023 ~year_hi:2026 ~allow_list_path ~expected_rows:400
(* expected_preceding_sunday 66 -> 63 (celebrant-rubrics-phase1, Bug 2
fix): this window's own three Ascension-week days move from
Preceding_sunday to Own_slug, the same reasoning as the 2023-2024
window's own note above; expected_ascension_week stays 3. *)
~expected_bvm_votive:14 ~expected_proper:179 ~expected_common:2 ~expected_preceding_sunday:63
~expected_ascension_week:3 ~expected_gloria_l5:5 ~expected_gloria_l6:0
let suite_2025_2026 =
make_suite ~label:"lms-ordo-2025-2026" ~fixture_path:"fixtures/lms-ordo-2025-2026.sexp"
~fixture_sha256:"8839a61e0c7d1e6c8326114f4f45a5c183551287a88f4ed01127154add6ae5a5" ~window_first:"2025-11-28"
~window_last:"2026-12-31" ~year_lo:2024 ~year_hi:2027 ~allow_list_path ~expected_rows:399
(* expected_preceding_sunday 68 -> 66 (celebrant-rubrics-phase1, Bug 2
fix): this window's own two Ascension-week days move from
Preceding_sunday to Own_slug, the same reasoning as the 2023-2024
window's own note above; expected_ascension_week stays 2. *)
~expected_bvm_votive:13 ~expected_proper:174 ~expected_common:2 ~expected_preceding_sunday:66
~expected_ascension_week:2 ~expected_gloria_l5:6 ~expected_gloria_l6:0
|