aboutsummaryrefslogtreecommitdiff
path: root/lib/rites/rite_of
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 10:11:47 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 10:11:47 +0200
commitb6d0eba04d1da51515dad70127112898b500c1bc (patch)
treea82263c3d436d94b6208b14c61295b0e550fbe93 /lib/rites/rite_of
parent8fa9db25f55f4ae92ad6a28115484dd981593a3c (diff)
downloadcolitur-b6d0eba04d1da51515dad70127112898b500c1bc.tar.gz
colitur-b6d0eba04d1da51515dad70127112898b500c1bc.zip
fix(of-lectionary): stop serving drifting readings for O-Antiphon and Christmas-season dates
The bootstrap excluded 19 lectio bases as "date-keyed duplicates", but 17 of them were not duplicates at all: the 8 O-Antiphon days (17-24 December) and 8 further Christmas-season dates (29-31 December, 2-5 and 7 January) each carry unique per-date content found nowhere else among the emitted entries, verified against lectio's own ini directly. 17 December is Gen 49:2,8-10 / Matthew 1:1-17, but colitur was falling through to that year's unrelated weekday-keyed "advent-3-mon" content instead. OLM n. 69.3 explains why: those ferias are fixed by civil date, unlike every other Advent/Christmastide/Paschaltide feria, which is merely non-alternating within a weekday slot. Temporal_of's own ferial slugs for these dates are weekday-keyed, so looking one of those up served whichever OTHER date happened to share that year's weekday alignment -- a citation that silently drifted year to year on ~16 real civil dates (6 January stays excluded: Temporal_of fixes Epiphany there unconditionally, so that date can never reach the ferial path at all; two more, easter-6-thu and advent-4-sat, stay excluded for their own, different structural reasons). Fixed with a date-keyed lookup route (Lectionary_of.date_keyed_slug), tried before the weekday-keyed one in readings' own step 3. Temporal_of's slugs are unchanged -- only which lectionary key resolves a day's citations. tools/bootstrap_lectionary_of.ml now maps the 17 bases to synthetic date-keyed slugs instead of excluding them, and its own reachability sweep calls date_keyed_slug directly so the two can never drift apart. Re-emitted data/of/lectionary.sexp (754 -> 770 entries) and corrected its provenance header, which previously described these dates as excluded duplicates. Also states, for the first time, a Minor finding from the same review: 190 of 222 shipped sanctoral slugs have no dedicated lectionary entry and correctly fall through to the ferial per OLM norms -- expected behaviour, not a gap, but never said in the coverage report before. New tests pin 17 December (Gen 49:2,8-10 / Matthew 1:1-17) and 2 January (1 John 2:22-28 / John 1:19-28) in two years with different weekday alignments, proving neither reading drifts, plus direct boundary coverage of date_keyed_slug itself (both windows, the 6 January exclusion, and the Sunday guard).
Diffstat (limited to 'lib/rites/rite_of')
-rw-r--r--lib/rites/rite_of/lectionary_of.ml36
-rw-r--r--lib/rites/rite_of/lectionary_of.mli50
2 files changed, 82 insertions, 4 deletions
diff --git a/lib/rites/rite_of/lectionary_of.ml b/lib/rites/rite_of/lectionary_of.ml
index 1664fad..eb3e4e0 100644
--- a/lib/rites/rite_of/lectionary_of.ml
+++ b/lib/rites/rite_of/lectionary_of.ml
@@ -60,6 +60,24 @@ let lookup_any lectionary ~year_start base date =
| Some cs -> Some cs
| None -> Lectionary.find lectionary (with_suffix (weekday_cycle_letter (weekday_cycle ~year_start date))))
+(* See lectionary_of.mli's own doc comment for the full citation (OLM n.
+ 69.3) and argument. Kept as ordinary [Printf.sprintf] string
+ construction, not a lookup table, so tools/bootstrap_lectionary_of.ml's
+ own reachability sweep (which calls this function directly, not a
+ hand-copied re-implementation) and this module's own runtime lookup can
+ never drift apart -- the single failure mode a hand-duplicated version
+ in each place would invite. *)
+let date_keyed_slug date =
+ if Date.weekday date = Date.Sun then None
+ else
+ let m = Date.month date and dd = Date.day date in
+ if m = 12 && dd >= 17 && dd <= 24 then Some (Slug.of_string_exn (Printf.sprintf "of-advent-dec-%d" dd))
+ else if m = 12 && dd >= 29 && dd <= 31 then
+ Some (Slug.of_string_exn (Printf.sprintf "of-christmas-dec-%d" dd))
+ else if m = 1 && ((dd >= 2 && dd <= 5) || dd = 7) then
+ Some (Slug.of_string_exn (Printf.sprintf "of-christmas-jan-%d" dd))
+ else None
+
let readings ~lectionary ~year_start ~(observed : Vocab_of.rank Celebration.t)
~(temporal : (Vocab_of.season, Vocab_of.rank) Temporal.t) ~date ~temporal_at:_ =
match observed.Celebration.citations with
@@ -99,9 +117,23 @@ let readings ~lectionary ~year_start ~(observed : Vocab_of.rank Celebration.t)
Mass_formulary.source was never built to carry. *)
(Some { Mass_formulary.said = Some observed.Celebration.slug; via = Mass_formulary.Proper }, cs)
| None -> (
- match lookup_any lectionary ~year_start temporal.Temporal.office.Celebration.slug date with
+ (* OLM n. 69.3's own date-fixed windows (see date_keyed_slug's
+ own doc comment): tried BEFORE the ordinary weekday-keyed
+ slug lookup, not instead of it, so every other temporal day
+ is unaffected -- this branch only ever hits inside the two
+ named civil-date windows, and [date_keyed_slug] itself
+ already refuses a Sunday, where a NAMED Sunday office (never
+ one of these weekday-keyed ferial slugs) is what temporal.
+ office actually names. *)
+ match (match date_keyed_slug date with Some s -> Lectionary.find lectionary s | None -> None) with
| Some cs ->
( Some { Mass_formulary.said = Some temporal.Temporal.office.Celebration.slug;
via = Mass_formulary.Own_slug },
cs )
- | None -> (None, [])))
+ | None -> (
+ match lookup_any lectionary ~year_start temporal.Temporal.office.Celebration.slug date with
+ | Some cs ->
+ ( Some { Mass_formulary.said = Some temporal.Temporal.office.Celebration.slug;
+ via = Mass_formulary.Own_slug },
+ cs )
+ | None -> (None, []))))
diff --git a/lib/rites/rite_of/lectionary_of.mli b/lib/rites/rite_of/lectionary_of.mli
index 0c4d1d6..4a447d2 100644
--- a/lib/rites/rite_of/lectionary_of.mli
+++ b/lib/rites/rite_of/lectionary_of.mli
@@ -110,8 +110,13 @@ val weekday_cycle : year_start:(int -> Date.t) -> Date.t -> weekday_cycle
closed kernel type outside this task's own scope to extend; see
lectionary_of.ml's own implementation comment on this branch for the
full argument. [said] the observed slug.
- - {b Step 3} -- the day's own temporal slug, same three-way lookup.
- {!Colitur_kernel.Mass_formulary.Own_slug}, [said] that slug.
+ - {b Step 3} -- the day's own temporal slug. FIRST tries
+ {!date_keyed_slug} (OLM n. 69.3's date-fixed windows -- the
+ O-Antiphons and the pre-Epiphany run of the Christmas season), THEN
+ falls back to the same three-way slug lookup step 2 uses.
+ {!Colitur_kernel.Mass_formulary.Own_slug} either way, [said] the
+ temporal office's own slug -- {!date_keyed_slug} changes which KEY
+ resolves the citations, not which office is said to have said them.
- A day matching neither gets [(None, [])] -- see data/of/lectionary
.sexp's own provenance header for exactly which days that is, both
directions, measured, not guessed.
@@ -134,3 +139,44 @@ val readings :
date:Date.t ->
temporal_at:(Date.t -> (Vocab_of.season, Vocab_of.rank) Temporal.t) ->
Mass_formulary.t option * Citation.t list
+
+(** OLM 1981 Praenotanda n. 69.3 (the same passage {!weekday_cycle}'s own
+ doc comment cites: "Pro feriis Adventus, temporis Nativitatis et
+ paschalis cyclus eodem modo annualis est: lectiones ideo non
+ mutantur" -- Advent/Christmastide/Paschaltide ferial readings do not
+ change year to year). Every OTHER such family is non-alternating
+ WITHIN a weekday slot ({!weekday_cycle}); two short windows go
+ further and are keyed by CIVIL DATE instead: the O-Antiphon days
+ (17-24 December) and the pre-Epiphany run of the Christmas season
+ (29-31 December, then 2-5 and 7 January) -- verified concretely
+ against lectio's own ini, not merely inferred from the citation: 17
+ December is Gen 49:2,8-10 / Ps 72:1-2,3-4ab,7-8,17 / Matthew 1:1-17
+ regardless which weekday it falls on in a given year.
+ {!Rite_of.Temporal_of}'s own ferial slugs for these same civil dates
+ are WEEKDAY-keyed
+ ({!Rite_of.Temporal_of.christmas_feria_slug}/the Advent ferial
+ branch), so looking one of THOSE slugs up would silently serve
+ whichever OTHER date happens to share that year's weekday alignment
+ -- the defect this function exists to close (bug found by review, not
+ by a test: colitur used to serve 17 December the unrelated
+ "advent-3-mon" reading in years where the two happened to align).
+
+ 6 January is deliberately NOT one of the six Christmas-season dates,
+ despite lectio's own "christmas-jan-6" base existing:
+ {!Rite_of.Temporal_of}'s own [named] fixes Epiphany to 6 January
+ unconditionally (temporal_of.ml, "m = 1 && dd = 6"), so that date is
+ never reached as a ferial by {!readings}'s own step 3 in the first
+ place -- a real lectio entry with no reachable colitur day, the same
+ "structurally unreachable" shape as tools/bootstrap_lectionary_of
+ .ml's own "easter-6-thu"/"advent-4-sat" (see its own [excluded_bases]
+ comment).
+
+ [None] on a Sunday: Advent 4, Holy Family, the Second Sunday after
+ Christmas and the Baptism of the Lord can each land inside these two
+ windows, each is a NAMED office with its own distinct slug
+ ({!Rite_of.Temporal_of.sunday_slug}), and each must keep taking
+ priority over a ferial date -- checked here explicitly rather than
+ trusted to the call site, since this function's whole contract is
+ "this civil date, unconditionally, is this citation", which is false
+ on the Sunday the window shares its calendar date with. *)
+val date_keyed_slug : Date.t -> Slug.t option