summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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