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
|
(* 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
(* WIDEN, not rename -- fix round (found by test_lectionary_ef.ml's own
pre-existing test_step3_christmas_feria_resumes_sunday going from
PASS to FAIL): "ef-christmas-sunday-0" is not only lectio's own
calendar-level slug for RG 17(a)'s Holy Name Sunday
(test_differential.ml's own [norm_slug] comment) -- it is ALSO
colitur's own GENUINE, DIRECTLY-COMPUTED temporal slug (Rite_ef.
Temporal_ef.temporal, "if m = 12 && dd >= 26 then Some
\"ef-christmas-sunday-0\"") for an ordinary Sunday landing on 26-28
December, the historical "Sunday within the Octave of the Nativity"
-- a DIFFERENT civil window than Holy Name Sunday (2-5 January), and
Missal-confirmed (docs/research/scan1.txt, "Dominica infra octavam
Nativitatis Domini, II classis") to be the IDENTICAL Mass (Gal.
4,1-7 / Luc. 2,33-40), not a coincidence. The first version of this
translation REPLACED the key outright and broke that pre-existing,
working case -- caught by the existing test suite, not by this
generator's own reasoning; keeping the original key alongside the
Holy-Name-Sunday alias fixes it, following the same "widen, never
narrow" rule this table's own general design already intends. *)
[ "ef-christmas-sunday-0"; "ef-holy-name-sunday" ]
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 (
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 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 ]
(* One more colitur-only slug, DERIVED from [ef-holy-name-sunday]'s own
citations (just translated above) rather than a second hand-typed copy
of the same text: [ef-holy-name] is RG 17(a)'s own fallback ("secus die
2 ianuarii", 2 January in a year with no Sunday 2-5 January). The
calendarium's own table (data/ef/expected-divergences.sexp's own C16
note quotes it) gives ONE heading for both the Sunday and the fallback
shape -- the identical Mass, not two -- so this is the same citation
pair, not an independent lookup. Safe to hard-wire (unlike a step-3
fallback, which would walk back into a DIFFERENT, unrelated Sunday of
the OLD liturgical year): RG 17(a)'s whole point is that this Mass is
said in place of, not alongside, whatever ferial reading a bare
fallback would otherwise find.
[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 derived_entries entries =
let holy_name_sunday_citations =
match
List.find_opt (fun (s, _) -> String.equal (Slug.to_string s) "ef-holy-name-sunday") entries
with
| Some (_, cs) -> cs
| None ->
die "internal: ef-holy-name-sunday missing after translation -- cannot derive its dependants"
in
match Slug.of_string "ef-holy-name" with
| Ok s -> [ (s, holy_name_sunday_citations) ]
| Error e -> die "%s" e
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
@ nativity_octave_entries @ derived_entries translated
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 [nativity_octave_entries]; %d derived from an\n\
; already-translated entry above rather than re-typed\n\
; [derived_entries] -- see this generator's own comments on all six).\n\
; 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 nativity_octave_entries)
(List.length entries - List.length translated - List.length vigil_entries
- List.length holy_week_entries - List.length passion_tuesday_entry
- List.length nativity_octave_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
|