aboutsummaryrefslogtreecommitdiff
path: root/lib/rites/rite_ef/precedence_ef.mli
blob: b283a17aedeffe3958d6b4c19b5f938278c39890 (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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
(** RG 91's Table of Precedence for the EF (1962) rite: ranks any candidate
    for a given day by its RG 91 entry number. See
    docs/research/rules-register.md §4, whose 28-entry transcription this
    module follows line by line. *)

open Colitur_kernel

(** The universal (base) sanctoral layer's {!Celebration.t}.layer id. A
    Sanctoral-origin candidate whose layer is anything else is an overlay:
    "proper" (RG 91 entries 12, 19, 23) unless its layer id also carries
    {!indult_prefix} ("indult", entries 13, 20). This id and the prefix are
    colitur's own data-modelling convention, not an RG citation -- RG 91
    prescribes the ranking, not a machine encoding for it. Whichever task
    loads the real EF sanctoral base layer and its overlays must either
    reuse these two constants or this classifier will misfile them. *)
val universal_layer : string

(** See {!universal_layer}. *)
val indult_prefix : string

(** Slug suffix marking a celebration as a vigil (RG 91 entries 21, 26),
    e.g. "ef-ascension-vigil" -- colitur's own temporal-cycle convention
    (rite_ef/temporal_ef.ml's [named]). Also colitur's own convention, not
    an RG citation, exposed for the same reason as {!universal_layer}. See
    {!vigil_prefix} for the sanctoral data's own, different convention: a
    vigil can arrive named either way, and {!band}/{!disposition} must
    recognise both. *)
val vigil_suffix : string

(** Slug prefix marking a celebration as a vigil, e.g. "vigil-of-st-lawrence"
    -- the sanctoral data's own convention (data/ef/sanctoral.sexp, adopted
    verbatim from lectio's naming, per spec §4.4's "slugs are lectionary keys,
    not re-derived"). Also colitur's own convention, not an RG citation --
    see {!universal_layer}. Task 10 bootstrapped four real sanctoral vigils
    named this way (St Lawrence 08-09, Sts Peter & Paul 06-28, the Assumption
    08-14, the Nativity of St John the Baptist 06-23; a fifth, Christmas, is
    suppressed as a duplicate of the temporal cycle's own "ef-nativity-vigil"
    -- see data/ef/adjustments.sexp), none of which end in {!vigil_suffix}:
    without this prefix also being checked, {!band} would misfile all four at
    16/24 (an ordinary feast of the same rank) instead of RG 91's 21/26, and
    RG 33's vigil omission ({!disposition}'s [is_vigil] test, the same
    predicate) would never fire for them either -- two rubrics silently
    broken for four celebrations, exactly what Task 7's review predicted
    when it asked for {!vigil_suffix} to be exposed. *)
val vigil_prefix : string

(** Slug prefixes marking a celebration as one of RG 91 entry 18's three
    Ember-day sets (Advent, Lent, September -- the Pentecost/Whitsun set is
    I class and matched by entry 10 before this is ever consulted). Also
    colitur's own convention mirroring rite_ef/temporal_ef.ml's own "ef-<set>
    -ember-<day>" slug format, not re-derived from first principles; exposed
    so a rename of that format has somewhere to be caught other than a
    silently-wrong entry 18. *)
val ember_prefixes : string list

(** Returned for a candidate shape RG 91's 28-entry table has no row for --
    e.g. a [Class1] vigil that is not the Nativity or Pentecost (entries 5,
    9 are the only I-class vigils the table names), or a [Class4] candidate
    also marked as a vigil. Deliberately outside 1..28 and larger than any
    real entry, so an unclassified candidate can never win an occurrence
    contest by accident; a caller that sees it back knows the shape needs a
    new rule, not a silently wrong one. *)
val unclassified : int

(** [band ctx c]: RG 91's Table of Precedence. Returns the table's own entry
    number -- I class 1-13, II class 14-21, III class 22-26, IV class 27-28;
    lower wins (see {!Precedence.rules.band}) -- EXCEPT where the table's own
    text states an exception: entry 8 (All Souls) reads "yields to an
    occurring Sunday", so on a Sunday this returns a value that
    loses to entry 15 rather than the literal integer 8 (see the comment on
    entry 8 in precedence_ef.ml for the exact value and why). ALSO
    {!unclassified} for any [Cel.Commemoration_only] candidate, checked
    first, ahead of every rank-keyed branch -- CORRECTED, Task B fix round
    1 (coordinator finding 1): RG 91's own table enumerates only "dies
    liturgici" (entry 24's own text, "Festa III classis..." -- FEASTS), so
    a celebration reduced to a bare commemoration was never a row in the
    table to begin with, confirmed by the calendarium's own notation (a
    genuine feast carries a class number on its occurrence line; a bare
    "Commemoratio" carries none). Before this fix, a [Commemoration_only]
    candidate silently borrowed the SAME entry number as a genuine [Feast]
    of its own [rank] -- manufacturing ties {!Precedence.compare_by} and
    {!compare_precedence} both then had to break some other way, that RG 91
    itself never created. Total over every candidate {!Precedence.resolve}
    or {!Calendar} can construct, including shapes the 1962 table itself
    does not describe (see {!unclassified}). *)
