(** Everything a rite supplies, bundled. Passing these as loose arguments let a caller pair one rite's vocab with another's temporal; bundling makes that unrepresentable through the normal path. Carries functions, so it has no sexp form. *) type ('s, 'r) t = { id : string; vocab : ('s, 'r) Vocab.t; year_start : int -> Date.t; (** first day of the liturgical year opening in civil year y *) temporal : Date.t -> ('s, 'r) Temporal.t; anchors : int -> (string * Date.t) list; (** Easter-derived days: (expected slug, date) *) easter : int -> Date.t; (** The rite's own Easter for civil year [y]. Supplied by the rite, NOT computed in the kernel: {!Computus} ships both Gregorian and Julian reckonings, and choosing one here would hard-code a Roman assumption into rite-agnostic code and be silently wrong for a Julian-reckoning rite. Read by {!Layer.index} to resolve {!Date_spec.Easter_offset}. *) fixed_key : Date.t -> (int * int) option; (** The (month, day) under which a FIXED sanctoral entry ({!Layer.on_date}) is looked up for this civil date. [Some (month, day)] identity for every rite with no reason to differ -- a Byzantine or other non-Roman rite supplies nothing beyond that, and its output is therefore byte-identical to a rite that predates this field entirely. [None] means no fixed entry can ever be found for this date, regardless of what {!Layer.index} holds. Exists for the Roman calendarium's own bissextile (leap-year) footnote (February, docs/research/LT.txt:5011-5014): the intercalary day is inserted by DOUBLING the sixth kalends of March (civil 24 February in a common year), not by appending a 29th day at the month's end, so every fixed feast dated at or after that kalends position is kept one civil day later than usual, and 24 February itself carries no fixed entry that year. This is a fact about the ROMAN rite's own kalends reckoning, not a universal computus rule -- a rite with no such convention (or none at all, the default above) must not have it hardcoded into rite-agnostic code, the same reason {!easter} above is rite-supplied rather than chosen here. See rite_ef/temporal_ef.ml's [bissextile_fixed_key] for the concrete Roman implementation and its full citation. Deliberately untouched: the MOVABLE half of {!Layer.on_date}'s lookup ({!Date_spec.Easter_offset}, {!Date_spec.Nth_weekday}) -- this field's contract is fixed-date reckoning only, and nothing in the calendarium footnote concerns Easter-relative dates. *) rules : ('s, 'r) Precedence.rules; season_runs : 's list; (** the expected run-length-compressed season sequence over one liturgical year. NOT necessarily [vocab.seasons]: a rite may have one season appear in two separate runs (the modern form's Ordinary Time does). *) transfer_target : 'r Precedence.candidate -> Date.t -> (Date.t -> 'r Celebration.t) -> Date.t; (** RG 96: where an impeded I-class feast goes. Given the deferred candidate, the date it was impeded on, and [occupant] -- a callback exposing what {!Calendar} currently resolves as observed on any given date -- returns the date to place it on. Deliberately one rite-supplied function, not a generic search Calendar drives itself: "not I or II class" is not derivable from [band] or [disposition] alone. RG 91's own table would let a universal I-class feast (entry 11) numerically outrank an ordinary Sunday (entry 15, II class) in a raw occurrence contest -- entry 11 comes before entry 15, and lower wins -- so testing "would the translated feast win here" is not the same question as "is this day free to receive a translation": RG 96 forbids landing on the Sunday regardless of which one would structurally win. Only the rite knows which of its own ranks are exempt from translation onto them. The rite also owns the search's starting point, because RG 96's exception is rite-specific too: the Annunciation does not search forward from its own impeded date at all, it goes straight to the Monday after Low Sunday (searching onward from there only if that day is itself blocked). [occupant] is supplied rather than a raw layer/temporal pair so the rite never has to re-implement occurrence resolution just to answer "what sits here". OBLIGATIONS (not enforced by the type, and {!Calendar}'s own termination argument depends on both): the result must {b differ from} the [Date.t] argument (the date the candidate was impeded on) -- {!Calendar}'s placement pass treats [target = origin] as a legitimate placement, not an error, and {!Precedence.resolve} itself never deduplicates by slug, so a rite that can stand still would hand it the SAME candidate twice at the SAME date in the SAME contest (once via {!Layer.on_date}'s permanent natural entry, once via the injection this module's placement pass adds) -- a genuine self-collision (the candidate disposed of as both [winner] and [loser] against itself), not merely a docstring worry: reachable the moment any rite returns its argument unchanged. The call must also {b terminate} on its own: {!Calendar}'s round guard (calendar.ml's [max_transfer_rounds]) bounds how many ROUNDS the whole-year placement pass takes, which is a distinct, outer thing from whatever internal search a single call to this function runs -- an implementation that walks day by day (in EITHER direction) looking for an admissible date, without its own bound, can hang the caller outright on a rite/data shape it does not handle, never reaching the round guard at all. See rite_ef/precedence_ef.ml's [transfer_target] for a concrete termination argument (a structural step bound, not an appeal to the real calendar's own structure). {b EARLIER targets are permitted} (CORRECTED, [of-normae-56f]/W1, 2026-08-26: this obligation previously read "strictly later", full stop, and warned that an earlier target "would silently loop candidates in place or resurrect an already-superseded occupant rather than failing loudly" -- traced against the placement pass actually implemented, not merely against this comment's own prior claim, and found to overstate the risk). {!Calendar}'s round loop re-resolves the {b entire} year fresh from the currently-settled [assignment] every round (never patched incrementally), and its two safety nets -- [~start]/[~stop] catching a target outside the liturgical year, and [max_transfer_rounds] catching non-convergence -- both test the target's value directly ([target < start || target > stop]; round count) with no comparison against [origin] anywhere, so neither depends on which side of [origin] the target falls. "Resurrecting a superseded occupant" cannot occur either: a day's occupant is decided fresh each round by the same RG 91/Tabula contest every other day is, so a backward-placed candidate either wins that contest or is itself deferred and re-targeted like any other loser -- there is no stale state to resurrect FROM. A candidate's own permanent loss at its structural origin is rediscovered every round regardless of target direction and is filtered as stale under the identical rule either way (actionable only if never yet assigned, or if the sighting is at the candidate's OWN current target -- calendar.ml's [place_transfers], the [deferred] filter's own comment). This is a general property of the mechanism, not a special case carved out for one rite: it holds for ANY single call that returns a date [<> origin] and terminates on its own, per the two obligations above. What it does {b not} license is assuming an UNBOUNDED backward search is safe merely because a bounded one is -- the termination obligation still falls entirely on the rite, exactly as it always did for a forward search. The safest shape, and the only one exercised so far, is a CONSTANT one-shot displacement for a specifically-named collision (no internal search at all, hence trivially terminating) -- see rite_of/precedence_of.ml's [transfer_target] (Normae n. 56(f), St Joseph anticipated backward onto Palm Sunday) for the concrete worked example, the same role rite_ef/precedence_ef.ml's own [transfer_target] plays for a bounded FORWARD search above. *) readings : observed:'r Celebration.t -> temporal:('s, 'r) Temporal.t -> date:Date.t -> temporal_at:(Date.t -> ('s, 'r) Temporal.t) -> Mass_formulary.t option * Citation.t list; (** The Mass actually said -- which formulary, and how that was decided -- paired with its reading citations (EF: Epistle and Gospel; OF: First, and on Sundays/solemnities also Second, and Gospel -- see {!citation_shapes} for the rite-supplied shape [Validate] holds this list to). 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. The [Mass_formulary.t option] is [None] exactly when the rite's lectionary is not built at all (the citation list is then also []): a rite that HAS a lectionary is expected to resolve [Some] on every day it covers, the same total-coverage discipline {!Validate}'s own ["formulary"] check holds it to. [None] is never a per-day "no Mass today" answer for a rite that otherwise resolves readings -- that shape is coverage FAILURE, not a legitimate outcome, which is exactly what makes the [Validate] check meaningful. [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. *) citation_shapes : Citation.part list list; (** Every well-formed shape a resolved day's [citations] may take, each already sorted in {!Citation.part}'s own declaration order (the order [List.sort compare] gives it, and the order {!Validate}'s own ["citations"] check sorts a day's actual parts into before comparing). A day passes that check when its sorted parts are EMPTY (nothing resolved -- the separate ["citations-unresolved"] coverage check) or equal one of these shapes EXACTLY -- no more parts, no fewer, none repeated. Generalises what used to be a single kernel-hardcoded [[First; Gospel]], the fourth EF-shaped kernel misfit (after {!Preface.t}, {!Mass_formulary.source}, [transfer_target]'s strictly-later contract) and the first fixed rather than only documented: the OF's Mass has TWO legitimate sizes, not one -- [[First; Gospel]] on a feria, feast or memorial, and [[First; Second; Gospel]] on a Sunday or solemnity (OLM 1981 Praenotanda n. 66.1 vs n. 69.1, page-image verified, docs/research/of/olm-1981.pdf pp.32-33/"XXXII-XXXIII": "Quaelibet Missa tres exhibet lectiones" against "Quaelibet Missa duas exhibet lectiones"; n. 84(b)/(c), pp.37-38/"XXXVII-XXXVIII", extends the three-reading shape to every solemnity of a particular calendar and confines feasts/memorials to two). EF still supplies exactly the one shape it always had -- see rite_ef.ml's own [citation_shapes] -- so EF's resolved output is unaffected letter for letter. *) creed : temporal:('s, 'r) Temporal.t -> observed:'r Celebration.t -> date:Date.t -> bool; (** Whether the Creed is said, post-Gospel/homily, at this day's Mass (EF: RG 475-476). A [bool], not an [option]: this is a decision, and a rite that has not implemented the rule returns [false] explicitly rather than leaving the question unanswered. [temporal] and [observed] are supplied for the same reason [readings] gets both: a rubric like this one can turn on either the day's TEMPORAL-cycle identity (e.g. "is this a Sunday, even one a feast has displaced") or on the celebration actually observed, and only the rite knows which. [date] is supplied for the same reason [readings] gets it too -- a rubric keyed to an Easter-relative window (e.g. "within the octave of Easter") needs the civil date and the rite's own Easter to test it, and neither [temporal] nor [observed] alone carries that arithmetic. *) gloria : temporal:('s, 'r) Temporal.t -> observed:'r Celebration.t -> date:Date.t -> bool; (** Whether the Gloria in excelsis is said at this day's Mass (EF: RG 431-432). Same seam as {!creed} in every respect: same three parameters and the same reasons for each, a [bool] not an [option], and [false] is the answer a rite that has not implemented the rule returns explicitly. *) preface : temporal:('s, 'r) Temporal.t -> observed:'r Celebration.t -> date:Date.t -> Preface.t option; (** Which preface is said at this day's Mass (EF: RG 482-499). Same seam and same three parameters as {!creed}/{!gloria}, for the same reasons -- {!Preface}'s own citation has the rubric in full. [Preface.t option], not a bare [Preface.t]: unlike [creed]/ [gloria], where "not said" is itself a legitimate answer a [bool] can carry, a preface is said only at a MASS, and this engine constructs at least one day (Good Friday, the 1955-restored Holy Week) that resolves an [observed] celebration but has no Mass at all -- [None] is the honest answer there, not a manufactured preface. [None] is also the value a rite that has not implemented this rule returns, the same "the type's own neutral value" contract {!creed}/{!gloria} give for [false] -- the two meanings ("this rite does not model the question" and "this specific day has no Mass to preface") collapse onto the same representation deliberately: a caller with no rite-specific context to distinguish them should not need one. *) }