From f8d694d0cc19b71598e1ab64254efb069969f0a0 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 15 Aug 2026 00:36:43 +0200 Subject: kernel+ef: fix round 1 -- lectionary caller-supplied, not eager Critical (coordinator review): a clean `dune build` produced a `colitur` that died at startup on EVERY subcommand, including ones touching no lectionary data at all. Root cause was two-fold: data/ef/lectionary.sexp was never added to the root default-build alias (only materialised as a side effect of the test suite's own deps, which is why every check in the prior report passed), and Rite_ef.context loaded it as a module-init side effect via failwith, undoing Lectionary.load's own "never raises" promise at a point no caller could catch. Fixed structurally: Rite_ef.context is now a function taking ~lectionary, Lectionary_ef.readings takes ~lectionary, and neither touches the filesystem any more -- the same caller-supplied discipline the sanctoral layer already had, restoring rite_ef.mli's own pre-existing claim about it and leaving a seam for a future diocesan lectionary overlay. bin/main.ml grows load_ef_lectionary, a sibling of load_ef_layer, routed through the same colitur: %s / exit 2 path. data/ef/lectionary.sexp added to the root default alias. Every caller of Rite_ef.context updated to supply it. Also: two new tests that genuinely distinguish chain step 1 from step 2 (19 March 2026, Joseph's own proper over a competing temporal entry; 13 January 2030, Holy Family reached only through the temporal slug, the Baptism entirely absent) -- the prior two tests both survived swapping the chain order. Both new pins verified directly against the real data. The chain's own comment now states plainly that its warrant is lectio's observed behaviour, not a confirmed Missal citation, per the rules register's own open item. --- bin/main.ml | 91 +++++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 33 deletions(-) (limited to 'bin/main.ml') diff --git a/bin/main.ml b/bin/main.ml index 53cae87..bc02654 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -103,6 +103,25 @@ let load_ef_layer () = diagnostics; Ok layer) +(* Sibling to [load_ef_layer] above, same reasoning: [Rite_ef.context] now + takes [~lectionary] rather than loading data/ef/lectionary.sexp itself + (fix round 1, coordinator review -- a prior version had [Rite_ef]'s own + [context] load the file as a side effect of being linked, which killed + `colitur easter ` -- no lectionary data touched at all -- the + moment that file was missing from a bare `dune build`'s own default + target). Routed through the same [result] failure path as + [load_ef_layer], so a missing/malformed file is reported via + `colitur: %s` and `exit 2`, never an uncaught exception -- restoring the + promise [Lectionary.load]'s own .mli makes ("failures come back as + [Error], never as an exception"), which the reverted version broke by + re-wrapping it in [failwith] at module init where no caller could catch + it. *) +let load_ef_lectionary () = + let path = Filename.concat (data_dir ()) "lectionary.sexp" in + match Colitur_kernel.Lectionary.load path with + | Error e -> Error (Printf.sprintf "failed to load %s: %s" path e) + | Ok lectionary -> Ok lectionary + let day_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) = let t = d.Colitur_kernel.Liturgical_day.temporal in @@ -141,39 +160,45 @@ let day_report y = | Error msg -> Printf.eprintf "colitur: %s\n" msg; exit 2 - | Ok layer -> - let module Cal = Colitur_kernel.Calendar in - let by_rata : (int, (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) Hashtbl.t = - Hashtbl.create 400 - in - let index days = - Array.iter - (fun (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) -> - Hashtbl.replace by_rata (D.to_rata d.Colitur_kernel.Liturgical_day.date) d) - days - in - index (Cal.year Rite_ef.context layer (y - 1)); - index (Cal.year Rite_ef.context layer y); - let jan1 = match D.make ~year:y ~month:1 ~day:1 with Ok t -> t | Error e -> failwith e in - let dec31 = match D.make ~year:y ~month:12 ~day:31 with Ok t -> t | Error e -> failwith e in - let d = ref jan1 in - while D.compare !d dec31 <= 0 do - (match Hashtbl.find_opt by_rata (D.to_rata !d) with - | Some day -> day_line day - | None -> - (* Unreachable for any [y] in 1583..9999: the two indexed - liturgical years jointly cover [year_start (y-1), year_start - (y+1)), which contains all of civil year [y] - (calendar.mli). Not a [failwith] -- an out-of-domain [d] - inside this loop is impossible by construction (jan1/dec31 - are themselves validated in range, and [add_days] only ever - advances within the same civil year here) -- but a silent - skip would violate the same "never silently dropped" - standard the kernel holds itself to, so a gap surfaces - loudly on stderr rather than as a quietly short year. *) - Printf.eprintf "colitur: internal error: no resolved day for %s\n" (D.to_iso8601 !d)); - d := D.add_days !d 1 - done + | Ok layer -> ( + match load_ef_lectionary () with + | Error msg -> + Printf.eprintf "colitur: %s\n" msg; + exit 2 + | Ok lectionary -> + let context = Rite_ef.context ~lectionary in + let module Cal = Colitur_kernel.Calendar in + let by_rata : (int, (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) Hashtbl.t = + Hashtbl.create 400 + in + let index days = + Array.iter + (fun (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) -> + Hashtbl.replace by_rata (D.to_rata d.Colitur_kernel.Liturgical_day.date) d) + days + in + index (Cal.year context layer (y - 1)); + index (Cal.year context layer y); + let jan1 = match D.make ~year:y ~month:1 ~day:1 with Ok t -> t | Error e -> failwith e in + let dec31 = match D.make ~year:y ~month:12 ~day:31 with Ok t -> t | Error e -> failwith e in + let d = ref jan1 in + while D.compare !d dec31 <= 0 do + (match Hashtbl.find_opt by_rata (D.to_rata !d) with + | Some day -> day_line day + | None -> + (* Unreachable for any [y] in 1583..9999: the two indexed + liturgical years jointly cover [year_start (y-1), year_start + (y+1)), which contains all of civil year [y] + (calendar.mli). Not a [failwith] -- an out-of-domain [d] + inside this loop is impossible by construction (jan1/dec31 + are themselves validated in range, and [add_days] only ever + advances within the same civil year here) -- but a silent + skip would violate the same "never silently dropped" + standard the kernel holds itself to, so a gap surfaces + loudly on stderr rather than as a quietly short year. *) + Printf.eprintf "colitur: internal error: no resolved day for %s\n" (D.to_iso8601 !d)); + d := D.add_days !d 1 + done) let usage () = prerr_endline "colitur: usage: colitur easter | colitur temporal | colitur day "; -- cgit v1.3