val band : Vocab_ef.season Precedence.context -> Vocab_ef.rank Precedence.candidate -> int

(** RG 33's marker: every Sunday slug this rite's temporal cycle produces
    (temporal_ef.ml's [named] and [sunday_slug]) contains this substring;
    nothing else {!band} classifies does. Also colitur's own convention, not
    an RG citation -- see {!universal_layer} -- exposed for the same reason
    as {!vigil_suffix}: a rename of temporal_ef's Sunday-slug format has
    somewhere to be caught other than a silently-wrong RG 33 disposition. *)
val sunday_marker : string

(** [disposition ~winner ~loser]: RG 92-95, 33, 21-27, 94 (docs/research/
    rules-register.md §4, "Occurrence", "Vigils" and "Caput IV, 'De
    feriis'"). What becomes of a losing candidate, decided by the LOSER's
    own rank and status (RG 95), except RG 33's vigil omission, which also
    reads the winner:
    - a {!Celebration.status} of [Commemoration_only] is always
      [Commemorate] (checked first: it can never win -- see
      {!Precedence.resolve} -- and, by that same status's own definition,
      already denotes an office with nothing left to translate, so it never
      transfers either; not itself a further RG citation beyond RG 93's
      general four-mechanism statement above);
    - a [Class2] or [Class3] loser whose slug marks it a vigil
      ({!vigil_suffix} OR {!vigil_prefix} -- both conventions this
      codebase's data uses, see {!vigil_prefix}'s own comment) is [Omit]
      when the winner is any Sunday ({!sunday_marker}) or itself [Class1]
      (RG 33 -- entirely omitted, not merely commemorated). A [Class1]
      vigil (Nativity, Pentecost) is outside RG 33 entirely -- RG 30 makes
      it preferred to any feast whatsoever, so a real one can never reach
      this function as a [loser] in the first place (see the .ml's own
      comment on [is_omissible_vigil] for the full argument);
    - any other [Class1] loser that is NOT a Sunday ({!sunday_marker}) is
      [Transfer] (RG 95 -- only I-class FEASTS have the right of
      translation; RG 91's own table lists Sundays as a separate row, entry
      6, from feasts, entries 11-13, so a Sunday is never a "feast" in RG
      95's sense and does not transfer even when impeded by a higher
      I-class day. This is also what moves All Souls, RG 91 entry 8, once
      it loses to an occurring Sunday -- WHERE it lands is
      {!Rite.t.transfer_target}'s job, not this function's);
    - a TEMPORAL-origin, non-vigil loser of [Class4] is [Omit] (RG 26,
      "Caput IV, De feriis" -- "every feria not named in [RG 23-25] is IV
      class ... and IV-class ferias are NEVER commemorated." A SEPARATE
      rule from RG 109's honour-class one immediately below, keyed on
      ferial CLASS rather than on RG 109's privilege letters: RG 109 sorts
      commemorations that already exist into honour classes (RG 108's
      differing liturgical hours), it does not itself decide which offices
      have the right to be commemorated at all -- that is Caput IV's own
      business. A SANCTORAL loser of the same rank is unaffected by this
      branch (the [is_temporal] guard): RG 21 defines "feria" as any
      weekday, never a saint's day, and RG 111(c)/(d) admit an "ordinary"
      commemoration of a losing SAINT freely, with no such class-keyed
      gate);
    - everything else -- including an impeded I-class Sunday, and a
      SANCTORAL loser of any rank below I class -- is [Commemorate],
      carrying its real RG 109 privilege (see {!admit} below); RG 109(a)
      lists "of a Sunday" as a privileged commemoration category precisely
      because an impeded Sunday stays put rather than moving to another
      day, and RG 24/25 make a losing II- or III-class FERIA's own
      commemoration mandatory when impeded, not merely eligible.

    Total over every winner/loser pair {!Precedence.resolve} or {!Calendar}
    can construct: [Vocab_ef.rank] (RG 8) and {!Celebration.status} are both
    closed variants, and the five cases above -- an if/else-if chain ending
    in the unconditional [Commemorate] catch-all -- exhaust every value
    those two fields can take between them; there is no sixth,
    "unclassified" case the way {!band} needs one, because this function's
    own return type has no such slot to fall into by accident. *)
val disposition :
  winner:Vocab_ef.rank Precedence.candidate ->
  loser:Vocab_ef.rank Precedence.candidate ->
  Precedence.disposition

(** Slug prefix marking a celebration as one of RG 91 entry 17's days within
    the Octave of the Nativity (29-31 Dec -- 26-28 Dec are Stephen, John, the
    Innocents, sanctoral, never this prefix). Also colitur's own convention
    mirroring rite_ef/temporal_ef.ml's own "ef-nativity-octave-day-%d" slug
    format, not an RG citation -- see {!universal_layer} -- exposed for the
    same reason as {!vigil_suffix}: a rename of that format has somewhere to
    be caught other than a silently-wrong RG 109(c) privilege. *)
val nativity_octave_prefix : string

(** The September set of {!ember_prefixes}, broken out on its own because RG
    109(d) (§4, "Commemorations") privileges September Ember days under its
    own name, and September sits outside RG 109(e)'s three named seasons
    (Advent, Lent, Passiontide) under any reading of that clause -- NOT
    because the Advent and Lent Ember sets need excluding from (e), which
    they do not: (e)'s own bare text privileges them too, the same as any
    other Advent/Lent feria (see {!Precedence_ef.privilege_of}'s own (e)
    comment in the .ml for the full argument, corrected fix round 1).
    {!ember_prefixes} is built from this constant, not a duplicated literal,
    so the two cannot silently drift apart. *)
val september_ember_prefix : string

(** [admit ~observed ~temporal comms]: RG 108-111 (docs/research/rules-register.md
    §4, "Commemorations"). How many of [comms] -- each already tagged with
    its real RG 109 privilege by {!disposition}, and its own {!band} value
    (RG 91's table-of-precedence entry, computed once by
    {!Precedence.resolve} -- see {!Precedence.rules.admit}'s own doc) -- RG
    111 admits, and which, given the day actually observed:
    - [observed] a [Class1] day: none, except at most one privileged
      commemoration (the highest-{!band}-precedence one, if several are
      due) -- an ordinary one is never admitted here, no matter how many
      are due;
    - the CIVIL DAY is a [Class2] Sunday ([temporal]'s slug carries
      {!sunday_marker} -- CORRECTED, fix round 1, RG16(a) task: read off
      [temporal], not [observed]; see below): one, subject to TWO
      conditions, not one -- (i) a privileged commemoration, whenever due,
      categorically takes the day's one slot over any ordinary one, not by
      comparing table position, so an ordinary commemoration that would
      otherwise win on table order is still dropped; (ii) failing that, the
      slot is reserved for a [Class2] candidate SPECIFICALLY ("de festo II
      classis", RG 111(b)'s own wording -- a RANK FLOOR, not "whichever
      ordinary candidate has the best table position"): a III- or IV-class
      ordinary loser has no standing for this slot at all and is admitted
      nothing, even when it is the only candidate due;
    - the civil day is any other [Class2] day: one, by {!band}'s table order
      alone -- no privilege override and no rank floor, unlike the Sunday
      case immediately above;
    - [observed] a [Class3] or [Class4] day: at most two, by {!band}'s table
      order alone.

    CORRECTED, Task B (branch ef-rg16a): this used to sort by
    [Vocab_ef.rank] (RG 8's coarse four-class "dignity") and break same-rank
    ties alphabetically by slug -- an engineering convention with no
    rubrical warrant, cited nowhere. Measured, fix round 1 (2005-2050,
    reproducible via a temporary [git worktree] at the pre-fix commit):
    a same-[rank] tie existed in the candidate pool offered to [admit] on
    599 days, most never reaching a real decision (RG 111's own admission
    count often has room for both, e.g. a III-class day admits two); of
    those, reversing the slug tie-break ALONE actually changed the ADMITTED
    SET on 65 days and only the printed ORDER of an already-admitted pair
    on a further 149 (docs/research/rules-register.md §6.1's own full
    account, superseding this comment's earlier, unreproduced "66 days"
    claim). RG 113's own second sentence, previously uncited
    (docs/research/rules-register.md §4's RG 113 entry), is the real rule:
    *"in admittendis et ordinandis aliis commemorationibus, servetur ordo
    tabellae praecedentiae"* -- admitting and ordering commemorations both
    run on {!band}'s own finer RG 91 table, not RG 8's four classes.
    {!band} needs a [context] (date/season/weekday) this function does not
    receive on its own, so {!Precedence.resolve} computes it once,
    generically, and hands each candidate its own value as the trailing
    [int] on [comms]'s triples (see {!Precedence.rules.admit}). Ties break
    on slug ONLY within one identical [band] value -- CORRECTED, fix round
    1 (coordinator finding 1): [band] itself used to hand a
    [Cel.Commemoration_only] candidate the same table entry as a genuine
    [Cel.Feast] of its own rank (RG 91's table has no row for a bare
    commemoration at all), manufacturing most of the "identical [band]
    value" ties this comment used to describe as RG 113's own residual.
    Fixed in [band] itself; checked afterward (2005-2050 and the full
    1583-9999 domain, register §6.1) and found EMPTY of any remaining
    "two different real table entries tied" case -- the slug fallback below
    is exercised only between two [Commemoration_only] candidates both at
    {!unclassified}, still an uncited engineering convention (register §6
    "RG 113 tie-break"), matching {!Precedence.compare_by}'s own fallback,
    so the result never depends on the order [comms] arrives in.

    [temporal] -- CORRECTED, fix round 1 (RG16(a) task): this function used
    to read the Sunday/non-Sunday split off [observed]'s own slug, which
    silently assumed [observed] IS the day's temporal-cycle office. RG 16(a)
    breaks that assumption on purpose: a Feast of the Lord holds an impeded
    II-class Sunday's own place "cum omnibus iuribus et privilegiis"
    (RG 16(a)'s own text), so the day still counts as a "dominica II
    classis" for RG 111(b) even though [observed] is now the FEAST.
    [temporal] -- {!Precedence.rules.admit}'s own parameter, the day's
    temporal-cycle candidate regardless of who won -- is the reliable
    signal; oracle-confirmed (6 August: Sunday excludes a Class3 saint
    Sixtus II from the freed slot, an ordinary weekday admits him). Every
    row of the previous behaviour where [observed] and [temporal] coincide
    (no RG 16(a) displacement occurred) is unaffected.

    Every candidate this returns is a value taken unchanged from [comms],
    never rebuilt: {!Precedence.resolve}'s own [dropped]/[omitted]
    accounting tells an admitted candidate from a dropped one by physical
    equality on the candidate value, so anything this function admitted
    stays admitted, and anything it did not is reported in
    {!Precedence.resolution.omitted}, never silently lost. Total: every
    [Vocab_ef.rank] is one of the four cases above, and every branch is
    itself total over an empty or arbitrarily long [comms]. *)
val admit :
  observed:Vocab_ef.rank Precedence.candidate ->
  temporal:Vocab_ef.rank Precedence.candidate ->
  (Vocab_ef.rank Precedence.candidate * Precedence.privilege * int) list ->
  (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list

(** The Annunciation's own bootstrapped slug (data/ef/sanctoral.sexp, Task
    10), reused verbatim by {!transfer_target} to recognise RG 96's named
    exception. Not an RG citation -- see {!universal_layer} -- exposed so a
    future re-bootstrap that renames the slug has somewhere to be caught
    other than a silently-wrong transfer target. *)
val annunciation_slug : string

(** [transfer_target c origin occupant]: RG 96 (docs/research/rules-register
    .md §4, "Transfer/translation") -- where an impeded I-class feast, once
    {!disposition} has decided it is [Transfer]-disposed, is placed. This
    *is* {!Colitur_kernel.Rite.t}.transfer_target; see that field's own
    fuller rationale for why the search has to be rite-supplied at all.

    RG 96's own rule: the next following day whose currently-resolved
    occupant is not I or II class (read off [Vocab_ef.rank], RG 8's dignity
    -- not {!band}'s finer occurrence-table entry, the same distinction
    {!admit} draws for RG 111). This general target is computed for EVERY
    candidate, always, first.

    RG 96's own named exception (Attamen (a), primary-source-verified --
    see {!annunciation_slug}'s comment for the Latin and the register's own
    correction note): for the Annunciation specifically, IF that general
    target would fall after Easter Sunday itself ("quando est transferendum
    post Pascha" -- when it is to be transferred past Easter), the
    Annunciation is placed instead at the Monday after Low Sunday (its
    [sedes propria]), searching onward from there only if that day is
    itself occupied by a blocking class. The exception is CONDITIONAL, not
    unconditional: an Annunciation impeded for a reason that resolves
    BEFORE Easter (e.g. an ordinary Lent Sunday with a free feria the next
    day) takes the general target like any other I-class feast. Operationally
    the condition holds exactly when 25 March falls close enough to Easter
    that the general walk crosses it -- concretely, when 25 March itself
    falls within Holy Week or Easter Week.

    Total, terminating, and its result is always strictly later than
    [origin] -- {!Colitur_kernel.Rite.t}.transfer_target's own obligations,
    which {!Colitur_kernel.Calendar}'s placement pass relies on and its own
    round guard does not itself enforce (calendar.ml's [place_transfers]
    bounds ROUNDS across a whole year, not one call's internal walk).
    Terminating by a structural bound on the internal walk (max 400 days,
    an engineering ceiling, not an RG citation -- see the .ml) AND a guard
    at {!Colitur_kernel.Date}'s own domain ceiling (31 December 9999,
    beyond which probing [occupant] can itself raise -- see the .ml's
    [domain_max_date]), not by an argument about the real 1962 calendar's
    own structure, so a rite/data shape this function has not anticipated
    fails FINITELY rather than hanging or crashing the caller. Strictly
    later than [origin]: the general search starts at [origin + 1] and only
    ever advances forward from there; the Annunciation's own alternate
    starting point is provably later than 25 March for every representable
    year (Easter's documented range, register §0) -- see the .ml for the
    full argument. *)
val transfer_target :
  Vocab_ef.rank Precedence.candidate -> Date.t -> (Date.t -> Vocab_ef.rank Celebration.t) -> Date.t