(* 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. *) 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 (Passiontide: lectio never distinguishes Passion week from Holy week at the slug level, "ef-passiontide-0-" for both -- confirmed empirically, its OWN computed reading is byte-identical between the two weeks' same weekday throughout 2005-2050 -- so both of colitur's own week-specific keys legitimately point at the one source entry); 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 Passiontide below already established. *) [ "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 ( match List.find_opt (fun wd -> String.equal name ("ef-passiontide-0-" ^ wd)) weekdays with | Some wd -> [ "ef-passiontide-1-" ^ wd; "ef-passiontide-2-" ^ 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 vigil_entries = [ ( (match Slug.of_string "ef-nativity-vigil" with Ok s -> s | Error e -> die "%s" e), [ { Citation.part = Citation.First; reference = "Rom 1:1-6" }; { Citation.part = Citation.Gospel; reference = "Matt 1:18-21" } ] ) ] (* 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. REJECTED, and recorded so it is not re-attempted: giving [ef-nativity-octave-day-{5,6,7}] (29-31 December, RG 91 entry 17) this SAME derived value too. Tried, then measured against the real differential and REVERTED -- it is wrong. lectio's own citation for those three dates is NOT uniform across years (confirmed against the fixture: 2005-12-29 reads "Heb 1:1-12"/"John 1:1-14", 2006-12-29 reads "1 Cor. 4:1-5"/"Luke 3:1-6" -- two different Masses, not one). Read against the Missal directly (docs/research/scan1.txt, "Dominica infra octavam Nativitatis Domini, II classis": Epistle "Gal. 4, 1-7", Gospel "Luc. 2, 33-40" -- the SAME pair as Holy Name Sunday, corroborating the guess would have been right ONLY for that specific case): the two observed fixture values are each the PREVIOUS true civil Sunday's OWN Mass -- Christmas Day itself (25 Dec 2005 was a Sunday that year: "In Nativitate Domini... Missa in die", Heb 1:1-12/John 1:1-14) or, in a year with no Sunday between Christmas and New Year at all, all the way back to Advent IV (24 Dec 2006 was a Sunday; 1 Cor. 4:1-5/Luke 3:1-6 is that Mass's own citation, already in [ef-advent-sunday-4] above). colitur's OWN existing step 3 ([Lectionary_ef.readings], NOT this generator) already walks back to the true PRECEDING CIVIL Sunday regardless of season and would find EXACTLY these same answers on its own -- a hard-wired entry here would have SHORT-CIRCUITED that correct, already-general mechanism with one wrong, uniform guess. Left absent deliberately: these three dates stay whatever step 3 resolves through [ef-advent-sunday-4]/[ef-holy-name-sunday] et al, or [] on a date whose own preceding Sunday has no entry either (data/ef/expected- divergences.sexp's own C21 entry records what remains, if anything, after that). *) 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) 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 @ derived_entries translated in 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.\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 derived from an already-\n\ ; translated entry above rather than re-typed [derived_entries] --\n\ ; see this generator's own comments on all three). 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 entries - List.length translated - List.length vigil_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