summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-15 00:36:43 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-15 00:36:43 +0200
commitf8d694d0cc19b71598e1ab64254efb069969f0a0 (patch)
tree48e9c2fa7246d43f0d49b4a29fedadc30a2a1c1b
parent124d7e2261c721c4a49f7203efc276088c7cb217 (diff)
downloadcolitur-f8d694d0cc19b71598e1ab64254efb069969f0a0.tar.gz
colitur-f8d694d0cc19b71598e1ab64254efb069969f0a0.zip
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.
-rw-r--r--bin/main.ml91
-rw-r--r--dune10
-rw-r--r--lib/rites/rite_ef/lectionary_ef.ml44
-rw-r--r--lib/rites/rite_ef/lectionary_ef.mli18
-rw-r--r--lib/rites/rite_ef/rite_ef.ml13
-rw-r--r--lib/rites/rite_ef/rite_ef.mli20
-rw-r--r--test/test_calendar.ml13
-rw-r--r--test/test_differential.ml10
-rw-r--r--test/test_golden.ml12
-rw-r--r--test/test_lectionary_ef.ml59
-rw-r--r--test/test_oracle.ml10
-rw-r--r--test/test_rite_ef.ml21
-rw-r--r--test/test_validate.ml12
13 files changed, 250 insertions, 83 deletions
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 <year>` -- 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 <year> | colitur temporal <year> | colitur day <year>";
diff --git a/dune b/dune
index 8a00001..b7cd19b 100644
--- a/dune
+++ b/dune
@@ -16,9 +16,17 @@
; (the package's own install artifacts -- executables, libraries, reached
; recursively through every subdirectory's own `install` alias) so this
; ADDS a requirement rather than replacing dune's own default behaviour.
+;
+; data/ef/lectionary.sexp added here for the identical reason (Task 4 fix
+; round 1, coordinator review): `colitur day <year>` needs it at runtime
+; (bin/main.ml's own [load_ef_lectionary]), and the same clean-build gap
+; this file was originally written to close applied to it too -- a fresh
+; `dune build` left it absent from _build/default/data/ef/, only ever
+; materialised there as a side effect of test/dune's own deps.
(alias
(name default)
(deps
(alias_rec install)
data/ef/sanctoral.sexp
- data/ef/adjustments.sexp))
+ data/ef/adjustments.sexp
+ data/ef/lectionary.sexp))
diff --git a/lib/rites/rite_ef/lectionary_ef.ml b/lib/rites/rite_ef/lectionary_ef.ml
index f9ce9ab..ec1aa0a 100644
--- a/lib/rites/rite_ef/lectionary_ef.ml
+++ b/lib/rites/rite_ef/lectionary_ef.ml
@@ -1,37 +1,23 @@
open Colitur_kernel
-(* [data/ef/lectionary.sexp] is located relative to the BUILD TREE, not the
- process's own cwd -- the same reasoning bin/main.ml's own [data_dir]
- documents at length: cwd varies with how the binary that eventually links
- this library is invoked (a user's shell for `dune exec colitur --`, a
- dune cram test's own sandboxed temp directory, `_build/default/test/` for
- `dune test`), and nothing in this project's build pins it to the
- repository root. [Sys.executable_name] resolves through /proc/self/exe on
- Linux, so it reports the executable's own canonical absolute path even
- when launched through a symlink (dune's cram sandbox places exactly one;
- confirmed working there already by bin/main.ml). Every executable that
- links this library -- bin/main.exe, test/test_colitur.exe,
- tools/*.exe -- sits exactly one directory below _build/default/, so
- climbing up twice and back down into data/ef always finds the file,
- regardless of the caller's own cwd. *)
-let data_path () =
- Filename.dirname (Filename.dirname Sys.executable_name) ^ "/data/ef/lectionary.sexp"
-
-(* Loaded once, at module initialisation: the data is year-independent and
- build_day is called 3 074 246 times over the full domain. *)
-let lectionary =
- match Lectionary.load (data_path ()) with
- | Ok l -> l
- | Error e -> failwith ("lectionary_ef: " ^ e)
-
(* Step 1: the observed celebration's own proper.
Step 2: the day's own temporal slug.
- Nothing here encodes "Lent has daily propers": the presence of an entry is
- the discriminator. Verified against lectio -- Lent 1 Monday returns its own
- Ezech 34:11-16, while Advent, Christmas and post-Pentecost Mondays return
- their Sunday's Mass. *)
-let readings ~observed ~temporal ~date:_ ~temporal_at:_ =
+ Nothing here encodes "Lent has daily propers": the presence of an entry in
+ [lectionary] is the sole discriminator -- this function does not branch on
+ season, rank, or any other field to decide whether a temporal slug "ought"
+ to have its own Mass.
+
+ The WARRANT for that shape is lectio's own observed behaviour only, not a
+ confirmed Missal citation: Lent 1 Monday returns its own Ezech 34:11-16,
+ while Advent, Christmas and post-Pentecost Mondays return their Sunday's
+ Mass, in both streams. docs/research/rules-register.md records this
+ openly as unconfirmed against the primary source ("EF reading-selection
+ rules ... Have lectio's behaviour; confirm against the Missal's
+ ferial-Mass rubrics when coding") -- that confirmation has not been done;
+ do not read this comment as citing RG/the Missal for the SELECTION rule
+ itself, only [Lectionary.find]'s presence-or-absence as the mechanism. *)
+let readings ~lectionary ~observed ~temporal ~date:_ ~temporal_at:_ =
match observed.Celebration.citations with
| _ :: _ as cs -> cs
| [] -> (
diff --git a/lib/rites/rite_ef/lectionary_ef.mli b/lib/rites/rite_ef/lectionary_ef.mli
index fe9359e..65fbfe5 100644
--- a/lib/rites/rite_ef/lectionary_ef.mli
+++ b/lib/rites/rite_ef/lectionary_ef.mli
@@ -3,11 +3,29 @@ open Colitur_kernel
(** The EF lectionary resolution chain. All rubric knowledge about what a day
with no proper falls back to lives here, not in the kernel.
+ [lectionary] is caller-supplied, not loaded by this module -- the same
+ reasoning rite_ef.mli's own [context] doc comment already gives for why
+ the sanctoral {!Colitur_kernel.Layer.t} stays a separate argument rather
+ than an embedded field: it lets a caller load data/ef/lectionary.sexp
+ however suits it, and leaves room for a future diocesan/proper
+ lectionary overlay to attach without this module changing at all.
+
+ An eager filesystem read at module initialisation was tried first and
+ reverted (fix round 1, coordinator review): [readings] used to close
+ over a [lectionary] value loaded as a side effect of this module being
+ LINKED, so `colitur easter <year>` -- which touches no lectionary data
+ at all -- died at startup the moment data/ef/lectionary.sexp was
+ missing from a bare `dune build`'s own default target (it was only
+ present because test/dune's own deps happened to materialise it,
+ masking the gap in every test run). See the task report for the
+ reproduction.
+
Steps 1 and 2 only (Task 4): the observed celebration's own proper, else
the day's own temporal slug in the lectionary. A day matching neither
gets [] for now -- the ferial fallback to the preceding Sunday (Task 5)
and the Commons (Task 6) are not built here. *)
val readings :
+ lectionary:Lectionary.t ->
observed:Vocab_ef.rank Celebration.t ->
temporal:(Vocab_ef.season, Vocab_ef.rank) Temporal.t ->
date:Date.t ->
diff --git a/lib/rites/rite_ef/rite_ef.ml b/lib/rites/rite_ef/rite_ef.ml
index 4cf52a6..ded960b 100644
--- a/lib/rites/rite_ef/rite_ef.ml
+++ b/lib/rites/rite_ef/rite_ef.ml
@@ -11,7 +11,16 @@ module Lectionary_ef = Lectionary_ef
open Colitur_kernel
-let context : (Vocab_ef.season, Vocab_ef.rank) Rite.t =
+(* [~lectionary], not a value closed over an internal load: fix round 1
+ (coordinator review) found the previous version -- [context] as a plain
+ value, [Lectionary_ef] loading data/ef/lectionary.sexp as a side effect
+ of being linked -- made `colitur easter <year>` (no lectionary data
+ touched at all) die at startup the moment that file was absent from a
+ bare `dune build`'s own default target. A function mirrors how the
+ sanctoral [Layer.t] already travels: caller-supplied, not embedded (see
+ this module's own .mli doc comment on [context] for the fuller
+ rationale, shared with data/ef/sanctoral.sexp). *)
+let context ~lectionary : (Vocab_ef.season, Vocab_ef.rank) Rite.t =
{ Rite.id = Temporal_ef.id;
vocab = Vocab_ef.vocab;
year_start = Temporal_ef.year_start;
@@ -23,4 +32,4 @@ let context : (Vocab_ef.season, Vocab_ef.rank) Rite.t =
admit = Precedence_ef.admit };
season_runs = Vocab_ef.seasons;
transfer_target = Precedence_ef.transfer_target;
- readings = Lectionary_ef.readings }
+ readings = Lectionary_ef.readings ~lectionary }
diff --git a/lib/rites/rite_ef/rite_ef.mli b/lib/rites/rite_ef/rite_ef.mli
index 710db1d..1a4c715 100644
--- a/lib/rites/rite_ef/rite_ef.mli
+++ b/lib/rites/rite_ef/rite_ef.mli
@@ -25,9 +25,10 @@ module Lectionary_ef = Lectionary_ef
- [transfer_target]: {!Precedence_ef.transfer_target}, RG 96 (see that
value's own documentation for the termination and forward-progress
argument {!Colitur_kernel.Rite.t.transfer_target}'s contract requires).
- - [readings]: {!Lectionary_ef.readings} -- the observed celebration's own
- proper, else the day's own temporal slug in data/ef/lectionary.sexp
- (chain steps 1-2; the ferial fallback and the Commons are later work).
+ - [readings]: {!Lectionary_ef.readings} partially applied to the caller's
+ own [~lectionary] -- the observed celebration's own proper, else the
+ day's own temporal slug in data/ef/lectionary.sexp (chain steps 1-2;
+ the ferial fallback and the Commons are later work).
Deliberately carries no [sanctoral]/[lectionary] fields the way the
original design-doc sketch of [RITE] does: {!Colitur_kernel.Rite.t} (the
@@ -35,5 +36,14 @@ module Lectionary_ef = Lectionary_ef
a separate argument to {!Colitur_kernel.Calendar.year}/[day] rather than
embedding it here, so a caller can load data/ef/sanctoral.sexp (plus
data/ef/adjustments.sexp's overlay) however suits it -- bin/main.ml's
- [load_ef_layer] is the one this module ships with. *)
-val context : (Vocab_ef.season, Vocab_ef.rank) Colitur_kernel.Rite.t
+ [load_ef_layer] is the one this module ships with. [~lectionary] here is
+ the exact same discipline (fix round 1, coordinator review: [context]
+ used to be a plain value that loaded data/ef/lectionary.sexp as a side
+ effect of {!Lectionary_ef} being linked, which broke every subcommand
+ that never touches lectionary data at all the moment that file was
+ missing from a bare build -- see the task report). A future
+ diocesan/proper lectionary overlay has a caller-side seam to attach to
+ for the same reason the sanctoral overlay already does. *)
+val context :
+ lectionary:Colitur_kernel.Lectionary.t ->
+ (Vocab_ef.season, Vocab_ef.rank) Colitur_kernel.Rite.t
diff --git a/test/test_calendar.ml b/test/test_calendar.ml
index 2f6df6d..aeaeea0 100644
--- a/test/test_calendar.ml
+++ b/test/test_calendar.ml
@@ -464,6 +464,13 @@ let real_ef_layer_for_transfer_probes =
if diagnostics <> [] then failwith "unexpected overlay diagnostics loading the real EF layer";
layer)
+(* [Rite_ef.context] takes [~lectionary] (fix round 1, coordinator review) --
+ caller-supplied, same as the sanctoral layer above. *)
+let real_ef_lectionary_for_transfer_probes =
+ match Colitur_kernel.Lectionary.load "../data/ef/lectionary.sexp" with
+ | Error e -> failwith ("../data/ef/lectionary.sexp: " ^ e)
+ | Ok l -> l
+
let test_transferred_commemoration_only_capped_out_at_target_settles_cleanly () =
let probe_date =
match Colitur_kernel.Date_spec.fixed ~month:4 ~day:26 with Ok d -> d | Error e -> failwith e
@@ -479,7 +486,11 @@ let test_transferred_commemoration_only_capped_out_at_target_settles_cleanly ()
let augmented_layer = Colitur_kernel.Layer.set real_ef_layer_for_transfer_probes probe in
(* Liturgical year "2010" (Advent 2010 -- eve of Advent 2011) covers both
25 and 26 April 2011. *)
- let year = C.year Rite_ef.context augmented_layer 2010 in
+ let year =
+ C.year
+ (Rite_ef.context ~lectionary:real_ef_lectionary_for_transfer_probes)
+ augmented_layer 2010
+ in
let find_date target =
match Array.to_list year |> List.find_opt (fun d -> D.compare d.LD.date target = 0) with
| Some d -> d
diff --git a/test/test_differential.ml b/test/test_differential.ml
index a4e550d..0b68f90 100644
--- a/test/test_differential.ml
+++ b/test/test_differential.ml
@@ -207,6 +207,13 @@ let real_layer () =
(List.map Overlay.diagnostic_to_string diagnostics);
layer
+(* [Rite_ef.context] takes [~lectionary] (fix round 1, coordinator review) --
+ caller-supplied, same as [real_layer] above. *)
+let real_lectionary () =
+ match Colitur_kernel.Lectionary.load "../data/ef/lectionary.sexp" with
+ | Ok l -> l
+ | Error e -> Alcotest.failf "../data/ef/lectionary.sexp: failed to load: %s" e
+
(* --- The seven-column row both streams share (commemorations excluded, --- *)
(* limit 1 above). *)
type row = {
@@ -255,9 +262,10 @@ let lectio_rows () = List.map row_of_line (read_lines fixture_path)
test_rite_ef.ml already made for [real_layer] above. *)
let colitur_rows_2005_2050 () =
let layer = real_layer () in
+ let rite = Rite_ef.context ~lectionary:(real_lectionary ()) in
let by_rata : (int, (V.season, V.rank) LD.t) Hashtbl.t = Hashtbl.create 20000 in
for y = 2004 to 2050 do
- let days = Cal.year Rite_ef.context layer y in
+ let days = Cal.year rite layer y in
Array.iter (fun (d : (V.season, V.rank) LD.t) -> Hashtbl.replace by_rata (Date.to_rata d.LD.date) d) days
done;
let mk y m d = match Date.make ~year:y ~month:m ~day:d with Ok t -> t | Error e -> failwith e in
diff --git a/test/test_golden.ml b/test/test_golden.ml
index a873738..188e37f 100644
--- a/test/test_golden.ml
+++ b/test/test_golden.ml
@@ -63,6 +63,7 @@ module V = Rite_ef.Vocab_ef
runs from _build/default/test/). *)
let sanctoral_path = "../data/ef/sanctoral.sexp"
let adjustments_path = "../data/ef/adjustments.sexp"
+let lectionary_path = "../data/ef/lectionary.sexp"
(* Loaded once at module init, same convention test_validate.ml's own
[real_ef_layer] uses (not test_oracle.ml/test_differential.ml's
@@ -83,12 +84,21 @@ let real_ef_layer =
(String.concat "; " (List.map Overlay.diagnostic_to_string diagnostics)));
layer)
+(* [Rite_ef.context] takes [~lectionary] (fix round 1, coordinator review) --
+ caller-supplied, same as [real_ef_layer] above. *)
+let real_ef_lectionary =
+ match Colitur_kernel.Lectionary.load lectionary_path with
+ | Error e -> failwith (Printf.sprintf "%s: failed to load: %s" lectionary_path e)
+ | Ok l -> l
+
+let real_ef_rite = Rite_ef.context ~lectionary:real_ef_lectionary
+
let mk y m d = match Date.make ~year:y ~month:m ~day:d with Ok t -> t | Error e -> failwith e
(* [Cal.day] recomputes its whole liturgical year on every call (calendar.mli
-- "pure, no cache"); acceptable here, same as test_rite_ef.ml's own use
of [Cal.year] for a handful of dates, not a hot loop. *)
-let fetch y m d = Cal.day Rite_ef.context real_ef_layer (mk y m d)
+let fetch y m d = Cal.day real_ef_rite real_ef_layer (mk y m d)
let slug_s (c : V.rank Cel.t) = Slug.to_string c.Cel.slug
let rank_s (c : V.rank Cel.t) = V.rank_to_string c.Cel.rank
diff --git a/test/test_lectionary_ef.ml b/test/test_lectionary_ef.ml
index a1a324f..b1c9e7d 100644
--- a/test/test_lectionary_ef.ml
+++ b/test/test_lectionary_ef.ml
@@ -10,6 +10,7 @@ open Rite_ef
declares both as deps. *)
let sanctoral_path = "../data/ef/sanctoral.sexp"
let adjustments_path = "../data/ef/adjustments.sexp"
+let lectionary_path = "../data/ef/lectionary.sexp"
let real_layer () =
let layer =
@@ -27,6 +28,18 @@ let real_layer () =
(List.map Overlay.diagnostic_to_string diagnostics);
layer
+(* [Rite_ef.context] takes [~lectionary] (fix round 1, coordinator review):
+ the module used to load data/ef/lectionary.sexp itself, as a side effect
+ of being linked, which meant a bare `dune build` produced a `colitur`
+ that died at startup on EVERY subcommand -- including ones (`easter`)
+ that touch no lectionary data at all -- the moment that file was absent
+ from the default build target. Caller-supplied now, same as the
+ sanctoral layer above. *)
+let real_lectionary () =
+ match Lectionary.load lectionary_path with
+ | Ok l -> l
+ | Error e -> Alcotest.failf "%s: failed to load: %s" lectionary_path e
+
(* [Calendar.day] (not [year]): the liturgical year "opening in civil year y"
is Advent-anchored (RG 61), so [Calendar.year _ _ 2030] covers Advent 2030
through November 2031 -- it would never contain 13 January 2030, which
@@ -35,7 +48,7 @@ let real_layer () =
let day y m d =
let date = match Date.make ~year:y ~month:m ~day:d with
| Ok x -> x | Error e -> Alcotest.fail e in
- Calendar.day Rite_ef.context (real_layer ()) date
+ Calendar.day (Rite_ef.context ~lectionary:(real_lectionary ())) (real_layer ()) date
let refs (ld : _ Liturgical_day.t) =
List.map (fun c -> c.Citation.reference) ld.citations
@@ -63,6 +76,48 @@ let test_step2_lenten_feria_has_its_own () =
[ "Ezech 34:11-16"; "Matt 25:31-46" ]
(refs (day 2026 2 23))
+(* Fix round 1 (coordinator review, Important finding 2): neither test above
+ actually distinguishes step 1 from step 2 -- both survive swapping the
+ chain order. 13 January 2026's temporal slug (ef-time-after-epiphany-1-
+ tuesday) has no lectionary entry at all, so a swapped chain falls through
+ to the same []-then-sanctoral answer; 23 February 2026's OBSERVED
+ celebration IS the temporal office (ef-lent-1-monday carries no sanctoral
+ entry of its own), so [observed.citations] and the temporal-slug lookup
+ are the same lookup wearing two names -- order is a no-op either way.
+
+ 19 March 2026 (St Joseph) genuinely needs step 1 to run FIRST: the
+ observed celebration (Joseph, Class1, a real sanctoral entry with its own
+ citations) and the day's own temporal slug (ef-lent-4-thursday, ALSO a
+ real lectionary entry, with different citations) disagree. Verified
+ directly against the real data (not transcribed): both value pairs below
+ were read off the actual resolved day and the actual
+ data/ef/lectionary.sexp entry, not assumed. *)
+let test_step1_wins_over_a_competing_step2_entry () =
+ Alcotest.(check (list string))
+ "19 March 2026: Joseph's own proper wins over Lent 4 Thursday's, which the temporal slug also has"
+ [ "Ecclus 45:1-6"; "Matt 1:18-21" ]
+ (refs (day 2026 3 19))
+
+(* Fix round 1 (coordinator review): the mirror-image pin for step 2 -- a day
+ whose OBSERVED celebration carries no citations of its own (Holy Family,
+ synthesised by [Temporal_ef] itself, not sourced from
+ data/ef/sanctoral.sexp, so [Celebration.citations] is empty) falls
+ through to the temporal slug, and the temporal slug's own lectionary
+ entry is genuinely Holy Family's Mass, not the Baptism's -- RG 112(a)
+ (see test_golden.ml's own [test_holy_family_excludes_baptism_2030])
+ excludes the Baptism from this day entirely, so there is no sanctoral
+ citation anywhere to fall back to even in principle. Verified directly
+ against the real data. *)
+let test_step2_holy_family_reached_through_temporal_slug () =
+ Alcotest.(check (list string))
+ "13 January 2030: Holy Family reached via the temporal slug, the Baptism entirely absent"
+ [ "Col 3:12-17"; "Luke 2:42-52" ]
+ (refs (day 2030 1 13))
+
let suite =
[ ("step 1: sanctoral proper", `Quick, test_step1_sanctoral_proper);
- ("step 2: own temporal proper", `Quick, test_step2_lenten_feria_has_its_own) ]
+ ("step 2: own temporal proper", `Quick, test_step2_lenten_feria_has_its_own);
+ ("step 1 wins over a competing step 2 entry", `Quick,
+ test_step1_wins_over_a_competing_step2_entry);
+ ("step 2: Holy Family reached through the temporal slug, Baptism absent", `Quick,
+ test_step2_holy_family_reached_through_temporal_slug) ]
diff --git a/test/test_oracle.ml b/test/test_oracle.ml
index c1d0e6a..e1e4184 100644
--- a/test/test_oracle.ml
+++ b/test/test_oracle.ml
@@ -220,6 +220,13 @@ let real_layer () =
(List.map Overlay.diagnostic_to_string diagnostics);
layer
+(* [Rite_ef.context] takes [~lectionary] (fix round 1, coordinator review) --
+ caller-supplied, same as [real_layer] above. *)
+let real_lectionary () =
+ match Colitur_kernel.Lectionary.load "../data/ef/lectionary.sexp" with
+ | Ok l -> l
+ | Error e -> Alcotest.failf "../data/ef/lectionary.sexp: failed to load: %s" e
+
(* ---------------------------------------------------------------------- *)
(* The oracle side: one line per day, as tools/extract_missalemeum_oracle *)
(* .py's own header documents. *)
@@ -321,9 +328,10 @@ let en = Lang.of_string_exn "en"
let colitur_rows_2026_2027 () =
let layer = real_layer () in
+ let rite = Rite_ef.context ~lectionary:(real_lectionary ()) in
let by_rata : (int, (V.season, V.rank) LD.t) Hashtbl.t = Hashtbl.create 800 in
for y = 2025 to 2027 do
- let days = Cal.year Rite_ef.context layer y in
+ let days = Cal.year rite layer y in
Array.iter (fun (d : (V.season, V.rank) LD.t) -> Hashtbl.replace by_rata (Date.to_rata d.LD.date) d) days
done;
let mk y m d = match Date.make ~year:y ~month:m ~day:d with Ok t -> t | Error e -> failwith e in
diff --git a/test/test_rite_ef.ml b/test/test_rite_ef.ml
index 67b42ce..7680545 100644
--- a/test/test_rite_ef.ml
+++ b/test/test_rite_ef.ml
@@ -49,6 +49,15 @@ let real_layer () =
(List.map Overlay.diagnostic_to_string diagnostics);
layer
+(* [Rite_ef.context] takes [~lectionary] (fix round 1, coordinator review) --
+ caller-supplied, same as [real_layer] above. *)
+let real_lectionary () =
+ match Colitur_kernel.Lectionary.load "../data/ef/lectionary.sexp" with
+ | Ok l -> l
+ | Error e -> Alcotest.failf "../data/ef/lectionary.sexp: failed to load: %s" e
+
+let context () = Rite_ef.context ~lectionary:(real_lectionary ())
+
let slug_of (c : V.rank Cel.t) = Slug.to_string c.Cel.slug
(* Finding 3: the suppression's ONLY observable effect is on 24 December's
@@ -64,7 +73,7 @@ let slug_of (c : V.rank Cel.t) = Slug.to_string c.Cel.slug
alone structurally cannot. *)
let test_vigil_of_christmas_suppressed () =
let layer = real_layer () in
- let days = Cal.year Rite_ef.context layer 2026 in
+ let days = Cal.year (context ()) layer 2026 in
let christmas_eve =
Array.to_list days |> List.find (fun d -> Date.compare d.LD.date (mk 2026 12 24) = 0)
in
@@ -251,7 +260,7 @@ let test_barbara_added () =
Alcotest.(check bool) "barbara: Red (RG124(e), \"Virg. et Mart.\")" true
(e.Layer.cel.Cel.colour = Colour.Red);
Alcotest.(check bool) "barbara: appears as a commemoration on 4 December in real output" true
- (let days = Cal.year Rite_ef.context layer 2026 in
+ (let days = Cal.year (context ()) layer 2026 in
Array.to_list days
|> List.exists (fun d ->
Date.compare d.LD.date (mk 2026 12 4) = 0
@@ -281,7 +290,7 @@ let test_transfer_search_does_not_raise_at_domain_ceiling () =
in
let layer, _diagnostics = Overlay.apply layer overlay in
(* Must not raise -- the whole point of the fix. *)
- let days = Cal.year Rite_ef.context layer 9999 in
+ let days = Cal.year (context ()) layer 9999 in
Alcotest.(check bool) "year 9999 resolves without raising, even with an impeded Christmas Day"
true (Array.length days > 0);
let impeder_placed_or_recorded =
@@ -350,7 +359,7 @@ let test_no_transfer_lands_in_easter_octave () =
let violations = ref [] in
List.iter
(fun y ->
- let days = Cal.year Rite_ef.context layer y in
+ let days = Cal.year (context ()) layer y in
Array.iter
(fun (d : (V.season, V.rank) LD.t) ->
(match d.LD.transferred_in with
@@ -388,7 +397,7 @@ let test_major_litanies_transfers_inside_easter_octave_exactly_when_rg80_require
let landings = ref [] in
List.iter
(fun y ->
- let days = Cal.year Rite_ef.context layer y in
+ let days = Cal.year (context ()) layer y in
Array.iter
(fun (d : (V.season, V.rank) LD.t) ->
List.iter
@@ -491,7 +500,7 @@ let test_maurice_thomas_band_fidelity_end_to_end () =
is reached via [Cal.year ... 2026], not 2027 -- the same
[y-1]/straddling indexing test_oracle.ml's own header comment already
documents for exactly this reason. *)
- let days = Cal.year Rite_ef.context layer 2026 in
+ let days = Cal.year (context ()) layer 2026 in
let day = Array.to_list days |> List.find (fun d -> Date.compare d.LD.date (mk 2027 9 22) = 0) in
Alcotest.(check string) "the September Ember Wednesday office itself is observed (band entry 18)"
"ef-september-ember-wed" (slug_of day.LD.observed);
diff --git a/test/test_validate.ml b/test/test_validate.ml
index 9fa3c5c..8e63a1b 100644
--- a/test/test_validate.ml
+++ b/test/test_validate.ml
@@ -15,6 +15,7 @@ module T = Rite_ef.Temporal_ef
of the (test ...) stanza. *)
let sanctoral_path = "../data/ef/sanctoral.sexp"
let adjustments_path = "../data/ef/adjustments.sexp"
+let lectionary_path = "../data/ef/lectionary.sexp"
(* Loaded once at module init, not per call: [run] below is called by every
test and by the 200-sample property, and Calendar.year's own resolution
@@ -34,7 +35,16 @@ let real_ef_layer =
(String.concat "; " (List.map Overlay.diagnostic_to_string diagnostics)));
layer)
-let run year = Val.run Rite_ef.context real_ef_layer ~year
+(* [Rite_ef.context] takes [~lectionary] (fix round 1, coordinator review) --
+ caller-supplied, same as [real_ef_layer] above. *)
+let real_ef_lectionary =
+ match Colitur_kernel.Lectionary.load lectionary_path with
+ | Error e -> failwith (Printf.sprintf "%s: failed to load: %s" lectionary_path e)
+ | Ok l -> l
+
+let real_ef_rite = Rite_ef.context ~lectionary:real_ef_lectionary
+
+let run year = Val.run real_ef_rite real_ef_layer ~year
let check_year year =
match run year with