summaryrefslogtreecommitdiff
path: root/tools/bootstrap_lectionary.ml
blob: 1e9b8edfd17a7389cbfa3b325be9275416e7d10b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
(* Bootstraps data/ef/lectionary.sexp from lectio's tridentine-lectionary.ini.
   Lives in tools/ and not the kernel for the same reason
   bootstrap_sanctoral.ml does: reading someone else's INI needs a reader,
   which does not belong in a pure kernel.

   Only `first` and `gospel` are mapped -- the plan's Global Constraints fix
   the scope at Epistle + Gospel, and lectio's data carries nothing else.

   Links `rite_ef` (fix round 1, coordinator review, Critical 2): the
   generator used to have no way to check its own [colitur_keys] table
   against reality, which is exactly how a translation this table forgot
   (Lent's own Ember days) shipped silently -- both engines independently
   fall through to the same wrong ferial answer there, so even the
   differential could not see it (the "Holy Thursday was violet in both"
   shape this project's own CLAUDE.md already names). [assert_reachable]
   below sweeps {!Rite_ef.Temporal_ef.temporal} directly -- no sanctoral
   layer, no lectionary of its own, no circularity with the file this tool
   is generating -- exactly the same "code, not data" seam
   tools/bootstrap_sanctoral.ml already links this library for. *)
open Colitur_kernel

let default_source = "../lectio/internal/caldata/tridentine-lectionary.ini"
let default_dest = "data/ef/lectionary.sexp"

let die fmt =
  Printf.ksprintf
    (fun s -> prerr_endline ("bootstrap_lectionary: " ^ s); exit 1)
    fmt

type section = { name : string; fields : (string * string) list }

let parse_ini path =
  let ic = try open_in path with Sys_error e -> die "%s" e in
  let sections = ref [] and cur = ref None in
  let flush () =
    match !cur with
    | Some (n, fs) -> sections := { name = n; fields = List.rev fs } :: !sections
    | None -> ()
  in
  (try
     while true do
       let line = String.trim (input_line ic) in
       if line = "" || line.[0] = ';' || line.[0] = '#' then ()
       else if line.[0] = '[' then begin
         flush ();
         cur := Some (String.sub line 1 (String.length line - 2), [])
       end
       else
         match String.index_opt line '=' with
         | None -> die "%s: cannot parse line %S" path line
         | Some i ->
             let k = String.trim (String.sub line 0 i) in
             let v =
               String.trim (String.sub line (i + 1) (String.length line - i - 1))
             in
             cur :=
               (match !cur with
               | Some (n, fs) -> Some (n, (k, v) :: fs)
               | None -> die "%s: field %S before any section" path k)
     done
   with End_of_file -> ());
  flush ();
  close_in ic;
  List.rev !sections

(* Task 8 (branch ef-lectionary, differential fix round): lectio's own ini
   section names are NOT always the slug {!Rite_ef.Temporal_ef} computes
   for the IDENTICAL office. Confirmed the hard way: the first version of
   this generator carried section names through {!Slug.of_string}
   verbatim, so the data these renamed families need was sitting in this
   very file all along -- under lectio's spelling -- while
   [Lectionary_ef.readings]' step 2 looked it up under colitur's own,
   found nothing, and silently fell through to step 3's ferial-resumption
   answer instead. Invisible until test/test_differential.ml first
   compared citation CONTENT (this task): a day's SLUG/rank/colour already
   matched, so nothing before this task ever noticed the READING was
   wrong. [colitur_keys] is the closed translation table -- the SAME
   families test/test_differential.ml's own Layer A ([norm_slug]) already
   established as pure vocabulary, carrying no liturgical substance, not
   re-derived here. Kept in the generator, not in [Lectionary_ef.readings]
   itself, because a bootstrap step exists exactly to translate an
   external vocabulary into colitur's own ONCE, at data-generation time --
   baking an alias table into the rite's own runtime lookup would just
   move the vocabulary problem, not solve it.

   A translation can widen ONE ini section into several colitur keys; it
   never narrows or drops one, and every key not named here passes through
   unchanged (e.g. "ef-easter-8-{monday,tuesday,thursday}", the three
   non-Ember Pentecost-octave ferias colitur does NOT rename). *)
let weekdays = [ "monday"; "tuesday"; "wednesday"; "thursday"; "friday"; "saturday" ]

let colitur_keys name =
  if String.equal name "ef-christmas-sunday-0" then
    (* Task 9 (branch ef-lectionary, layer-4 oracle): CORRECTED -- this
       branch used to ALSO widen into "ef-holy-name-sunday" (see git
       history for the removed paragraph's own reasoning), on the claim
       that RG 17(a)'s Holy Name Sunday shares the IDENTICAL Mass with the
       historical "Sunday within the Octave of the Nativity" this ini
       section actually carries (Gal. 4,1-7 / Luc. 2,33-40). WRONG,
       DISPROVED by an INDEPENDENT primary source once this task built a
       real oracle comparison against it: missalemeum's own extraction for
       2026-01-04/2027-01-03 (both real Holy Name Sundays) shows Acts
       4:8-12 / Luke 2:21 instead, and docs/research/scan1.txt AND scan2.txt
       both confirm it, twice over, word for word ("SANCTISSIMI NOMINIS
       IESU, II classis... Lectio Actuum Apostolorum. Act. 4, 8-12...
       Sequentia sancti Evangelii secundum Lucam. Luc. 2, 21",
       scan1.txt:6579-6608, scan2.txt:7225-7297) -- a DIFFERENT, DISTINCT
       Mass from the Sunday-within-the-Octave's own (still correctly
       carried by THIS key, "ef-christmas-sunday-0", unaffected by this
       correction). The widen is removed; Holy Name Sunday's own citation
       is now hand-authored directly, [holy_name_entries] below, sourced
       from the Missal rather than mis-derived from an unrelated ini
       section by analogy. *)
    [ "ef-christmas-sunday-0" ]
  else if String.equal name "ef-easter-8-wednesday" then [ "ef-pentecost-ember-wed" ]
  else if String.equal name "ef-easter-8-friday" then [ "ef-pentecost-ember-fri" ]
  else if String.equal name "ef-easter-8-saturday" then [ "ef-pentecost-ember-sat" ]
  else if String.equal name "ef-lent-1-wednesday" then
    (* Critical 2, fix round 1 (coordinator review): the Lenten Ember days
       (RG 91 entry 18, "Quatuor Tempora... post primam dominicam
       Quadragesimae") fall on the Wednesday/Friday/Saturday of Lent's own
       first week -- the SAME three civil days colitur names
       "ef-lent-ember-{wed,fri,sat}" (Rite_ef.Temporal_ef's own [ember],
       checked ahead of the generic week-numbering fallback, so colitur
       NEVER emits "ef-lent-1-wednesday/friday/saturday" as its own slug
       for ANY civil day -- confirmed by sweeping [Temporal_ef.temporal]
       directly, not assumed; [assert_reachable] below would now catch it
       if that stopped being true). A RENAME, not a widen, unlike
       "ef-christmas-sunday-0" above: colitur has no second, legitimate use
       for the un-renamed key the way it does there. temporal_ef.ml's own
       comment on [ember] previously claimed "lectio has no Ember slug for
       [Lent]" -- true only in the sense that lectio's OWN naming is the
       generic "ef-lent-1-<weekday>" family, not that the DATA is missing;
       it is right there in the ini, just unreachable under colitur's own
       spelling until this rename. That comment is corrected alongside
       this fix. *)
    [ "ef-lent-ember-wed" ]
  else if String.equal name "ef-lent-1-friday" then [ "ef-lent-ember-fri" ]
  else if String.equal name "ef-lent-1-saturday" then [ "ef-lent-ember-sat" ]
  else if String.equal name "ef-passiontide-0-tuesday" then
    (* Important, fix round 2 (coordinator review): the general
       Passiontide rename below carries every OTHER weekday's citation
       straight through from lectio's own ini, correct for Passion week
       (Critical 1's own fix, this comment's own sibling branch). Tuesday
       is the ONE exception, already flagged, not yet acted on, by
       [holy_week_entries]'s own footnote in the previous fix round: the
       ini's "ef-passiontide-0-tuesday" section is not Passion week's own
       Mass at all -- it is HOLY Tuesday's (Jer 11:18-20 / the Passion
       according to Mark), confirmed independently against BOTH scans at
       the time. Passion week's own real Tuesday Mass ("Feria tertia, III
       classis, Statio ad S. Cyriacum", scan1.txt:11085-11121, corroborated
       scan2.txt:12063-12104) is Dan 14:27, 28-42 / John 7:1-13 -- a
       DIFFERENT Epistle and a DIFFERENT Gospel from what the ini section
       carries. Excluded here (empty translation, not a rename) so the
       wrong ini value is never used; the correct one is hand-authored
       below, [passion_tuesday_entry]. lectio itself never surfaces this,
       since its own computed reading is drawn from the SAME (Holy
       Tuesday's) data on the day it labels "Passion Tuesday" too --
       data/ef/expected-divergences.sexp's own new entry records the
       resulting divergence. *)
    []
  else if String.equal name "ef-advent-ember-sat" then
    (* Important, fix round 3 (coordinator review): lectio's own ini value
       for this key is St THOMAS THE APOSTLE's Mass (Eph 2:19-22 / John
       20:24-29, "doubting Thomas" -- 21 December, which the Advent Ember
       Saturday's own civil date is NOT), not the real "Sabbato Quatuor
       Temporum Adventus". Same shape as the Passion-Tuesday exclusion
       above and C26's own entry: a wrong value inherited verbatim from
       lectio's ini, invisible to the differential because lectio's own
       computed reading is drawn from the identical wrong source. Excluded
       here; the correct citation is hand-authored below,
       [ember_saturday_corrections]. *)
    []
  else if String.equal name "ef-september-ember-sat" then
    (* Important, fix round 3 (coordinator review): lectio's own ini value
       for this key is ST MATTHEW's Mass (Ezek 1:10-14 / Matt 9:9-13, the
       calling of Matthew -- 21 September, which the September Ember
       Saturday's own civil date is NOT), the identical shape as
       "ef-advent-ember-sat" immediately above. Excluded here; the correct
       citation is hand-authored below, [ember_saturday_corrections]. *)
    []
  else (
    match List.find_opt (fun wd -> String.equal name ("ef-passiontide-0-" ^ wd)) weekdays with
    | Some wd ->
        (* Critical 1, fix round 1 (coordinator review): the original
           version of this table widened "ef-passiontide-0-<weekday>" into
           BOTH "ef-passiontide-1-<weekday>" (Passion week) AND
           "ef-passiontide-2-<weekday>" (HOLY WEEK, including the entire
           Sacred Triduum) on the strength of lectio's own citation being
           byte-identical between the two weeks. That only proves lectio
           CONFLATES the two weeks -- it has no Holy Week propers of its
           own -- not that the Missal does: Holy Monday's real Mass
           (docs/research/scan1.txt, "Feria II Hebdomadae sanctae, I
           classis") is Isa 50:5-10 / John 12:1-9, nothing like
           Passion-week Monday's Jonas 3:1-10 / John 7:32-39 that the widen
           was putting there, and Holy Thursday's is 1 Cor 11:20-32 / John
           13:1-15 -- the Mass of the Lord's Supper reading Passion
           Sunday's own ferial Mass was the actual defect, not merely a
           citation nicety. Narrowed to Passion week ONLY; Holy Week's own
           citations (all six weekdays, fix round 2) are hand-authored
           below, [holy_week_entries]. *)
        [ "ef-passiontide-1-" ^ wd ]
    | None -> [ name ])

let convert sec =
  let cite part key =
    match List.assoc_opt key sec.fields with
    | None | Some "" -> None
    | Some reference -> Some { Citation.part; reference }
  in
  let cs = List.filter_map Fun.id [ cite Citation.First "first"; cite Citation.Gospel "gospel" ] in
  if cs = [] then die "%s: no first/gospel field" sec.name;
  if List.length cs = 1 then
    die "%s: has one reading, not two -- an Epistle without a Gospel (or the \
         reverse) is malformed and must be investigated, not silently shipped"
      sec.name;
  List.map
    (fun key ->
      match Slug.of_string key with
      | Ok slug -> (slug, cs)
      | Error e -> die "bad slug %S (translated from %S): %s" key sec.name e)
    (colitur_keys sec.name)

(* [ef-nativity-vigil] (RG 91 entry 5, {!Rite_ef.Temporal_ef}'s own [named])
   is a TEMPORAL office with no section of its own in THIS ini at all --
   lectio computes ITS citations from a different source file entirely,
   its SANCTORAL calendar (tridentine-calendar.ini's own
   "[vigil-of-christmas]" section: "reading.first = Rom 1:1-6",
   "reading.gospel = Matt 1:18-21"). colitur's own sanctoral bootstrap
   (Task 3) already carried those identical citations onto
   data/ef/sanctoral.sexp's own `vigil-of-christmas` entry -- inert there
   because data/ef/adjustments.sexp suppresses it (that overlay's own
   comment: the SAME celebration as this temporal office, not a second
   one) -- which corroborates this value independently rather than merely
   asserting it. Hand-authored here, the same discipline
   data/ef/adjustments.sexp's own RG 110 companion and Major Litanies
   `Add` directives already use for a genuine upstream-source gap this
   generator's own single-ini design cannot reach on its own. *)
let slug_or_die name = match Slug.of_string name with Ok s -> s | Error e -> die "%s" e

(* CITATION FORMAT, fix round 2 (coordinator review, Minor): every
   hand-authored reference below uses ENGLISH book abbreviations with a
   colon before the verse ("John 12:1-9"), matching the form already
   DOMINANT in this file's own ini-derived data (counted directly: "John"
   40 occurrences vs "Io." 2; "Luke" 34 vs "Luc." 3; "Titus" 1 vs "Tit." 3
   -- the Latin forms were ALL this generator's own earlier, inconsistent
   choice, not lectio's), the same "one form per book, matching what's
   already dominant" discipline data/ef/commons.sexp's own header already
   states for hand-authored citations. An earlier version of every entry
   below used Latin book names and Missal-style comma verse separators
   ("Isai. 50, 5-10"), inconsistent with the rest of the file and, in one
   entry, inconsistent with ITSELF (Ier./Mark mixed in the same pair) --
   all converted here, content unchanged, only the spelling. *)
let pair ~first ~gospel =
  [ { Citation.part = Citation.First; reference = first };
    { Citation.part = Citation.Gospel; reference = gospel } ]

let vigil_entries =
  [ (slug_or_die "ef-nativity-vigil", pair ~first:"Rom 1:1-6" ~gospel:"Matt 1:18-21") ]

(* Critical 1, fix round 1 (coordinator review): Holy Week's own Mass
   propers, hand-authored the same discipline [vigil_entries] above
   already uses for a genuine upstream-source gap -- lectio's own ini has
   no Holy Week data at all (see [colitur_keys]'s own Passiontide comment
   for the full account of what it has instead). Every citation below is
   Missal-verified TWICE, independently (docs/research/scan1.txt AND
   scan2.txt, the two different printings/scans Task 6 also cross-checked
   between):

   - [ef-passiontide-2-monday] (Holy Monday, "Feria II Hebdomadae sanctae,
     I classis"): Isa 50:5-10 / John 12:1-9 -- scan1.txt "Lectio Isaiae
     Prophetae... Isai. 50, 5-10" + "Sequentia... secundum Ioannem. Io. 12,
     1-9"; scan2.txt corroborates both citations word for word.
   - [ef-passiontide-2-tuesday] (Holy Tuesday, "Feria III Hebdomadae
     sanctae, I classis"): Jer 11:18-20 / the Passion according to Mark,
     14:32-72; 15:1-46 -- scan1.txt "Lectio Ieremiae Prophetae... Ier. 11,
     18-20" + "Evangelium Passionis et Mortis Domini secundum Marcum.
     14,32-72; 15,1-46"; scan2.txt corroborates both.
   - [ef-passiontide-2-thursday] (Holy Thursday, "Feria V in Cena Domini"):
     1 Cor 11:20-32 / John 13:1-15 -- scan1.txt "Lectio Epistolae beati
     Pauli Apostoli ad Corinthios... 1 Cor. 11, 20-32" + "Sequentia...
     secundum Ioannem... Io. 13,1-15"; scan2.txt corroborates both.
   - [ef-passiontide-2-saturday] (Holy Saturday, Missa Vigiliae Paschalis'
     own Epistle+Gospel -- the actual Mass, distinct from the earlier
     prophecies, explicitly labelled "Lectio EPISTOLAE"): Col 3:1-4 /
     Matt 28:1-7 -- scan1.txt "Lectio Epistolae beati Pauli Apostoli ad
     Colossenses... Col. 3,1-4" + "Sequentia... secundum Matthaeum. Mt.
     28,1-7"; scan2.txt corroborates both.

   Fix round 2 (coordinator review, Critical): the remaining two days,
   Holy Wednesday and Good Friday, were left ENTIRELY absent by the first
   pass, on the reasoning that neither has a single reading in the
   "Epistle" position. That reasoning was correct for the FIRST slot but
   the wrong conclusion for the pair as a whole, and it broke something
   worse than what it replaced: an absent lectionary key does not mean
   "no reading" to {!Rite_ef.Lectionary_ef.readings} -- it means STEP 3
   resumes the preceding Sunday, so Good Friday -- which has no Mass at
   all -- was emitting Palm Sunday's own Epistle and Passion narrative.
   Fixed properly:

   - The GOSPEL is unambiguous on both days -- one labelled Passion
     narrative each, nothing to choose among -- so it is always authored:
     Holy Wednesday, Luke 22:39-71; 23:1-53 (scan1.txt "Evangelium
     Passionis et Mortis Domini secundum Lucam. 22, 39-71; 23, 1-53");
     Good Friday, John 18:1-40; 19:1-42 (scan1.txt "Evangelium Passionis
     et Mortis Domini secundum Ioannem. 18,1-40; 19,1-42"). Both
     corroborated scan2.txt.
   - The FIRST slot is filled too, by the SAME "last lesson before the
     Gospel" convention this file's own Lenten Ember Wednesday entry
     (below, in [colitur_keys]'s own ini translation, not hand-authored
     here) already applies to an identical two-lesson shape: Exodi
     24,12-18 then 3 Reg. 19,3-8 (scan1.txt:8311/8377), and the file ships
     the SECOND. Applied here for the same reason -- consistency with
     already-shipped precedent, not a fresh editorial choice invented for
     this entry: Holy Wednesday's own two Isaiah lessons are Isai. 62,
     11;63,1-7 THEN Isai. 53,1-12 (scan1.txt:12379/12415, corroborated
     scan2.txt) -- the file ships Isa 53:1-12, the second. Good Friday's
     own two lessons are Osee 6,1-6 THEN Ex. 12,1-11 (scan1.txt:
     13143/13181, corroborated scan2.txt) -- the file ships Ex 12:1-11,
     the second. Neither lesson is labelled "Epistola" on either day
     (unlike Holy Saturday's genuinely labelled Epistle above), so this
     remains a stated CONVENTION, not a textual fact the Missal asserts
     -- but it is the SAME convention already load-bearing elsewhere in
     this exact file, not a new one invented to paper over this gap. *)
let holy_week_entries =
  [ (slug_or_die "ef-passiontide-2-monday", pair ~first:"Isa 50:5-10" ~gospel:"John 12:1-9");
    (slug_or_die "ef-passiontide-2-tuesday",
      pair ~first:"Jer 11:18-20" ~gospel:"Mark 14:32-72; 15, 1-46");
      (* The Gospel's own punctuation ("15, 1-46", not "15:1-46") is
         deliberately kept exactly as lectio's own ini already has it
         (`ef-passiontide-0-tuesday`'s own `gospel` field, byte for byte)
         rather than normalised to this file's own colon convention: this
         value is not merely CORROBORATED by lectio's data, it is the
         SAME data (Holy Tuesday's Mass, which the ini's Tuesday section
         happens to carry outright) -- reformatting it would turn a
         genuine byte-identical match into a false, punctuation-only
         divergence the differential would then have to explain away. *)
    (slug_or_die "ef-passiontide-2-wednesday",
      pair ~first:"Isa 53:1-12" ~gospel:"Luke 22:39-71; 23:1-53");
    (slug_or_die "ef-passiontide-2-thursday", pair ~first:"1 Cor 11:20-32" ~gospel:"John 13:1-15");
    (slug_or_die "ef-passiontide-2-friday", pair ~first:"Ex 12:1-11" ~gospel:"John 18:1-40; 19:1-42");
    (slug_or_die "ef-passiontide-2-saturday", pair ~first:"Col 3:1-4" ~gospel:"Matt 28:1-7")
  ]

(* Important, fix round 2 (coordinator review): Passion week's own real
   Tuesday Mass ("Feria tertia, III classis, Statio ad S. Cyriacum",
   scan1.txt:11085-11121, corroborated scan2.txt:12063-12104, word for
   word both scans): Dan 14:27, 28-42 / John 7:1-13. [colitur_keys]'s own
   "ef-passiontide-0-tuesday" branch above excludes lectio's ini value for
   this one weekday (it is Holy Tuesday's Mass, not Passion Tuesday's --
   see that branch's own comment for the full account) rather than
   translating it, so this is the ONLY source of a citation for
   "ef-passiontide-1-tuesday" at all -- not a widen, not a correction of
   an existing translated value. *)
let passion_tuesday_entry =
  [ (slug_or_die "ef-passiontide-1-tuesday", pair ~first:"Dan 14:27, 28-42" ~gospel:"John 7:1-13") ]

(* Important, fix round 3 (coordinator review): two more Ember Saturdays
   shipping a saint's Mass verbatim from lectio's ini -- the exact C26
   shape, found by re-review after this generator's own [assert_reachable]
   and the Holy Week/Passion-Tuesday fixes had already closed the shapes
   that check could and could not catch. Both are multi-lesson Ember
   Masses (like Lenten Ember Wednesday and Good Friday/Holy Wednesday
   above), but neither needed the "last lesson" CONVENTION: both readings
   below are explicitly labelled "Lectio Epistolae" in the Missal, exactly
   like Holy Saturday's own Epistle, distinct from the several numbered
   prophecies (Isaiah, Daniel's Benedictus, etc.) that precede them --
   a textual fact, not an editorial choice.

   - [ef-advent-ember-sat] ("Sabbato Quatuor Temporum Adventus, II
     classis"): 2 Thess 2:1-8 / Luke 3:1-6 -- scan1.txt "Lectio Epistolae
     beati Pauli Apostoli ad Thessalonicenses. 2 Thess. 2, 1-8" +
     "Sequentia sancti Evangelii secundum Lucam. Luc. 3, 1-6"; scan2.txt
     corroborates both word for word. lectio's own ini value (Eph 2:19-22
     / John 20:24-29) is St THOMAS THE APOSTLE's Mass (21 December,
     "doubting Thomas") -- a real office, just not this one.
   - [ef-september-ember-sat] ("Sabbato Quatuor Temporum septembris, II
     classis"): Heb 9:2-12 / Luke 13:6-17 -- scan1.txt "Lectio Epistolae
     beati Pauli Apostoli ad Hebraeos. Hebr. 9, 2-12" + "Sequentia sancti
     Evangelii secundum Lucam. Luc. 13, 6-17"; scan2.txt corroborates both
     word for word. lectio's own ini value (Ezek 1:10-14 / Matt 9:9-13) is
     ST MATTHEW's Mass (21 September, the calling of Matthew) -- again a
     real office, just not this one.

   Both genuinely observed offices (confirmed against the real resolver,
   not assumed): 40 civil days each in the 2005-2050 window. The other
   four Ember-day entries (Advent Wed/Fri, September Wed/Fri, and the
   Pentecost/Lent sets already fixed above) were checked against the
   Missal while investigating this and are correct as they already
   stood -- this is not a wider pattern across the whole Ember-day
   family, just these two Saturdays. *)
let ember_saturday_corrections =
  [ (slug_or_die "ef-advent-ember-sat", pair ~first:"2 Thess 2:1-8" ~gospel:"Luke 3:1-6");
    (slug_or_die "ef-september-ember-sat", pair ~first:"Heb 9:2-12" ~gospel:"Luke 13:6-17")
  ]

(* Important 3(a), fix round 1 (coordinator review): the fixed Nativity-
   Octave days (RG 91 entry 17, 29-31 December, colitur's own
   [ef-nativity-octave-day-{5,6,7}]) have a DIRECT formulary in the
   Missal, not merely a resolvable-by-walk-back gap -- "Diebus infra
   octavam Nativitatis Domini, II classis": Titus 3:4-7 / Luke 2:15-20
   (docs/research/scan1.txt:6281-6329, scan2.txt:6900-6960, both word for
   word), and each specific date's own rubric points straight at it
   ("Die 29 decembris... Missa Puer natus est nobis, ut supra [28]",
   scan1.txt, repeated verbatim at 30 and 31 December). This is the SAME
   shape [vigil_entries] above already uses (one direct entry, both
   scans), not the Task-6-sized verification this task's own report
   scoped Task 8 away from -- confirmed, not merely asserted, since a
   single Missal heading answers it completely.

   REJECTED, and recorded so it is not re-attempted: the FIRST guess tried
   here gave these three slugs [ef-holy-name-sunday]'s own citation
   instead (Gal 4:1-7 / Luke 2:33-40, "Dominica infra octavam Nativitatis
   Domini" -- a DIFFERENT heading, for the Sunday specifically, not the
   weekdays). Measured against the real fixture and reverted: lectio's own
   citation for these dates is not uniform across years (2005-12-29 reads
   Christmas Day's own Mass, Heb 1:1-12/John 1:1-14, since 25 December
   2005 was itself a Sunday that year; 2006-12-29 reads Advent IV's,
   1 Cor. 4:1-5/Luke 3:1-6) -- neither matches "Diebus infra octavam"'s
   own formulary, because in BOTH those years the civil date landed on an
   ordinary WEEKDAY within the octave, not the (different) Sunday the
   first guess's citation was actually for. This entry is right for the
   weekday case precisely because it is sourced from the weekday's own
   heading, not the Sunday's.

   IMPORTANT CAVEAT, carried into data/ef/expected-divergences.sexp's own
   C6 entry, not fixed here: colitur's OWN [Temporal_ef] assigns
   [ef-nativity-octave-day-N] to BOTH an ordinary weekday within 29-31
   December AND a Sunday landing there, undifferentiated at the temporal-
   slug level. RG 69 ("De dominica infra octavam Nativitatis Domini...
   semper fit Officium... nisi dominica incidat in festum I classis") is
   unconditional -- the Sunday's own distinct Office (RG 91 places II-class
   Sundays above days within the octave) should be observed instead of the
   weekday placeholder whenever 29-31 December IS a Sunday. This entry
   therefore gives the CORRECT citation for the majority (weekday) case
   and the WRONG one on the years the civil date is itself a Sunday -- a
   pre-existing [Temporal_ef] defect this generator cannot fix (it has no
   day-of-week logic of its own to add), out of this task's own scope, and
   the same RG 67/69 gap data/ef/expected-divergences-missalemeum.sexp's
   own M11 already tracks from a different differential layer. *)
let nativity_octave_entries =
  let cs = pair ~first:"Titus 3:4-7" ~gospel:"Luke 2:15-20" in
  List.map (fun n -> (slug_or_die (Printf.sprintf "ef-nativity-octave-day-%d" n), cs)) [ 5; 6; 7 ]

(* Task 9 (branch ef-lectionary, layer-4 oracle): [ef-holy-name-sunday] and
   [ef-holy-name] (RG 17(a)'s own 2-January fallback, "secus die 2
   ianuarii") HAND-AUTHORED directly from the Missal, replacing the
   PREVIOUS mechanism -- deriving [ef-holy-name] from
   [ef-holy-name-sunday]'s own (then wrongly-widened) ini-translated
   citation, itself borrowed from an unrelated ini section by analogy (see
   [colitur_keys]'s own "ef-christmas-sunday-0" branch, CORRECTED note,
   for the full account of the error this replaces). Both slugs get the
   SAME citation pair -- the calendarium's own table (scan1.txt/scan2.txt,
   same headings cited above) gives ONE Mass heading covering both the
   Sunday and the 2-January fallback shape, not two -- so this is still
   one hand-typed value, not a second independent lookup, the same
   discipline the removed [derived_entries] intended, just sourced
   correctly this time.

   [ef-nativity-octave-day-{5,6,7}] does NOT reuse this value -- a first
   attempt tried exactly that and was measured wrong and reverted; see
   [nativity_octave_entries] above for the correct, DIFFERENT, directly
   Missal-sourced formulary and the full account of why the two headings
   differ. *)
let holy_name_citations = pair ~first:"Acts 4:8-12" ~gospel:"Luke 2:21"

let holy_name_entries =
  [ (slug_or_die "ef-holy-name-sunday", holy_name_citations); (slug_or_die "ef-holy-name", holy_name_citations) ]

(* Task 9 (branch ef-lectionary, layer-4 oracle): three more colitur-only
   slug families, all in the same 2-13 January stretch as
   [holy_name_entries] just above and found the same way -- missalemeum's
   own oracle disagreeing with colitur's step-3 fallback answer on every
   civil day these slugs cover, traced back to an EXPLICIT primary-source
   rubric each time, not a guess. All three ranges and their own slug
   families are documented, independently, in
   {!Rite_ef.Temporal_ef.christmastide_feria_slug}'s own comment (that
   function's docstring gives the exact civil-date boundaries this
   generator relies on here; not re-derived, read directly from the code
   that computes them).

   NOT RG 17(a) -- CORRECTED, fix round 1 (coordinator review): RG 17(a)
   itself (scan1.txt:396-402, "festum Ssmi Nominis Iesu, celebrandum
   dominica quae occurrit a die 2 ad 5 ianuarii... secus die 2 ianuarii")
   states only the day-identity PRIVILEGE (which day the feast falls on),
   nothing about ferial Masses -- exactly the distinction docs/research/
   rules-register.md's own account already draws ("a THIRD, more specific
   primary text... the Mass propers' own rubric for this feast",
   deliberately NOT folded into RG 17(a) there). All three rubrics below
   are Mass-PROPERS rubrics, printed as part of a feast's own Mass
   heading, not General Rubrics chapter text -- and the second and third
   are a DIFFERENT feast's propers from the first, not a continuation of
   it (see each bullet's own attribution).

   - [ef-christmas-1-{monday,tuesday,wednesday,thursday,friday}] (2-5
     January, EXCLUDING Saturday -- see below): the Missal's own
     Mass-propers rubric for the Most Holy Name of Jesus, printed
     immediately BEFORE the Sunday-shape Mass heading (not after it),
     both scans, word for word: "Diebus ferialibus a 2 ad 5 ianuarii
     Missa dicitur ut die 1 ianuarii" (scan1.txt:6522-6526) / "Diebus
     ferialibua a 2 ad fi ianuarii Missa dicitur ut die 1 ianuarii"
     (scan2.txt:7213-7217 -- OCR-garbled there, "ferialibua"/"fi" for
     "ferialibus"/"5"; scan1 is clean and missalemeum's own extraction
     corroborates the citation this rubric produces, so the reading is
     not in doubt) -- ON FERIAL DAYS from 2 to 5 January, THE SAME MASS
     AS 1 JANUARY (the Circumcision) IS SAID.
   - [ef-christmas-2-{monday,tuesday,wednesday,thursday,friday}] (7
     January through the day before Holy Family Sunday's own civil date,
     EXCLUDING Saturday -- see below): a DIFFERENT Mass-propers rubric
     block, ~230 scan1 lines after the one immediately above, sitting
     within HOLY FAMILY'S OWN feast propers (RG 17(b)'s feast, page
     header "Sanctae Familiae Iesu, Mariae, Ioseph", scan1.txt:6751/
     scan2.txt:7412), printed immediately BEFORE the "DOMINICA PRIMA post
     Epiphaniam / SANCTAE FAMILIAE" Mass heading -- the same structural
     position the rubric above occupies relative to Holy Name's own
     heading, but a different feast's propers, not a continuation of the
     same rubric: "Diebus ferialibus a 7 ad 12 ianuarii ANTE dominicam I
     post Epiphaniam occurrentibus, dicitur Missa de festo Epiphaniae"
     (scan1.txt:6753-6757; scan2.txt:7413-7415 corroborates, OCR-garbled
     -- "occurentibus"/"Epipbaniee" -- reading not in doubt) -- ON FERIAL
     DAYS from 7 to 12 January occurring BEFORE the first Sunday after
     Epiphany (Holy Family), THE MASS OF THE FEAST OF EPIPHANY IS SAID.
   - [ef-time-after-epiphany-1-{monday,tuesday,wednesday,thursday,friday}]
     (Holy Family Sunday's own week, EXCLUDING Saturday -- see below,
     straddling the Christmastide/Time-after-Epiphany season boundary on
     14 January without changing slug -- {!christmastide_feria_slug}'s
     own comment: "it is the SAME COMPUTATION the ordinary
     Time-after-Epiphany ferial fallback... will give the rest of that
     same Sunday-to-Saturday week", so one hand-authored value covers the
     whole week regardless of which code path computed the slug for a
     given civil day): the SAME Holy Family propers block immediately
     above, its own next sentence, a named formula rather than a second
     "repeat the Sunday" instruction: "post eandem dominicam vero Missa
     de eadem dominica In excelso throno, ut infra... Haec Missa dicenda
     est diebus ferialibus huius hebdomadae" (scan1.txt:6758-6851;
     scan2.txt:7415-7434 corroborates "In excelso throno... ut infra",
     OCR-garbled elsewhere in the same passage, reading not in doubt),
     the formula's own heading "MISSA DOMINICAE PRIMAE post Epiphaniam"
     immediately below -- Epistle "Lectio Epistolae beati Pauli Apostoli
     ad Romanos. Rom. 12, 1-5" (scan1.txt:6875-6877), Gospel the SAME
     pericope as Holy Family's own Sunday Mass, Luke 2:42-52 (printed out
     in full immediately below the Epistle, scan1.txt:6885-6905) -- a
     DIFFERENT Epistle from Holy Family Sunday's own (Col 3:12-17), but
     the SAME Gospel, exactly as missalemeum's own fixture rows show
     (2026-01-12/2027-01-11/2027-01-12: Gospel already agreed before this
     fix, only the Epistle diverged).
   - 13 January's OWN separate case, checked and found to need NO entry
     here: when Holy Family does not fall there, 13 January carries a
     II-class sanctoral commemoration of its own ("Commemoration of the
     Baptism of the Lord", data/ef/sanctoral.sexp's own
     [commemoration-of-the-baptism-of-the-lord], already carrying its own
     citations, Isa 60:1-6 / John 1:29-34, matching missalemeum exactly)
     -- {!Rite_ef.Lectionary_ef.readings}'s own step 1 (the OBSERVED
     celebration's own proper) resolves it before step 2 (this
     generator's own data) is ever consulted, verified directly against
     the real resolver (tools/dump_dates.ml, a scratch diagnostic, not
     committed): 2026-01-13/2027-01-13 both already show the correct
     citation with none of the entries below in play.

   SATURDAY IS EXCLUDED FROM ALL THREE FAMILIES, deliberately, not an
   oversight, and NOT (as an earlier draft of this comment argued for the
   latter two) safe to include: RG 78 ("In sabbatis, in quibus occurrit
   Officium de feria IV classis, fit de sancta Maria in sabbato", already
   built, Rite_ef.Temporal_ef's own [bvm_saturday_names]) makes ANY
   otherwise-unoccupied IV-class Saturday the Saturday Office of the BVM
   instead of a plain feria -- UNCONDITIONALLY, not merely when nothing
   else happens to compete that day (Precedence_ef's own [band] gives it
   this ranking whenever the day's own office would otherwise be a plain
   Class4 feria, checked in {!Rite_ef.Temporal_ef}'s own [bvm_saturday_names]
   comment and confirmed against the real resolver here, not assumed).
   This window's own SIX otherwise-unoccupied Saturdays inside these
   three families (2026-01-03, 2026-01-10, 2027-01-02, 2027-01-09;
   2026-01-17 and 2027-01-16 are won outright by a saint instead, Anthony
   and Marcellus I respectively, whose own citations are unaffected
   either way) genuinely ARE the BVM Office, missalemeum's own title
   confirming it on every one that reaches the office at all ("II Mass of
   the B. V. M. -- Vultum Tuum", never "Feria") -- so none of the three
   "repeat X" rubrics above ever actually apply on a Saturday. (CORRECTED,
   fix round 1, coordinator review: this comment previously counted only
   five, omitting 2027-01-02 -- `ef-christmas-2-saturday`, the same shape
   as 2026-01-10 -- from its own list; no consequence for the code, which
   already excludes every [-saturday] slug in all three families
   unconditionally, not by enumerating dates.) The three [-saturday] slugs are
   DELIBERATELY left unentered, all three: the BVM Saturday Office's own
   citation is a genuine, ALREADY-DOCUMENTED open item (register, Task 6's
   own residual concern 4, "the BVM Saturday Office still emits its
   feria's Mass") this task does not build, out of scope here the same
   way it was there. (CORRECTED from this comment's own first draft,
   which excluded only [ef-christmas-1-saturday] and reasoned -- without
   checking -- that the other two families' own Saturdays were safe to
   include: measured directly against the real resolver and missalemeum's
   own fixture, they were not; the same RG 78 mechanism governs all
   three windows identically.) *)
let non_saturday_weekdays = [ "monday"; "tuesday"; "wednesday"; "thursday"; "friday" ]

let epiphanytide_weekday_citations =
  [ (`Christmas1, non_saturday_weekdays, pair ~first:"Titus 2:11-15" ~gospel:"Luke 2:21");
    (`Christmas2, non_saturday_weekdays, pair ~first:"Isa 60:1-6" ~gospel:"Matt 2:1-12");
    (`TimeAfterEpiphany1, non_saturday_weekdays, pair ~first:"Rom 12:1-5" ~gospel:"Luke 2:42-52")
  ]

(* RG 299's own escape clause, exercised (movable-date-specs follow-up,
   2026-08-17). The general ferial rule is "In reliquis feriis dicitur Missa
   dominicae praecedentis, NISI A RUBRICIS ALITER PROVISUM SIT" (RG 299,
   scan1.txt:1096-1098), restated in the propers as "Per totum annum, si
   dicenda sit infra hebdomadam Missa de feria, adhibetur Missa dominicae
   praecedentis, NISI PROPRIA MISSA ASSIGNETUR" (scan1.txt:4933-4935).

   The week after Trinity Sunday is the clearest case of that exception in the
   whole year. Trinity is the temporal identity of its own Sunday, so step 3's
   walkback lands on Trinity's Mass -- but the Missal prints a NAMED formula
   for the week immediately after it, "Missa dominicae I post Pentecosten"
   (heading, scan1.txt:21758), precisely because the Sunday's own Mass has
   been taken by the feast. Its readings are "Lectio Epistolae beati Ioannis
   Apostoli. 1 Ioann. 4, 8-21" (scan1.txt:21801-21802) and "Sequentia sancti
   Evangelii secundum Lucam. Luc. 6, 36-42" (scan1.txt:21813-21814).

   Given directly here so these ferias resolve at STEP 2 and never reach step
   3's walkback at all -- which is both correct and the honest shape: the
   Missal assigns them a Mass, so they have one, rather than inheriting one.

   This is test_oracle.ml's own M26 shape 2(a). Shape 2(b), Christ the King's
   week, is NOT the same case and is deliberately not given entries here: no
   proper Mass is assigned there (checked, scan1.txt:39163 ff, no rubric
   precedes that Mass heading), so RG 299's DEFAULT governs and the preceding
   Sunday's Mass genuinely is Christ the King's own. *)
let trinity_week_citations = pair ~first:"1 John 4:8-21" ~gospel:"Luke 6:36-42"

let trinity_week_entries =
  List.map
    (fun wd -> (slug_or_die ("ef-time-after-pentecost-1-" ^ wd), trinity_week_citations))
    (* NO Thursday: Corpus Christi is Easter+60, which IS the Thursday of this
       week every year without exception, so the ferial slug never exists.
       Found by the generator's own reachability guard rather than reasoned
       out in advance -- the guard refuses to emit a key no Temporal_ef slug
       can ever match, which is precisely the silent-lookup-miss this
       lectionary has been bitten by before. *)
    [ "monday"; "tuesday"; "wednesday"; "friday"; "saturday" ]

(* RG 299's own escape clause, second instance (celebrant-rubrics-phase1,
   Bug 2, 2026-08-22). The Proprium de Tempore carries an explicit
   marginal rubric immediately after the Ascension's own Mass propers,
   before "Dominica post Ascensionem" begins (scan1.txt:20749-20754, both
   scans corroborate): "Diebus a feria VI post Ascensionem usque ad
   feriam VI ante vigiliam Pentecostes inclusive, dicitur Missa de festo
   Ascensionis, cum Gloria et praefatione de Ascensione, sine Credo et
   Communicantes proprio" -- on the days from the Friday after the
   Ascension until the Friday before the Vigil of Pentecost INCLUSIVE,
   the MASS OF THE FEAST OF THE ASCENSION is said (with the Gloria and
   the Ascension preface, without the Creed and the proper
   Communicantes). Corroborated independently by RG 490(b) (LT.txt:3981-
   3985), which assigns the Ascension preface "tamquam de Tempore" to
   every Mass in that same window lacking a proper preface of its own --
   consistent only if the Mass repeated there is the Ascension's own,
   not a numbered Sunday's (which would need no preface override at
   all).

   Witnessed directly, not merely inferred from the rubric: the LMS Ordo
   prints "Mass of the Ascension" literally, across all three fixture
   windows this project has built (2023-2024: 1 day; 2024-2025: 3 days;
   2025-2026: 2 days -- data/ef/expected-divergences-lms.sexp's own L2,
   closed by this entry), on EVERY affected day in BOTH the remainder of
   Ascension's own week and the following week, not only the first --
   ruling out the alternative reading that the second week instead
   resumes "Dominica post Ascensionem"'s own Mass. The FIUV Ordo's own
   title convention for these days (2025-2026 fixture, 2026-05-21/22,
   "Feria V/VI Dom. post Ascensionem, De ea") could look like the
   opposite at a glance -- it is not: FIUV's title names which WEEK a
   feria belongs to (a bookkeeping label, the same role colitur's own
   week-numbered slug plays), while its own [praef] field for those
   exact two days independently reads "Ascensionis", confirming the Mass
   actually said is still the Ascension's, exactly as the marginal
   rubric and RG 490(b) both require.

   The window's two Saturdays (Easter+41, Easter+48) are DELIBERATELY
   NOT given entries here. Easter+48 is the Vigil of Pentecost, a named
   day with its own Mass ({!Rite_ef.Temporal_ef.named}), outside the
   rubric's own "usque ad feriam VI" upper bound. Easter+41 is an
   ordinary IV-class Saturday, so {!Rite_ef.Lectionary_ef.readings}'s
   own RG 78/RG 309(a) votive-Mass-of-Our-Lady-on-Saturday branch ALWAYS
   intercepts it before step 2 is ever reached (checked directly:
   [is_bvm_saturday_office] fires unconditionally on any unoccupied
   Class4 Saturday, which every ordinary Paschaltide Saturday is) -- an
   entry for either Saturday slug would be dead data that
   [assert_reachable] cannot catch (both slugs it would name ARE real,
   reachable Temporal_ef offices; the shadowing happens one level up, in
   [readings]' own step ordering, not in slug reachability).

   Six ferias, matching the Ascension's own citation already given above
   (Act. 1, 1-11 / Marc. 16, 14-20, scan1.txt:20701-20730,
   [ef-ascension]'s own entry): the one remaining non-Saturday day of
   Ascension's own week (Easter+40) and the five weekdays of the
   following week (Easter+43..47), bounded above by that week's own
   Friday -- the Saturday after it is the Vigil of Pentecost, named,
   never reaching this table. *)
let ascension_week_citations = pair ~first:"Acts 1:1-11" ~gospel:"Mark 16:14-20"

let ascension_week_entries =
  List.map
    (fun slug -> (slug_or_die slug, ascension_week_citations))
    [ "ef-easter-6-friday";
      "ef-easter-7-monday";
      "ef-easter-7-tuesday";
      "ef-easter-7-wednesday";
      "ef-easter-7-thursday";
      "ef-easter-7-friday"
    ]

let epiphanytide_opening_entries =
  List.concat_map
    (fun (family, wds, cs) ->
      let prefix =
        match family with
        | `Christmas1 -> "ef-christmas-1-"
        | `Christmas2 -> "ef-christmas-2-"
        | `TimeAfterEpiphany1 -> "ef-time-after-epiphany-1-"
      in
      List.map (fun wd -> (slug_or_die (prefix ^ wd), cs)) wds)
    epiphanytide_weekday_citations

(* Task 9 (branch ef-lectionary, layer-4 oracle): two major, permanently-
   named I-class movable feasts of the Lord that had NO lectionary entry
   at all until now -- not a renamed/mis-widened value like
   [holy_name_entries] above, a genuine ABSENCE. Both slugs are real,
   directly-computed [Temporal_ef] offices (temporal_ef.ml's own [named]
   function, "ef-corpus-christi"/"ef-sacred-heart"), so every occurrence
   fell through to step 3's ferial resumption (Trinity Sunday's own
   citation, repeated) instead of the feast's own Mass -- invisible before
   this task made citation CONTENT comparable at all, the identical shape
   Task 8's own [colitur_keys] fixes already found for renamed slug
   families, here for a slug that was never translated from anywhere
   because lectio's own EF ini has no equivalent section for either feast.
   Both citations verified against BOTH scans, word for word:

   - [ef-corpus-christi] ("IN FESTO SSMI CORPORIS CHRISTI, I classis"):
     1 Cor 11:23-29 / John 6:56-59 -- scan1.txt:21867-21917 ("Lectio
     Epistolae beati Pauli Apostoli ad Corinthios. 1 Cor. 11, 23-29" /
     "...secundum Ioannem. Io. 6, 56-59"), scan2.txt:24084-24172
     corroborates both citations.
   - [ef-sacred-heart] ("IN FESTO SACRATISSIMI CORDIS IESU, I classis"):
     Eph 3:8-12, 14-19 / John 19:31-37 -- scan1.txt:22030-22095 ("...ad
     Ephesios. Ephes. 3, 8-12 et 14-19" / "...secundum Ioannem. Io. 19,
     31-37"), scan2.txt:24284-24336 corroborates the heading and both
     citations (the Epistle's own "et 14-19" clause is OCR-truncated to
     "3, 8-19" on this second scan alone, its printed body text otherwise
     matching word for word; scan1's own clean transcription is
     unambiguous, so this is a scan-quality gap, not a genuine
     disagreement between the two printings). Both citations independently
     corroborated by missalemeum's own extraction too (test/fixtures/
     missalemeum-ef-2026-2027.txt, 2026-06-04 and 2026-06-12/2027-06-04). *)
let movable_feast_entries =
  [ (slug_or_die "ef-corpus-christi", pair ~first:"1 Cor 11:23-29" ~gospel:"John 6:56-59");
    (slug_or_die "ef-sacred-heart", pair ~first:"Eph 3:8-12, 14-19" ~gospel:"John 19:31-37")
  ]

let sha256 path =
  let ic = Unix.open_process_in (Printf.sprintf "sha256sum %s" (Filename.quote path)) in
  let line = try input_line ic with End_of_file -> die "sha256sum failed" in
  ignore (Unix.close_process_in ic);
  List.hd (String.split_on_char ' ' line)

(* Critical 2, fix round 1 (coordinator review): "fix the class, not just
   the instance". [colitur_keys] is a hand-maintained table with no
   reality check of its own -- exactly how the Lent Ember mismatch
   survived the first pass (both engines independently fall through to
   the same wrong answer there, so even the differential could not see
   it). This sweeps {!Rite_ef.Temporal_ef.temporal} directly over a real
   civil-year range -- no sanctoral layer, no lectionary, no circularity
   with the file this tool generates -- and collects every DISTINCT office
   slug it ever actually produces. [assert_reachable] then requires every
   key this generator is about to EMIT to be a member of that set (an
   emitted key that is not a real Temporal_ef slug is dead data,
   unreachable by any caller), dying loudly and naming every offender if
   not.

   CORRECTED, fix round 2 (coordinator review): the paragraph above used
   to claim this check "is exactly the shape both Critical findings had".
   Wrong for one of the two, and worth being precise about the limit: this
   assertion catches an emitted KEY that names no real office (Critical 2,
   the Lent Ember dead keys) -- it cannot catch, and did not catch, a real,
   live key carrying the WRONG VALUE (Critical 1, fix round 1's own Holy
   Week widening: "ef-passiontide-2-monday" etc were all perfectly
   reachable slugs, just paired with Passion week's citations instead of
   Holy Week's own). A reachability sweep is a name check, not a content
   check; nothing in this generator verifies citation VALUES against the
   Missal except the human cross-referencing docs/research/scan1.txt and
   scan2.txt line by line, which is why every hand-authored entry in this
   file states its own two-scan citation.

   Separately, informational only, [assert_reachable] also prints every
   real Temporal_ef slug that has NO entry in the final table -- not an
   error (most such gaps are the correctly-unproper ferias step 3 already
   resolves, task-5-report.md's own "297 of 304" measurement), but a
   standing audit log a human reader can check against that same report
   rather than trusting silence.

   1583 is deliberately NOT the sweep's start: the full 1583-9999 domain is
   the KERNEL's own contract, not this rite-specific bootstrap tool's --
   sweeping the differential's own window (2005-2050), widened by one year
   on each side for step-3 preceding/following-Sunday edge cases, is
   enough to enumerate every DISTINCT slug FAMILY (season/week/weekday
   combinations recur every year; only which YEAR exhibits a given
   alignment changes, e.g. how many Sundays fall in Time after
   Epiphany) -- confirmed against task-5-report.md's own domain-wide
   "412 distinct temporal slugs" figure: this sweep alone already finds
   all 412. *)
let reachable_temporal_slugs () =
  let tbl = Hashtbl.create 512 in
  let mk y m d = match Date.make ~year:y ~month:m ~day:d with Ok t -> t | Error e -> die "%s" e in
  for y = 2004 to 2051 do
    let d = ref (mk y 1 1) in
    let stop = mk y 12 31 in
    while Date.compare !d stop <= 0 do
      let t = Rite_ef.Temporal_ef.temporal !d in
      Hashtbl.replace tbl (Slug.to_string t.Temporal.office.Celebration.slug) true;
      d := Date.add_days !d 1
    done
  done;
  tbl

let assert_reachable entries =
  let reachable = reachable_temporal_slugs () in
  let dead =
    List.filter (fun (s, _) -> not (Hashtbl.mem reachable (Slug.to_string s))) entries
  in
  (match dead with
  | [] -> ()
  | _ ->
      List.iter
        (fun (s, _) ->
          Printf.eprintf
            "bootstrap_lectionary: DEAD KEY -- %S is not a real Temporal_ef slug over any civil \
             day 2004-2051, so nothing can ever look it up\n"
            (Slug.to_string s))
        dead;
      die "%d emitted key(s) are unreachable (see above) -- fix colitur_keys/vigil_entries/\
           holy_week_entries/nativity_octave_entries/derived_entries, never delete this check"
        (List.length dead));
  let emitted = Hashtbl.create 512 in
  List.iter (fun (s, _) -> Hashtbl.replace emitted (Slug.to_string s) true) entries;
  let uncovered =
    Hashtbl.fold (fun s _ acc -> if Hashtbl.mem emitted s then acc else s :: acc) reachable []
    |> List.sort compare
  in
  Printf.eprintf
    "bootstrap_lectionary: %d of %d real Temporal_ef slugs (2004-2051) have no lectionary entry \
     (informational -- most resolve correctly via step 3's ferial resumption; see \
     task-5-report.md):\n"
    (List.length uncovered) (Hashtbl.length reachable);
  List.iter (fun s -> Printf.eprintf "  %s\n" s) uncovered

let () =
  let src = if Array.length Sys.argv > 1 then Sys.argv.(1) else default_source in
  let dst = if Array.length Sys.argv > 2 then Sys.argv.(2) else default_dest in
  let secs = parse_ini src in
  let translated = List.concat_map convert secs in
  let entries =
    translated @ vigil_entries @ holy_week_entries @ passion_tuesday_entry
    @ ember_saturday_corrections @ nativity_octave_entries @ holy_name_entries
    @ epiphanytide_opening_entries @ trinity_week_entries @ movable_feast_entries
    @ ascension_week_entries
  in
  assert_reachable entries;
  let lect =
    match Lectionary.of_entries entries with
    | Ok l -> l
    | Error e -> die "%s" e
  in
  let oc = open_out dst in
  Printf.fprintf oc
    "; data/ef/lectionary.sexp -- EF (1962) temporal lectionary (Epistle +\n\
     ; Gospel citations, never scripture text), bootstrapped from lectio.\n\
     ; Generator: tools/bootstrap_lectionary.ml -- do not hand-edit; re-run the\n\
     ; generator against a newer lectio and commit the diff instead. Every\n\
     ; emitted key is asserted, at generation time, to be a slug\n\
     ; Rite_ef.Temporal_ef actually computes (see this generator's own\n\
     ; [assert_reachable]) -- a dead key cannot ship silently again (a\n\
     ; wrongly-VALUED live key still can; [assert_reachable]'s own comment\n\
     ; says why this check cannot be widened to catch that).\n\
     ;\n\
     ; Source: %s\n\
     ; SHA-256: %s\n\
     ; %d entries (%d ini sections translated/widened into colitur's own\n\
     ; Temporal_ef vocabulary via [colitur_keys]; %d hand-authored from a\n\
     ; second source file [vigil_entries]; %d hand-authored directly from\n\
     ; the Missal [holy_week_entries]; %d hand-authored directly from the\n\
     ; Missal, one entry [passion_tuesday_entry]; %d hand-authored directly\n\
     ; from the Missal, two entries [ember_saturday_corrections]; %d\n\
     ; hand-authored directly from the Missal [nativity_octave_entries];\n\
     ; %d hand-authored directly from the Missal, two entries\n\
     ; [holy_name_entries]; %d hand-authored directly from the Missal\n\
     ; [epiphanytide_opening_entries]; %d hand-authored directly from the\n\
     ; Missal, two entries [movable_feast_entries] -- see this generator's\n\
     ; own comments on all nine). Regenerate with:\n\
     ;   eval $(opam env) && dune exec tools/bootstrap_lectionary.exe -- %s %s\n"
    src (sha256 src) (List.length entries) (List.length secs) (List.length vigil_entries)
    (List.length holy_week_entries) (List.length passion_tuesday_entry)
    (List.length ember_saturday_corrections) (List.length nativity_octave_entries)
    (List.length holy_name_entries) (List.length epiphanytide_opening_entries)
    (List.length movable_feast_entries)
    src dst;
  Sexplib.Sexp.output_hum oc (Lectionary.sexp_of_t lect);
  output_char oc '\n';
  close_out oc;
  Printf.printf "bootstrap_lectionary: %d entries -> %s\n" (List.length entries) dst