aboutsummaryrefslogtreecommitdiff
path: root/lib/rites
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rites')
-rw-r--r--lib/rites/rite_of/lectionary_of.ml107
-rw-r--r--lib/rites/rite_of/lectionary_of.mli136
2 files changed, 243 insertions, 0 deletions
diff --git a/lib/rites/rite_of/lectionary_of.ml b/lib/rites/rite_of/lectionary_of.ml
new file mode 100644
index 0000000..1664fad
--- /dev/null
+++ b/lib/rites/rite_of/lectionary_of.ml
@@ -0,0 +1,107 @@
+(* See lectionary_of.mli for the full citations and design argument. *)
+open Colitur_kernel
+
+type sunday_cycle = Year_a | Year_b | Year_c
+type weekday_cycle = Year_i | Year_ii
+
+let sunday_cycle_letter = function Year_a -> "a" | Year_b -> "b" | Year_c -> "c"
+let weekday_cycle_letter = function Year_i -> "i" | Year_ii -> "ii"
+
+(* The civil year in which the liturgical year CONTAINING [date] opened --
+ i.e. the year y such that [year_start y <= date < year_start (y + 1)].
+ This is the one piece of arithmetic both cycle rules share, and it is
+ exactly the "get the boundary right" the task brief names: a date in,
+ say, early December belongs to the liturgical year that opened THAT
+ same Advent only once Advent has actually started that year (compared
+ by real date, not by month) -- a month-based shortcut ("December always
+ means the new liturgical year") is wrong in the (real, if rare) case of
+ a December date still ahead of that year's own Advent I. *)
+let liturgical_year_open_year ~year_start date =
+ let y = Date.year date in
+ if Date.compare date (year_start y) >= 0 then y else y - 1
+
+(* OLM n.66 + footnote 102 (mli's own citation): the LABEL year is the one
+ most of the liturgical year falls in, i.e. one more than the year
+ Advent opened in ("a prima hebdomada Adventus, quae cadit in anno
+ civili praecedente" -- the first week of Advent falls in the PRECEDING
+ civil year relative to the label). Divisible by 3 -> C, then A, then B,
+ worked example 1980=C/1981=A/1982=B/1983=C verified against the real
+ page image. *)
+let sunday_cycle ~year_start date =
+ let label = liturgical_year_open_year ~year_start date + 1 in
+ match label mod 3 with 0 -> Year_c | 1 -> Year_a | _ -> Year_b
+
+(* OLM n.69 point 4 (mli's own citation): "Annus primus adhibetur annis
+ imparibus, annus secundus vero annis paribus" -- Year I in odd label
+ years, Year II in even. Same label-year computation as [sunday_cycle]
+ above, for the identical reason: this is a two-year cycle, not a
+ three-year one, but the label-year definition itself does not depend on
+ which modulus governs it. *)
+let weekday_cycle ~year_start date =
+ let label = liturgical_year_open_year ~year_start date + 1 in
+ if label mod 2 = 1 then Year_i else Year_ii
+
+(* Tries [base] flat, then [base]-<sunday letter>, then [base]-<weekday
+ letter>, in that order. Safe unconditionally: tools/
+ bootstrap_lectionary_of.ml emits a given base under AT MOST ONE of
+ these three forms (a flat entry when the two/three cycle years were
+ byte-identical, a cycle-lettered family otherwise -- never both), so at
+ most one of the three lookups below can ever hit, and trying the two
+ irrelevant forms for a family of the other kind or a flat family is
+ simply two wasted, harmless misses. This is what lets [readings] below
+ stay ignorant of which of the two cycle systems (or neither) a given
+ slug's own family actually uses. *)
+let lookup_any lectionary ~year_start base date =
+ match Lectionary.find lectionary base with
+ | Some cs -> Some cs
+ | None -> (
+ let with_suffix suffix = Slug.of_string_exn (Slug.to_string base ^ "-" ^ suffix) in
+ match Lectionary.find lectionary (with_suffix (sunday_cycle_letter (sunday_cycle ~year_start date))) with
+ | Some cs -> Some cs
+ | None -> Lectionary.find lectionary (with_suffix (weekday_cycle_letter (weekday_cycle ~year_start date))))
+
+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
+ | _ :: _ as cs ->
+ (Some { Mass_formulary.said = Some observed.Celebration.slug; via = Mass_formulary.Proper }, cs)
+ | [] -> (
+ (* Step 2: a sanctoral entity actually won the day -- i.e. the
+ observed celebration is not itself the day's own temporal office.
+ Same guard shape as Rite_ef.Lectionary_ef.readings' own step 4
+ (see that module's implementation comment for the full argument
+ for why this comparison, not a [subject]/[status] test, is the
+ right one): comparing SLUGS is exact whenever the temporal and
+ sanctoral streams cannot collide, which holds here for the
+ identical reason it holds for EF -- every Temporal_of slug this
+ module ever constructs carries the "of-" prefix (temporal_of.ml's
+ own [temporal], every branch), and 0 of the 222 shipped
+ data/of/calendar-2002.sexp + amendments sanctoral slugs do. *)
+ let sanctoral_office =
+ not (Slug.equal observed.Celebration.slug temporal.Temporal.office.Celebration.slug)
+ in
+ match
+ if sanctoral_office then lookup_any lectionary ~year_start observed.Celebration.slug date else None
+ with
+ | Some cs ->
+ (* Mass_formulary.Proper, not .Common -- see lectionary_of.mli's
+ own doc comment on this branch for the full argument. In
+ short: EF's [Common] constructor names a SPECIFIC mechanism
+ (several saints sharing one formulary via an explicit
+ assignment table, data/ef/commons.sexp) that OF's own data has
+ no counterpart for -- every sanctoral slug this step resolves
+ carries its OWN dedicated lectio entry, not a shared one, so
+ relabelling it [Common] would claim a mechanism that is not
+ actually present. [Proper] is the honest fit: "the observed
+ celebration's own proper Mass", true regardless of whether the
+ citations are embedded on Celebration.t (step 1) or held
+ externally in [lectionary] (this step) -- a storage detail
+ 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
+ | 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
new file mode 100644
index 0000000..0c4d1d6
--- /dev/null
+++ b/lib/rites/rite_of/lectionary_of.mli
@@ -0,0 +1,136 @@
+(** The OF reading-cycle arithmetic and the temporal + sanctoral lectionary
+ resolution chain (Task 4, 2026-08-25-colitur-of-phases-3-5).
+
+ Two halves with a different character, per the task's own brief:
+
+ - {b The cycle rules are CODE}, computed from the civil year, cited to
+ the OLM (Ordo Lectionum Missae) 1981 Praenotanda -- see {!sunday_cycle}
+ and {!weekday_cycle} below for the citations, each verified against the
+ real page image (docs/research/of/olm-1981.pdf pp.32-33/"XXXII-XXXIII"),
+ not the unreliable OCR text layer (design spec 2026-08-24-colitur-of-
+ rite-module-design.md sec4.4's own caveat).
+ - {b The data is bootstrapped from lectio} (binding decision 3), a
+ POLISH VERNACULAR pastoral lectionary (niedziela.pl), not the Latin
+ OLM itself -- see tools/bootstrap_lectionary_of.ml's own top-of-file
+ comment and data/of/lectionary.sexp's own provenance header for the
+ full lineage statement and what it cannot show. *)
+
+open Colitur_kernel
+
+(** OLM 1981 Praenotanda n.66 (page-image verified, p.32/"XXXII"): "cyclus
+ trium annorum proponitur" -- the three-year Sunday/solemnity cycle.
+ Footnote 102, same page, gives the determining rule with a worked
+ example: "Per litteram C designatur annus cuius numerus in tres partes
+ aequales dividi potest... Sic annus 1980 est annus C, annus vero
+ sequens, scilicet annus 1981, est annus A, annus vero 1982 est annus B,
+ et annus 1983 est iterum annus C" -- the year divisible by three is
+ Year C, the next Year A, the next Year B. The SAME footnote also gives
+ the Gospel-per-year identity quoted on {!sunday_cycle}'s own doc
+ comment below (A = Matthew, B = Mark, C = Luke), and states explicitly
+ that each cycle's own "year" runs "iuxta dispositionem anni liturgici,
+ nempe a prima hebdomada Adventus, quae cadit in anno civili
+ praecedente" -- according to the liturgical year, namely from the first
+ week of Advent, WHICH FALLS IN THE PRECEDING CIVIL YEAR. This is the
+ task brief's own "the cycle turns at Advent I, not 1 January": Advent
+ 1980 (civil year 1980) already opens the 1981-labelled liturgical year
+ (Year A), even though most of civil year 1980 was still Year C. *)
+type sunday_cycle = Year_a | Year_b | Year_c
+
+(** OLM 1981 Praenotanda n.69 (page-image verified, p.33/"XXXIII"), the
+ "c) De ordinatione lectionum pro feriis" subsection's own point 4:
+ "Pro feriis vero triginta quattuor hebdomadarum <<per annum>> lectiones
+ evangelicae unico disponuntur cyclo, qui singulis annis resumitur.
+ Prior vero lectio, in duplici cyclo ordinatur, alternis annis legenda.
+ Annus primus adhibetur annis imparibus, annus secundus vero annis
+ paribus" -- for the ferias of the thirty-four weeks of Ordinary Time,
+ the GOSPEL is arranged in a single cycle repeated every year; the FIRST
+ reading alone alternates over two years. Year I is used in odd years,
+ Year II in even years. The SAME point 4, its own preceding points 2-3,
+ state the two-year alternation is confined to Ordinary Time: Lent has
+ its own single seasonal cycle (point 2) and "Pro feriis Adventus,
+ temporis Nativitatis et paschalis cyclus eodem modo annualis est:
+ lectiones ideo non mutantur" -- for the ferias of Advent, Christmastide
+ and Paschaltide the cycle is likewise annual: the readings DO NOT
+ CHANGE (point 3). {!Lectionary.t} does not need to encode that
+ distinction itself: a non-alternating family's own two cycle-letter
+ keys were collapsed to one flat entry at bootstrap time (see
+ tools/bootstrap_lectionary_of.ml's own [collapse_weekday]), so trying
+ both letters here is always safe -- the flat entry is found first
+ either way. *)
+type weekday_cycle = Year_i | Year_ii
+
+val sunday_cycle_letter : sunday_cycle -> string
+val weekday_cycle_letter : weekday_cycle -> string
+
+(** [sunday_cycle ~year_start date]: which of the three-year cycle's own
+ labelled years [date] falls in, per {!sunday_cycle}'s own citation.
+ A = Matthew, B = Mark, C = Luke -- OLM n.66's own footnote 102, same
+ passage: "primus cycli annus dicitur et est annus legendi Matthaeum,
+ ceteri vero, secundus nempe et tertius, respective annus legendi
+ Marcum et annus legendi Lucam". [year_start] is the rite's own
+ Advent-anchor function ({!Temporal_of.year_start}), supplied by the
+ caller rather than hard-coded, the same seam {!Rite_ef.Lectionary_ef
+ .readings}'s own [~lectionary]/[~commons] already use for rite data --
+ this module reads no filesystem and knows no OF-specific season
+ vocabulary of its own beyond what [year_start] and {!Colitur_kernel
+ .Temporal.t} already carry. *)
+val sunday_cycle : year_start:(int -> Date.t) -> Date.t -> sunday_cycle
+
+(** [weekday_cycle ~year_start date]: which of the two-year cycle's own
+ labelled years [date] falls in, per {!weekday_cycle}'s own citation. *)
+val weekday_cycle : year_start:(int -> Date.t) -> Date.t -> weekday_cycle
+
+(** The Mass's Epistle and Gospel citations for this day, and (when the
+ shipped data can name one) which formulary they came from -- the OF
+ counterpart of {!Rite_ef.Lectionary_ef.readings}, deliberately simpler:
+ OF has no Commons indirection table (spec's own scope for this task),
+ so the chain is three steps, not four:
+
+ - {b Step 1} -- the observed celebration's own embedded citations, if
+ any. {!Colitur_kernel.Mass_formulary.Proper}. Defensive: every
+ shipped data/of/calendar-2002.sexp entry carries [citations ()]
+ today (Task 1), so this step is currently dead on shipped data, kept
+ for the same forward-compatibility reason EF's own step 1 stays
+ first even where most callers reach it rarely.
+ - {b Step 2} -- when the observed celebration is NOT itself the day's
+ own temporal office (a real sanctoral entity won occurrence), its
+ own slug looked up directly in [lectionary] (tried flat, then both
+ cycle-letter suffixes via {!sunday_cycle}/{!weekday_cycle} -- a
+ sanctoral entry may be tagged either way in the shipped data, see
+ tools/bootstrap_lectionary_of.ml's own header, so both are tried
+ rather than assumed). Tagged {!Colitur_kernel.Mass_formulary.Proper}
+ too, not {!Colitur_kernel.Mass_formulary.Common}: OF has no shared-
+ formulary indirection the way EF's Commons do (this task's own
+ scope), so a looked-up sanctoral citation is still, substantively,
+ "the observed celebration's own proper" -- only ITS STORAGE differs
+ from step 1's (external file vs an embedded field), which
+ {!Colitur_kernel.Mass_formulary.source} was never designed to
+ distinguish (its own doc names what DECIDED the Mass, not where the
+ bytes happen to live). {!Colitur_kernel.Mass_formulary.source} is a
+ 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.
+ - 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.
+
+ NO preceding-Sunday fallback (EF's own step 3): unlike EF's RG 299,
+ the OF's own two-year FERIAL cycle assigns every Ordinary Time weekday
+ its own reading by design (OLM n.69 point 4, {!weekday_cycle}'s own
+ citation) -- there is no OF norm this task found instructing a ferial
+ with no proper of its own to repeat the preceding Sunday's Mass, so no
+ such step is built. [temporal_at] is therefore accepted, unused, to
+ satisfy {!Colitur_kernel.Rite.t}'s own [readings] field shape (the same
+ contract {!Rite_ef.Lectionary_ef.readings} satisfies) -- kept rather
+ than dropped so a future task that DOES find such a rule has the
+ callback already in place. *)
+val readings :
+ lectionary:Lectionary.t ->
+ year_start:(int -> Date.t) ->
+ observed:Vocab_of.rank Celebration.t ->
+ temporal:(Vocab_of.season, Vocab_of.rank) Temporal.t ->
+ date:Date.t ->
+ temporal_at:(Date.t -> (Vocab_of.season, Vocab_of.rank) Temporal.t) ->
+ Mass_formulary.t option * Citation.t list