summaryrefslogtreecommitdiff
path: root/lib/kernel/rite.mli
blob: b0ba7acc28d84aef126dcefb05494a7aa482986d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
(** 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. *)
}