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
|
open Colitur_kernel
open Rite_ef
(* Same pattern test_rite_ef.ml already uses: both [Calendar.year] and
[Calendar.day] take the rite AND the sanctoral layer (Rite.t carries no
layer of its own -- see rite_ef.mli's own note on why), so step 1's
sanctoral-proper case needs the REAL data/ef/sanctoral.sexp +
data/ef/adjustments.sexp loaded, not a bare [Rite_ef.context]. Relative
to this test's own build directory (_build/default/test/); test/dune
declares both as deps. *)
let sanctoral_path = "../data/ef/sanctoral.sexp"
let adjustments_path = "../data/ef/adjustments.sexp"
let lectionary_path = "../data/ef/lectionary.sexp"
let commons_path = "../data/ef/commons.sexp"
let real_layer () =
let layer =
match Layer.load Vocab_ef.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 Vocab_ef.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
(* [Rite_ef.context] takes [~lectionary] (fix round 1, coordinator review):
the module used to load data/ef/lectionary.sexp itself, as a side effect
of being linked, which meant a bare `dune build` produced a `colitur`
that died at startup on EVERY subcommand -- including ones (`easter`)
that touch no lectionary data at all -- the moment that file was absent
from the default build target. Caller-supplied now, same as the
sanctoral layer above. *)
let real_lectionary () =
match Lectionary.load lectionary_path with
| Ok l -> l
| Error e -> Alcotest.failf "%s: failed to load: %s" lectionary_path e
(* The Commons travel the same caller-supplied seam (Task 6). Note that
[Commons.load]'s own validation runs here on the committed file: a
duplicate common id, a duplicate assignment, an empty formulary, or an
assignment naming a common that does not exist all come back as [Error]
and fail every test in this file rather than degrading silently to "this
saint has no Common". *)
let real_commons () =
match Lectionary_ef.Commons.load commons_path with
| Ok c -> c
| Error e -> Alcotest.failf "%s: failed to load: %s" commons_path e
(* [Calendar.day] (not [year]): the liturgical year "opening in civil year y"
is Advent-anchored (RG 61), so [Calendar.year _ _ 2030] covers Advent 2030
through November 2031 -- it would never contain 13 January 2030, which
belongs to the liturgical year that opened in Advent 2029. [Calendar.day]
finds the containing liturgical year itself; see calendar.mli. *)
let day y m d =
let date = match Date.make ~year:y ~month:m ~day:d with
| Ok x -> x | Error e -> Alcotest.fail e in
Calendar.day
(Rite_ef.context ~lectionary:(real_lectionary ()) ~commons:(real_commons ()))
(real_layer ()) date
(* Same resolution, with a caller-supplied Commons table instead of the
committed one -- the seam the whole design rests on, and the only way to
exercise step 4's guard, which is deliberately a no-op on shipped data. *)
let day_with ~commons y m d =
let date = match Date.make ~year:y ~month:m ~day:d with
| Ok x -> x | Error e -> Alcotest.fail e in
Calendar.day
(Rite_ef.context ~lectionary:(real_lectionary ()) ~commons)
(real_layer ()) date
let refs (ld : _ Liturgical_day.t) =
List.map (fun c -> c.Citation.reference) ld.citations
(* Chain step 1: the observed celebration's own proper wins.
NOT 2030: that is the one pinned collision year (test_golden.ml,
[test_holy_family_excludes_baptism_2030]) where 13 January is itself the
Holy Family Sunday and RG 112(a) excludes the Baptism commemoration
entirely -- [observed] there is [ef-time-after-epiphany-sunday-1], not
this slug, so it is the wrong year to exercise step 1 against. 2026 is an
ordinary year (13 January a Tuesday, no Sunday collision), where the
fixed Commemoration of the Baptism of the Lord (Class2, subject Lord,
data/ef/sanctoral.sexp) is observed outright. *)
let test_step1_sanctoral_proper () =
Alcotest.(check (list string))
"13 January 2026, Commemoration of the Baptism of the Lord"
[ "Isa 60:1-6"; "John 1:29-34" ]
(refs (day 2026 1 13))
(* Chain step 2: the day's own temporal slug has a proper (Lent has one daily). *)
let test_step2_lenten_feria_has_its_own () =
Alcotest.(check (list string))
"Monday of the 1st week of Lent is not the Sunday's Mass"
[ "Ezech 34:11-16"; "Matt 25:31-46" ]
(refs (day 2026 2 23))
(* Fix round 1 (coordinator review, Important finding 2): neither test above
actually distinguishes step 1 from step 2 -- both survive swapping the
chain order. 13 January 2026's temporal slug (ef-time-after-epiphany-1-
tuesday) has no lectionary entry at all, so a swapped chain falls through
to the same []-then-sanctoral answer; 23 February 2026's OBSERVED
celebration IS the temporal office (ef-lent-1-monday carries no sanctoral
entry of its own), so [observed.citations] and the temporal-slug lookup
are the same lookup wearing two names -- order is a no-op either way.
19 March 2026 (St Joseph) genuinely needs step 1 to run FIRST: the
observed celebration (Joseph, Class1, a real sanctoral entry with its own
citations) and the day's own temporal slug (ef-lent-4-thursday, ALSO a
real lectionary entry, with different citations) disagree. Verified
directly against the real data (not transcribed): both value pairs below
were read off the actual resolved day and the actual
data/ef/lectionary.sexp entry, not assumed. *)
let test_step1_wins_over_a_competing_step2_entry () =
Alcotest.(check (list string))
"19 March 2026: Joseph's own proper wins over Lent 4 Thursday's, which the temporal slug also has"
[ "Ecclus 45:1-6"; "Matt 1:18-21" ]
(refs (day 2026 3 19))
(* Fix round 1 (coordinator review): the mirror-image pin for step 2 -- a day
whose OBSERVED celebration carries no citations of its own (Holy Family,
synthesised by [Temporal_ef] itself, not sourced from
data/ef/sanctoral.sexp, so [Celebration.citations] is empty) falls
through to the temporal slug, and the temporal slug's own lectionary
entry is genuinely Holy Family's Mass, not the Baptism's -- RG 112(a)
(see test_golden.ml's own [test_holy_family_excludes_baptism_2030])
excludes the Baptism from this day entirely, so there is no sanctoral
citation anywhere to fall back to even in principle. Verified directly
against the real data. *)
let test_step2_holy_family_reached_through_temporal_slug () =
Alcotest.(check (list string))
"13 January 2030: Holy Family reached via the temporal slug, the Baptism entirely absent"
[ "Col 3:12-17"; "Luke 2:42-52" ]
(refs (day 2030 1 13))
(* Chain step 3: a feria with no proper of its own says the preceding
Sunday's Mass. *)
let test_step3_advent_feria_resumes_sunday () =
Alcotest.(check (list string))
"Monday after Advent I says Advent I's Mass"
[ "Rom 13:11-14"; "Luke 21:25-33" ]
(refs (day 2025 12 1))
(* Task 8, fix round 1 (coordinator review, Important 3(a)): this test used
to pin 2025-12-29 (a Monday), reached by resuming its preceding Sunday's
temporal identity. That date no longer exercises step 3 at all --
tools/bootstrap_lectionary.ml's own [nativity_octave_entries] now gives
the fixed Nativity-Octave slug (RG 91 entry 17, "ef-nativity-octave-
day-5") its OWN direct citation ("Diebus infra octavam Nativitatis
Domini", Tit. 3,4-7/Luc. 2,15-20), so 29 December resolves at STEP 2,
unconditionally, regardless of weekday. Every civil day in 29-31
December now resolves the same way -- there is no date left in that
specific window this test could use to exercise step 3.
Replaced with 2028-01-05 (a Wednesday in Holy Name week, "ef-christmas-
1-wednesday"), which still has no citation of its own and genuinely
resumes ITS preceding Sunday, 2028-01-02 -- confirmed a real Sunday
that year, and confirmed to be Holy Name Sunday specifically (its own
[ef-holy-name-sunday] slug, RG 17(a)), not a coincidence: the whole
POINT of this test is a Christmas-season feria genuinely falling
through to step 3, and this is the closest remaining civil-calendar
case to the original 29-31 December window that still does. *)
let test_step3_christmas_feria_resumes_sunday () =
Alcotest.(check (list string))
"Wednesday of Holy Name week resumes Holy Name Sunday's Mass"
[ "Gal 4:1-7"; "Luke 2:33-40" ]
(refs (day 2028 1 5))
(* Fix round 1 (coordinator review, Important finding 1): the original test
here (2028-12-26) did not exercise step 3 at all -- 26 December is always
Stephen (Class2, a real sanctoral entry with its own citations, fixed
regardless of weekday), so that date resolves entirely at step 1 and never
reaches step 2 or 3. Its justifying comment was also factually wrong: 24
December's TEMPORAL slug is [ef-nativity-vigil], identical to its OBSERVED
slug -- [Temporal_ef.named] hard-codes the Vigil for 24 December ahead of
any Sunday computation, so there is no temporal/observed split on that
date to exploit in the first place.
2025-02-03 genuinely discriminates, verified directly against the real
data (data/ef/sanctoral.sexp, data/ef/lectionary.sexp) and the real
resolver, not transcribed: 2025-02-02 (a Sunday) is observed as the
Purification of the BVM (RG 16(a): a Feast of the Lord takes the Sunday's
place outright), with its own citations Mal 3:1-4 / Luke 2:22-32 -- but
its TEMPORAL identity is [ef-time-after-epiphany-sunday-4], whose
lectionary entry is the different Rom 13:8-10 / Matt 8:23-27. 2025-02-03
(Monday) has no sanctoral office of its own and no entry for its own
temporal slug ([ef-time-after-epiphany-4-monday], absent from
data/ef/lectionary.sexp), so it reaches step 3 and must return the
Sunday's TEMPORAL readings (Rom 13:8-10 / Matt 8:23-27). A "step 3 reads
the observed office instead of the temporal one" bug would instead return
the Purification's (Mal 3:1-4 / Luke 2:22-32) -- different, checkable
values, not a bare length check. *)
let test_step3_uses_temporal_not_observed () =
Alcotest.(check (list string))
"3 February 2025 takes the preceding Sunday's TEMPORAL Mass (Time after Epiphany IV), not the Purification's"
[ "Rom 13:8-10"; "Matt 8:23-27" ]
(refs (day 2025 2 3))
(* Termination: a Sunday that reaches step 3 would consult itself. It must
not: the guard is weekday <> Sun. *)
let test_step3_sunday_does_not_recurse () =
let d = day 2026 6 14 in
Alcotest.(check int) "a Sunday resolves without looping" 2 (List.length (refs d))
(* ---------------------------------------------------------------------- *)
(* Chain step 4: the Commons (Task 6). *)
(* *)
(* Step 4 EXECUTES SECOND, between step 1 and step 2, not last as the task *)
(* brief sketched -- see lectionary_ef.ml's own branch comment for the *)
(* measurement and the primary-source warrant. The step NUMBER is kept as *)
(* the plan wrote it so that every "step 3" already recorded elsewhere *)
(* still names the same branch. *)
(* *)
(* Every expected value below is quoted from data/ef/commons.sexp, which in *)
(* turn cites the Missal page it was read from. There is NO oracle for any *)
(* of it: lectio has no readings for these feasts (that is why Task 6 *)
(* exists) and missalemeum's 2026-2027 window never observes one of them. *)
(* These pins and the golden ones are the entire regression net. *)
(* ---------------------------------------------------------------------- *)
(* 2038-03-06 observes Sts Perpetua and Felicity: a III-class feast that
beats the Septuagesima II Saturday feria. The Missal sends 6 March to
"Missa Me exspectaverunt, de Communi non Virginum I loco", whose printed
Epistle and Gospel are these.
This test is also the ORDERING pin, and it is the reason it uses explicit
values rather than a length check: with step 4 placed last (the brief's
sketch) this day resolves through step 2 to the feria's own
"2 Cor. 11:19-33; 12:1-9" / "Luke 8:4-15" instead -- a green
two-citation answer that a [List.length = 2] assertion would not
distinguish from the right one. *)
let test_step4_commons_perpetua_and_felicity () =
Alcotest.(check (list string))
"6 March 2038: Sts Perpetua and Felicity take the Common of non-Virgins I"
[ "Ecclus 51:1-8, 12"; "Matt 13:44-52" ]
(refs (day 2038 3 6))
(* 2038-03-09, St Frances of Rome: "Missa Cognovi, de Communi non Virginum
II loco". Same year, three days later, and a DIFFERENT Common -- so this
pin also proves the assignment table is consulted per saint rather than
one Common being handed to everything that reaches step 4. Its temporal
slug (ef-septuagesima-3-tuesday) resolves to 1 Cor. 13:1-13 / Luke
18:31-43, so the wrong-order failure is again a plausible-looking green. *)
let test_step4_commons_frances_of_rome () =
Alcotest.(check (list string))
"9 March 2038: St Frances of Rome takes the Common of non-Virgins II"
[ "Prov 31:10-31"; "Matt 13:44-52" ]
(refs (day 2038 3 9))
(* 2008-04-04, St Isidore: "Missa In medio, de Communi Doctorum". A
Paschaltide witness, where step 2 would otherwise have supplied the
Easter-week feria's 1 John 5:4-10 / John 20:19-31. *)
let test_step4_commons_isidore () =
Alcotest.(check (list string))
"4 April 2008: St Isidore takes the Common of Doctors"
[ "2 Tim 4:1-8"; "Matt 5:13-19" ]
(refs (day 2008 4 4))
(* 2005-04-05, St Vincent Ferrer: "Missa Os iusti, de Communi Confessoris
non Pontificis I loco". *)
let test_step4_commons_vincent_ferrer () =
Alcotest.(check (list string))
"5 April 2005: St Vincent Ferrer takes the Common of a Confessor not a Bishop I"
[ "Ecclus 31:8-11"; "Luke 12:35-40" ]
(refs (day 2005 4 5))
(* Step 1 still wins over step 4 for a saint the Missal prints a full Mass
for. St John of God (8 March) is the sharp case: his Epistle IS the
Common of a Confessor not a Bishop's own (Ecclus 31:8-11), but the Missal
assigns him a different GOSPEL (Mt 22:34-46, not that Common's Luke
12:35-40), so a chain that reached for a Common here would be caught by
the Gospel alone. He has no assignment in commons.sexp at all -- this
asserts the proper arrived via adjustments.sexp and step 1. *)
let test_step1_proper_beats_any_common_john_of_god () =
Alcotest.(check (list string))
"8 March 2038: St John of God's own printed Mass, not the Common that shares its Epistle"
[ "Ecclus 31:8-11"; "Matt 22:34-46" ]
(refs (day 2038 3 8));
Alcotest.(check bool)
"and he is deliberately absent from the Commons assignment table" true
(Lectionary_ef.commons_for ~commons:(real_commons ())
(Slug.of_string_exn "john-of-god")
= None)
(* The other two proper-Mass saints a real year ever observes. Both would
otherwise silently emit their feria's Mass. *)
let test_step1_proper_thomas_aquinas () =
Alcotest.(check (list string))
"7 March 2011: St Thomas Aquinas's own proper"
[ "Wis 7:7-14"; "Matt 5:13-19" ]
(refs (day 2011 3 7))
let test_step1_proper_francis_of_paola () =
Alcotest.(check (list string))
"2 April 2008: St Francis of Paola's own proper"
[ "Phil 3:7-12"; "Luke 12:32-34" ]
(refs (day 2008 4 2))
(* A day whose observed office IS its temporal office is untouched by step 4
on the SHIPPED data. 4 July 2026 is such a day, and the case most at risk:
it is an RG 78 Saturday Office of the BVM, which deliberately REUSES the
ordinary ferial slug (temporal_ef.ml's [bvm_saturday_names], "Slug"
paragraph).
This test alone does NOT pin the guard -- it passes with the guard
removed, because no shipped assignment names a temporal slug. It is the
baseline the next test needs, and it is labelled as such (fix round 1,
coordinator review: its comment previously claimed to "pin the guard
behaviourally", which was false -- the reviewer forced
[sanctoral_office = true] and all 357 tests stayed green). *)
let test_step4_leaves_a_temporal_office_alone_on_shipped_data () =
Alcotest.(check (list string))
"4 July 2026 (a BVM Saturday) keeps its ferial Mass"
[ "1 Pet 3:8-15."; "Matt 5:20-24." ]
(refs (day 2026 7 4))
(* THE GUARD, pinned. The guard is a no-op on shipped data by design, so the
only way to exercise it is to build the table it defends against: a
SYNTHETIC [Commons.t] that assigns a Common to a really-observed FERIAL
slug. That is exactly the future-overlay mistake the guard exists to
catch -- and note the blast radius it would have, since a ferial slug is
shared by every year in which that week and weekday coincide, not by one
date.
Built through [Commons.of_tables], so it passes the same validation the
committed file does -- this is a well-formed table with a wrong
assignment, not malformed data caught by the loader.
TEETH (verified, fix round 1): with the guard removed this returns the
Common of Abbots' Ecclus 45:1-6 / Matt 19:27-29 and the assertion fails.
The second check is what makes the first one mean something: it proves
the hostile assignment really is present and resolvable, so the ferial
Mass wins because the GUARD refused it, not because the lookup missed. *)
let test_step4_guard_refuses_a_common_assigned_to_a_ferial_slug () =
let ferial_slug = Slug.of_string_exn "ef-time-after-pentecost-5-saturday" in
let abbots = Slug.of_string_exn "common-of-abbots" in
let hostile =
match
Lectionary_ef.Commons.of_tables
~commons:
[ (abbots,
[ { Citation.part = Citation.First; reference = "Ecclus 45:1-6" };
{ Citation.part = Citation.Gospel; reference = "Matt 19:27-29" } ]) ]
~assigned:[ (ferial_slug, abbots) ]
with
| Ok c -> c
| Error e -> Alcotest.failf "synthetic commons failed to build: %s" e
in
Alcotest.(check bool)
"the synthetic table really does resolve that ferial slug to a Common" true
(Lectionary_ef.commons_for ~commons:hostile ferial_slug <> None);
Alcotest.(check (list string))
"4 July 2026 keeps its ferial Mass even so: step 4 refuses a temporal office"
[ "1 Pet 3:8-15."; "Matt 5:20-24." ]
(refs (day_with ~commons:hostile 2026 7 4))
(* THE DATA ASSERTION, and the one that cannot drift: after Task 6, every
sanctoral entry that can ever BE the observed office -- i.e. every
[status Feast] entry -- either carries its own proper or has a Common
assigned. Stated over the real loaded layer rather than as a hard-coded
list of fifteen names, so that a future re-bootstrap adding a
readingless class-3 feast fails here instead of silently emitting its
feria's Mass.
[Commemoration_only] entries are excluded deliberately and not as an
oversight: they are never the observed celebration (Precedence never
returns one as [observed]), and [readings] only ever consults
[observed], so their empty citations are unreachable. 104 of the 119
entries with no citations are of that kind. *)
let test_every_observable_sanctoral_feast_has_readings () =
let commons = real_commons () in
let layer = real_layer () in
let gaps =
List.filter_map
(fun (entry : Vocab_ef.rank Layer.entry) ->
let cel = entry.Layer.cel in
if cel.Celebration.status <> Celebration.Feast then None
else if cel.Celebration.citations <> [] then None
else if Lectionary_ef.commons_for ~commons cel.Celebration.slug <> None then None
else Some (Slug.to_string cel.Celebration.slug))
layer.Layer.entries
in
Alcotest.(check (list string))
"every sanctoral Feast has either a proper or an assigned Common" []
(List.sort_uniq compare gaps)
(* The seven saints the Missal sends to a Common, and the exact Common each
one is sent to -- quoted per saint, with its Missal citation, in
data/ef/commons.sexp. Named explicitly (in addition to the generic
invariant above) because three of them -- Benedict, Gregory the Great,
Patrick -- are NEVER the observed office anywhere in 1583-9999 (12, 17
and 21 March always fall to a Lenten or Passiontide feria that outranks a
III-class feast), so no end-to-end pin can reach them and this table is
their only coverage.
The plan expected Benedict, Patrick and Gregory the Great to have PROPERS
and Thomas Aquinas likewise; the Missal gives a proper only to Thomas
Aquinas. All three reversals were verified on the page images
(pp. 488, 492) as well as in both OCR text layers. *)
let test_step4_assignment_table () =
let commons = real_commons () in
let assigned =
List.map
(fun (saint, common) -> (Slug.to_string saint, Slug.to_string common))
(Lectionary_ef.Commons.assignments commons)
in
Alcotest.(check (list (pair string string)))
"the Commons assignment table, exactly"
[ ("benedict", "common-of-abbots");
("frances-rome", "common-of-non-virgins-2");
("gregory-the-great", "common-of-supreme-pontiffs");
("isidore-of-seville", "common-of-doctors");
("patrick", "common-of-a-confessor-bishop-1");
("sts-felicitas-perpetua", "common-of-non-virgins-1");
("vincent-ferrer", "common-of-a-confessor-not-a-bishop-1") ]
assigned
(* The three Commons no real year reaches, resolved through [commons_for] so
that the assignment AND the formulary behind it are both exercised. *)
let test_step4_unreachable_commons_still_resolve () =
let commons = real_commons () in
let for_saint s = Lectionary_ef.commons_for ~commons (Slug.of_string_exn s) in
let refs_of = function
| None -> [ "<no common>" ]
| Some cs -> List.map (fun c -> c.Citation.reference) cs
in
Alcotest.(check (list string))
"St Benedict (21 March), Common of Abbots"
[ "Ecclus 45:1-6"; "Matt 19:27-29" ]
(refs_of (for_saint "benedict"));
Alcotest.(check (list string))
"St Gregory the Great (12 March), Common of Supreme Pontiffs"
[ "1 Pet 5:1-4, 10-11"; "Matt 16:13-19" ]
(refs_of (for_saint "gregory-the-great"));
Alcotest.(check (list string))
"St Patrick (17 March), Common of a Confessor Bishop I"
[ "Ecclus 44:16-27; 45:3-20"; "Matt 25:14-23" ]
(refs_of (for_saint "patrick"))
(* The five proper-Mass saints no real year reaches, asserted on the layer
rather than end-to-end, for the same reason. *)
let test_step4_unreachable_propers_are_present () =
let layer = real_layer () in
let refs_of slug =
match
List.find_opt
(fun (e : Vocab_ef.rank Layer.entry) ->
Slug.equal e.Layer.cel.Celebration.slug (Slug.of_string_exn slug))
layer.Layer.entries
with
| None -> [ "<absent from the layer>" ]
| Some e -> List.map (fun c -> c.Citation.reference) e.Layer.cel.Celebration.citations
in
Alcotest.(check (list string))
"Forty Holy Martyrs of Sebaste (10 March)"
[ "Heb 11:33-39"; "Luke 6:17-23" ]
(refs_of "forty-holy-martyrs-of-sebaste");
Alcotest.(check (list string))
"St Cyril of Jerusalem (18 March)"
[ "Ecclus 39:6-14"; "Matt 10:23-28" ]
(refs_of "cyril-of-jerusalem");
(* The one genuine disagreement between the two printings: scan1's edition
misprints this Gospel as "Luc. 2, 26-38" (confirmed on its own page
image, so a typographical error, not OCR). Luke 1:26-38 is settled by
scan2's edition, by the pericope text in both, and by the same edition
citing the identical pericope correctly two pages later at the
Annunciation. See data/ef/adjustments.sexp for the full account. *)
Alcotest.(check (list string))
"St Gabriel the Archangel (24 March)"
[ "Dan 9:21-26"; "Luke 1:26-38" ]
(refs_of "gabriel-the-archangel");
(* Wis 10:10-17 here versus 10:10-14 for John of Capistrano below: a real
difference, agreed by both scans and both page images. *)
Alcotest.(check (list string))
"St John Damascene (27 March)"
[ "Wis 10:10-17"; "Luke 6:6-11" ]
(refs_of "john-damascene");
Alcotest.(check (list string))
"St John of Capistrano (28 March)"
[ "Wis 10:10-14"; "Luke 9:1-6" ]
(refs_of "john-of-capistrano")
(* [Commons.load] must reject, not silently degrade, the four data defects
that are indistinguishable downstream from "this saint has no Common".
Written against strings rather than the committed file so the committed
file stays clean. *)
let test_commons_load_rejects_bad_data () =
(* [Filename.temp_file] + [Fun.protect], exactly as test_lectionary.ml's
own [with_temp_file] does it (fix round 1, coordinator review: this
used four FIXED names in the temp dir and removed none of them, so
concurrent runs collided and the files leaked). *)
let with_temp_file contents f =
let path = Filename.temp_file "commons_test" ".sexp" in
let oc = open_out path in
output_string oc contents;
close_out oc;
Fun.protect ~finally:(fun () -> Sys.remove path) (fun () -> f path)
in
let err what contents =
with_temp_file contents (fun path ->
match Lectionary_ef.Commons.load path with
| Ok _ -> Alcotest.failf "%s: expected Error, got Ok" what
| Error e -> e)
in
let good_common = "(c-a (((part First) (reference \"A 1:1\")) ((part Gospel) (reference \"B 2:2\"))))" in
let contains needle haystack =
let n = String.length needle and h = String.length haystack in
let rec go i = i + n <= h && (String.sub haystack i n = needle || go (i + 1)) in
go 0
in
let check_msg label needle msg =
Alcotest.(check bool) (Printf.sprintf "%s names the culprit (%s)" label needle) true
(contains needle msg)
in
check_msg "duplicate common" "duplicate common"
(err "duplicate common"
(Printf.sprintf "((commons (%s %s)) (assigned ()))" good_common good_common));
check_msg "duplicate assignment" "duplicate assignment"
(err "duplicate assignment"
(Printf.sprintf "((commons (%s)) (assigned ((s c-a) (s c-a))))" good_common));
check_msg "empty formulary" "no citations"
(err "empty formulary" "((commons ((c-a ()))) (assigned ()))");
check_msg "unknown common" "unknown common"
(err "unknown common"
(Printf.sprintf "((commons (%s)) (assigned ((s c-missing))))" good_common));
(* A missing file is an [Error] too, never an exception -- the contract
[Lectionary.load] already makes and the reason neither is read at
module-initialisation time. *)
(match Lectionary_ef.Commons.load "../data/ef/no-such-commons.sexp" with
| Ok _ -> Alcotest.fail "a missing commons file must not load"
| Error _ -> ());
(* [empty] is the identity: every lookup [None], nothing invented. *)
Alcotest.(check bool) "Commons.empty resolves nothing" true
(Lectionary_ef.commons_for ~commons:Lectionary_ef.Commons.empty
(Slug.of_string_exn "benedict")
= None)
let suite =
[ ("step 1: sanctoral proper", `Quick, test_step1_sanctoral_proper);
("step 2: own temporal proper", `Quick, test_step2_lenten_feria_has_its_own);
("step 1 wins over a competing step 2 entry", `Quick,
test_step1_wins_over_a_competing_step2_entry);
("step 2: Holy Family reached through the temporal slug, Baptism absent", `Quick,
test_step2_holy_family_reached_through_temporal_slug);
("step 3: Advent feria resumes the preceding Sunday", `Quick,
test_step3_advent_feria_resumes_sunday);
("step 3: Christmas feria resumes the preceding Sunday", `Quick,
test_step3_christmas_feria_resumes_sunday);
("step 3: 3 Feb 2025 takes the Sunday's temporal Mass, not the Purification's", `Quick,
test_step3_uses_temporal_not_observed);
("step 3: a Sunday does not recurse into itself", `Quick,
test_step3_sunday_does_not_recurse);
("step 4: Perpetua and Felicity take the Common of non-Virgins I", `Quick,
test_step4_commons_perpetua_and_felicity);
("step 4: Frances of Rome takes the Common of non-Virgins II", `Quick,
test_step4_commons_frances_of_rome);
("step 4: Isidore takes the Common of Doctors", `Quick, test_step4_commons_isidore);
("step 4: Vincent Ferrer takes the Common of a Confessor not a Bishop I", `Quick,
test_step4_commons_vincent_ferrer);
("step 1 beats step 4: John of God's proper, not the Common sharing its Epistle", `Quick,
test_step1_proper_beats_any_common_john_of_god);
("step 1: Thomas Aquinas's proper", `Quick, test_step1_proper_thomas_aquinas);
("step 1: Francis of Paola's proper", `Quick, test_step1_proper_francis_of_paola);
("step 4 leaves a temporal office alone on shipped data (BVM Saturday)", `Quick,
test_step4_leaves_a_temporal_office_alone_on_shipped_data);
("step 4's GUARD refuses a Common wrongly assigned to a ferial slug", `Quick,
test_step4_guard_refuses_a_common_assigned_to_a_ferial_slug);
("every observable sanctoral Feast has a proper or a Common", `Quick,
test_every_observable_sanctoral_feast_has_readings);
("the Commons assignment table, exactly", `Quick, test_step4_assignment_table);
("the three Commons no real year reaches still resolve", `Quick,
test_step4_unreachable_commons_still_resolve);
("the five propers no real year reaches are present", `Quick,
test_step4_unreachable_propers_are_present);
("Commons.load rejects the four silent-degradation defects", `Quick,
test_commons_load_rejects_bad_data) ]
|