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] and [commons] are both 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 and data/ef/commons.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 ` -- 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. The Commons follow the same path for the same reason. *) (** The Commons of the 1962 Missal (Epistle and Gospel citations only) and the per-saint assignments that route a readingless class-3 feast to one. Two tables rather than one, because the two facts have different warrants and different lifetimes: a FORMULARY is read from the Commune Sanctorum and is the same for every saint sent to it, while an ASSIGNMENT is read from one saint's own date in the Proprium Sanctorum. A diocesan overlay adds assignments; it rarely adds formularies. Assignment is explicit per saint, never inferred: {!Colitur_kernel.Subject.t} is [Temporal|Saint|Bvm|Lord] and {!Colitur_kernel.Celebration.t} carries no martyr/confessor/virgin/bishop/abbot classification at all, so there is nothing to infer one from -- those words appear only inside display names. A saint with no proper and no assignment gets no Common. *) module Commons : sig type t (** No formularies and no assignments -- the identity for this table, and what a caller that genuinely has no Commons data should pass. Every lookup returns [None]; nothing is silently invented. *) val empty : t (** Loads from a sexp file. Parse and validation failures come back as [Error], never as an exception, and never at module-initialisation time -- the same contract {!Colitur_kernel.Lectionary.load} makes. [Error] (never a silently-degraded lookup) on: a duplicate common id; a duplicate assignment for one saint; a formulary with no citations (indistinguishable downstream from "no Common at all"); and an assignment naming a common that does not exist (likewise). *) val load : string -> (t, string) result (** Builds a table directly, with exactly the validation {!load} applies to a file (same four [Error]s, same canonical sort). Exposed so that a caller can construct Commons from something other than a sexp file -- a future diocesan overlay, or a test that needs a table the shipped data deliberately does not contain. The guard on {!readings}' step 4 is one such case: no shipped assignment names a temporal slug, so the only way to exercise the guard at all is to build a table that does. *) val of_tables : commons:(Slug.t * Citation.t list) list -> assigned:(Slug.t * Slug.t) list -> (t, string) result (** The Commons themselves, canonically sorted by id. *) val formularies : t -> (Slug.t * Citation.t list) list (** Saint slug -> common id, canonically sorted by saint. *) val assignments : t -> (Slug.t * Slug.t) list end (** The Common assigned to a saint who has no proper, if any -- its own id ALONGSIDE its citations, not the citations alone: {!readings}' step 4 needs the id to name which Common fired in the {!Colitur_kernel.Mass_formulary.t} it builds. Exposed for the golden pins too, which must show WHICH Common fired, not merely that two citations appeared. Takes the table explicitly for the same reason {!readings} takes [~lectionary]: the data is the caller's, not this module's. *) val commons_for : commons:Commons.t -> Slug.t -> (Slug.t * Citation.t list) option (** The Mass actually said -- which formulary, and how that was decided -- paired with the day's Epistle and Gospel citations. [(None, [])] when none of the four steps below answers. Four steps, in EXECUTION order 1, 4, 2, 3 (the numbers are the plan's and are kept as written, so that every "step 3" already recorded in a test name, comment or report still means the same branch). Each step's own {!Colitur_kernel.Mass_formulary.source} is built at the point the step decides, not re-derived afterwards from the citations it returns: - {b Step 1} -- the observed celebration's own proper. {!Colitur_kernel.Mass_formulary.Proper}, [said] the observed slug. - {b Step 4} -- a saint who is the day's observed office and has no proper says his assigned Common. Runs before the temporal fallbacks, not after them: this is the only step in the chain with a direct primary-source warrant (the Missal names the Mass at each such saint's own date), and placing it last makes it unreachable on every date in 1583-9999 as well as wrong on the days it would fire. The full argument, with the measurement behind it, is on the branch itself. Guarded so it can only ever apply to a SANCTORAL observed office -- a feria, a Sunday, the Triduum and the RG 78 Saturday Office of the BVM (whose observed celebration is its own temporal office) are structurally excluded, not merely absent from the data. {!Colitur_kernel.Mass_formulary.Common}, [said] the Common's own id. - {b Step 2} -- the day's own temporal slug in the lectionary. {!Colitur_kernel.Mass_formulary.Own_slug}, [said] that slug. The RG 309(a) Saturday votive Mass of Our Lady also answers here (structurally, not as a fifth numbered step), but is tagged {!Colitur_kernel.Mass_formulary.Votive} instead, not [Own_slug]: RG 309(a)/431(e) classify it, in the Missal's own words, as a "Missa votiva" said IN PLACE of the day's own office's Mass, the office (RG 78) itself being kept -- witnessed by the Latin Mass Society Ordo, which prints this day's Mass as "V" (Votive). [said] is still that slug: only the source constructor differs from an ordinary Step 2 lookup, because the guard that reaches this branch only ever fires when the observed celebration already IS the day's own temporal office -- see the implementation comment on that branch. - {b Step 3} -- for a weekday whose own slug has no entry, the preceding Sunday's temporal slug (never its observed one; a Sunday is guarded out because it has no PRECEDING Sunday to resume, not because consulting itself would loop -- [readings] is not recursive, see its own implementation comment). {!Colitur_kernel.Mass_formulary.Preceding_sunday}, [said] that Sunday's temporal slug. A day matching none of the four gets [(None, [])]. *) val readings : lectionary:Lectionary.t -> commons:Commons.t -> 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) -> Mass_formulary.t option * Citation.t list