summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-15 00:16:35 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-15 00:16:35 +0200
commit124d7e2261c721c4a49f7203efc276088c7cb217 (patch)
tree9aa82ff189c924e9327236d77acd6891114108cb
parentf1d90a83ece4d8301061e8247e23ec2af8293ab4 (diff)
downloadcolitur-124d7e2261c721c4a49f7203efc276088c7cb217.tar.gz
colitur-124d7e2261c721c4a49f7203efc276088c7cb217.zip
kernel+ef: resolve readings, chain steps 1 and 2
Liturgical_day.citations has read "always empty until Plan 4" since Plan 3; it is now filled. Rite.t gains a readings function, rite-supplied for the same reason transfer_target is: what a day with no proper falls back to is a rubric, not a universal. Calendar calls it and passes its own temporal function as the callback the rite needs to reach another date. Steps 1 and 2 only: the observed celebration's own proper, else the day's own temporal slug. Nothing encodes "Lent has daily propers" -- the presence of an entry is the discriminator.
-rw-r--r--lib/kernel/calendar.ml4
-rw-r--r--lib/kernel/rite.ml6
-rw-r--r--lib/kernel/rite.mli14
-rw-r--r--lib/rites/rite_ef/lectionary_ef.ml40
-rw-r--r--lib/rites/rite_ef/lectionary_ef.mli15
-rw-r--r--lib/rites/rite_ef/rite_ef.ml4
-rw-r--r--lib/rites/rite_ef/rite_ef.mli4
-rw-r--r--test/test_calendar.ml6
-rw-r--r--test/test_colitur.ml3
-rw-r--r--test/test_lectionary_ef.ml68
-rw-r--r--test/test_validate.ml6
11 files changed, 165 insertions, 5 deletions
diff --git a/lib/kernel/calendar.ml b/lib/kernel/calendar.ml
index cffdf69..724641d 100644
--- a/lib/kernel/calendar.ml
+++ b/lib/kernel/calendar.ml
@@ -435,7 +435,9 @@ let build_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.by_date)
transferred_in;
transferred_out;
omitted;
- citations = [];
+ citations =
+ rite.Rite.readings ~observed:resolution.Precedence.observed.Precedence.cel ~temporal ~date
+ ~temporal_at:rite.Rite.temporal;
}
let year (rite : ('s, 'r) Rite.t) (layer : 'r Layer.t) (y : int) :
diff --git a/lib/kernel/rite.ml b/lib/kernel/rite.ml
index 89fceb6..91326a6 100644
--- a/lib/kernel/rite.ml
+++ b/lib/kernel/rite.ml
@@ -11,4 +11,10 @@ type ('s, 'r) t = {
season_runs : 's list;
transfer_target :
'r Precedence.candidate -> Date.t -> (Date.t -> 'r Celebration.t) -> Date.t;
+ readings :
+ observed:'r Celebration.t ->
+ temporal:('s, 'r) Temporal.t ->
+ date:Date.t ->
+ temporal_at:(Date.t -> ('s, 'r) Temporal.t) ->
+ Citation.t list;
}
diff --git a/lib/kernel/rite.mli b/lib/kernel/rite.mli
index ffe9471..a41e034 100644
--- a/lib/kernel/rite.mli
+++ b/lib/kernel/rite.mli
@@ -59,4 +59,18 @@ type ('s, 'r) t = {
[transfer_target] for a concrete termination argument (a
structural step bound, not an appeal to the real calendar's own
structure). *)
+ readings :
+ observed:'r Celebration.t ->
+ temporal:('s, 'r) Temporal.t ->
+ date:Date.t ->
+ temporal_at:(Date.t -> ('s, 'r) Temporal.t) ->
+ Citation.t list;
+ (** The day's Epistle and Gospel citations, or []. Rite-supplied for the
+ same reason [transfer_target] is: what a day with no proper of its
+ own falls back to is a rubric of a particular rite, not a universal.
+
+ [temporal_at] is a callback so the rite can reach another date's
+ temporal identity (the preceding Sunday's, for the ferial rule)
+ without re-implementing the temporal cycle -- the same shape
+ [transfer_target]'s own [occupant] callback established. *)
}
diff --git a/lib/rites/rite_ef/lectionary_ef.ml b/lib/rites/rite_ef/lectionary_ef.ml
new file mode 100644
index 0000000..f9ce9ab
--- /dev/null
+++ b/lib/rites/rite_ef/lectionary_ef.ml
@@ -0,0 +1,40 @@
+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:_ =
+ match observed.Celebration.citations with
+ | _ :: _ as cs -> cs
+ | [] -> (
+ match Lectionary.find lectionary temporal.Temporal.office.Celebration.slug with
+ | Some cs -> cs
+ | None -> [])
diff --git a/lib/rites/rite_ef/lectionary_ef.mli b/lib/rites/rite_ef/lectionary_ef.mli
new file mode 100644
index 0000000..fe9359e
--- /dev/null
+++ b/lib/rites/rite_ef/lectionary_ef.mli
@@ -0,0 +1,15 @@
+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.
+
+ 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 :
+ observed:Vocab_ef.rank Celebration.t ->
+ temporal:(Vocab_ef.season, Vocab_ef.rank) Temporal.t ->
+ date:Date.t ->
+ temporal_at:(Date.t -> (Vocab_ef.season, Vocab_ef.rank) Temporal.t) ->
+ Citation.t list
diff --git a/lib/rites/rite_ef/rite_ef.ml b/lib/rites/rite_ef/rite_ef.ml
index 7a29421..4cf52a6 100644
--- a/lib/rites/rite_ef/rite_ef.ml
+++ b/lib/rites/rite_ef/rite_ef.ml
@@ -7,6 +7,7 @@
module Vocab_ef = Vocab_ef
module Temporal_ef = Temporal_ef
module Precedence_ef = Precedence_ef
+module Lectionary_ef = Lectionary_ef
open Colitur_kernel
@@ -21,4 +22,5 @@ let context : (Vocab_ef.season, Vocab_ef.rank) Rite.t =
disposition = Precedence_ef.disposition;
admit = Precedence_ef.admit };
season_runs = Vocab_ef.seasons;
- transfer_target = Precedence_ef.transfer_target }
+ transfer_target = Precedence_ef.transfer_target;
+ readings = Lectionary_ef.readings }
diff --git a/lib/rites/rite_ef/rite_ef.mli b/lib/rites/rite_ef/rite_ef.mli
index e2b3e6d..710db1d 100644
--- a/lib/rites/rite_ef/rite_ef.mli
+++ b/lib/rites/rite_ef/rite_ef.mli
@@ -9,6 +9,7 @@
module Vocab_ef = Vocab_ef
module Temporal_ef = Temporal_ef
module Precedence_ef = Precedence_ef
+module Lectionary_ef = Lectionary_ef
(** The EF rite, bundled (design spec's [RITE] signature, realised as a
{!Colitur_kernel.Rite.t} value rather than a functor -- see rite.mli):
@@ -24,6 +25,9 @@ module Precedence_ef = Precedence_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).
Deliberately carries no [sanctoral]/[lectionary] fields the way the
original design-doc sketch of [RITE] does: {!Colitur_kernel.Rite.t} (the
diff --git a/test/test_calendar.ml b/test/test_calendar.ml
index 22f5f04..2f6df6d 100644
--- a/test/test_calendar.ml
+++ b/test/test_calendar.ml
@@ -99,9 +99,13 @@ module Fixture = struct
let rec search d = if (occupant d).Cel.rank = Lo then d else search (D.add_days d 1) in
search (D.add_days origin 1)
+ (* No fixture here exercises citations -- readings is a harmless constant
+ [], the same role [empty_layer] plays for the sanctoral side. *)
+ let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = []
+
let rite : (season, rank) Rite.t =
{ Rite.id = "synthetic-calendar"; vocab; year_start; temporal; anchors = (fun _ -> []);
- rules; season_runs = [ A; B ]; transfer_target }
+ rules; season_runs = [ A; B ]; transfer_target; readings }
let entry ~month ~day ~slug ~rank =
{ Layer.date = (match Date_spec.fixed ~month ~day with Ok d -> d | Error e -> failwith e);
diff --git a/test/test_colitur.ml b/test/test_colitur.ml
index 66aec59..db2d41c 100644
--- a/test/test_colitur.ml
+++ b/test/test_colitur.ml
@@ -5,4 +5,5 @@ let () =
Test_overlay.suite; Test_temporal_ef.suite; Test_validate.suite; Test_precedence.suite;
Test_calendar.suite; Test_precedence_ef.suite; Test_sanctoral_ef.suite; Test_rite_ef.suite;
Test_differential.suite; Test_oracle.suite; Test_golden.suite;
- ("lectionary", Test_lectionary.suite) ]
+ ("lectionary", Test_lectionary.suite);
+ ("lectionary-ef", Test_lectionary_ef.suite) ]
diff --git a/test/test_lectionary_ef.ml b/test/test_lectionary_ef.ml
new file mode 100644
index 0000000..a1a324f
--- /dev/null
+++ b/test/test_lectionary_ef.ml
@@ -0,0 +1,68 @@
+open Colitur_kernel
+open Rite_ef
+
+(* Same pattern test_rite_ef.ml already uses: both [Calendar.year] and
+ [Calendar.day] take the rite AND the sanctoral layer (Rite.t carries no
+ layer of its own -- see rite_ef.mli's own note on why), so step 1's
+ sanctoral-proper case needs the REAL data/ef/sanctoral.sexp +
+ data/ef/adjustments.sexp loaded, not a bare [Rite_ef.context]. Relative
+ to this test's own build directory (_build/default/test/); test/dune
+ declares both as deps. *)
+let sanctoral_path = "../data/ef/sanctoral.sexp"
+let adjustments_path = "../data/ef/adjustments.sexp"
+
+let real_layer () =
+ let layer =
+ match Layer.load Vocab_ef.rank_of_sexp sanctoral_path with
+ | Ok l -> l
+ | Error e -> Alcotest.failf "%s: failed to load: %s" sanctoral_path e
+ in
+ let overlay =
+ match Overlay.load Vocab_ef.rank_of_sexp adjustments_path with
+ | Ok o -> o
+ | Error e -> Alcotest.failf "%s: failed to load: %s" adjustments_path e
+ in
+ let layer, diagnostics = Overlay.apply layer overlay in
+ Alcotest.(check (list string)) "the committed overlay applies cleanly, no diagnostics" []
+ (List.map Overlay.diagnostic_to_string diagnostics);
+ layer
+
+(* [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
+ belongs to the liturgical year that opened in Advent 2029. [Calendar.day]
+ finds the containing liturgical year itself; see calendar.mli. *)
+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
+
+let refs (ld : _ Liturgical_day.t) =
+ List.map (fun c -> c.Citation.reference) ld.citations
+
+(* Chain step 1: the observed celebration's own proper wins.
+
+ NOT 2030: that is the one pinned collision year (test_golden.ml,
+ [test_holy_family_excludes_baptism_2030]) where 13 January is itself the
+ Holy Family Sunday and RG 112(a) excludes the Baptism commemoration
+ entirely -- [observed] there is [ef-time-after-epiphany-sunday-1], not
+ this slug, so it is the wrong year to exercise step 1 against. 2026 is an
+ ordinary year (13 January a Tuesday, no Sunday collision), where the
+ fixed Commemoration of the Baptism of the Lord (Class2, subject Lord,
+ data/ef/sanctoral.sexp) is observed outright. *)
+let test_step1_sanctoral_proper () =
+ Alcotest.(check (list string))
+ "13 January 2026, Commemoration of the Baptism of the Lord"
+ [ "Isa 60:1-6"; "John 1:29-34" ]
+ (refs (day 2026 1 13))
+
+(* Chain step 2: the day's own temporal slug has a proper (Lent has one daily). *)
+let test_step2_lenten_feria_has_its_own () =
+ Alcotest.(check (list string))
+ "Monday of the 1st week of Lent is not the Sunday's Mass"
+ [ "Ezech 34:11-16"; "Matt 25:31-46" ]
+ (refs (day 2026 2 23))
+
+let suite =
+ [ ("step 1: sanctoral proper", `Quick, test_step1_sanctoral_proper);
+ ("step 2: own temporal proper", `Quick, test_step2_lenten_feria_has_its_own) ]
diff --git a/test/test_validate.ml b/test/test_validate.ml
index 501dd2e..9fa3c5c 100644
--- a/test/test_validate.ml
+++ b/test/test_validate.ml
@@ -276,10 +276,14 @@ module Synthetic = struct
defaults against the default empty [layer], since nothing ever contests
the temporal office there) so the resolution fixtures further down can
override them without duplicating every other field. *)
+ (* No fixture here exercises citations -- readings is a harmless constant
+ [], the same role the other placeholder defaults above play. *)
+ let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = []
+
let rite ?(vocab = vocab) ?(anchors = fun _ -> []) ?(season_runs = [ A; B ]) ?(rules = rules)
?(transfer_target = fun _ origin _ -> origin) temporal : (season, rank) Rite.t =
{ Rite.id = "synthetic"; vocab; year_start; temporal; anchors; rules; season_runs;
- transfer_target }
+ transfer_target; readings }
(* Empty by default: every check built before Task 12 exercises the
TEMPORAL-only pass, where an empty layer is exactly the fixture that