diff options
| -rw-r--r-- | bin/main.ml | 91 | ||||
| -rw-r--r-- | dune | 10 | ||||
| -rw-r--r-- | lib/rites/rite_ef/lectionary_ef.ml | 44 | ||||
| -rw-r--r-- | lib/rites/rite_ef/lectionary_ef.mli | 18 | ||||
| -rw-r--r-- | lib/rites/rite_ef/rite_ef.ml | 13 | ||||
| -rw-r--r-- | lib/rites/rite_ef/rite_ef.mli | 20 | ||||
| -rw-r--r-- | test/test_calendar.ml | 13 | ||||
| -rw-r--r-- | test/test_differential.ml | 10 | ||||
| -rw-r--r-- | test/test_golden.ml | 12 | ||||
| -rw-r--r-- | test/test_lectionary_ef.ml | 59 | ||||
| -rw-r--r-- | test/test_oracle.ml | 10 | ||||
| -rw-r--r-- | test/test_rite_ef.ml | 21 | ||||
| -rw-r--r-- | test/test_validate.ml | 12 |
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>"; @@ -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 |
