(* 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-" 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-" into BOTH "ef-passiontide-1-" (Passion week) AND "ef-passiontide-2-" (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). - [ef-christmas-1-{monday,tuesday,wednesday,thursday,friday}] (2-5 January, EXCLUDING Saturday -- see below): RG 17(a)'s OWN rubric, immediately following the Holy-Name-Sunday heading, both scans, word for word: "Diebus ferialibus a 2 ad 5 ianuarii Missa dicitur ut die 1 ianuarii" (scan1.txt:6522-6526) / "Diebus ferialibus a 2 ad 6 ianuarii Missa dicitur ut die 1 ianuarii" (scan2.txt, the "6" a one-digit OCR variant of scan1's "5" -- immaterial either way, since 6 January is always Epiphany, a fixed I-class feast outranking any ferial rubric outright) -- 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): the SAME rubric's own next sentence, both scans, word for word: "Diebus ferialibus a 7 ad 12 ianuarii ANTE dominicam I post Epiphaniam occurrentibus, dicitur Missa de festo Epiphaniae" (scan1.txt:6753-6757, scan2.txt corroborates) -- 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 rubric's own THIRD sentence, same citation, 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, 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 five otherwise-unoccupied Saturdays inside these three families (2026-01-03, 2026-01-10, 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. 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") ] 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 @ movable_feast_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