(* The Tabula dierum liturgicorum secundum ordinem praecedentiae disposita (Missale Romanum, editio typica tertia 2002; "Normae universales de anno liturgico et de calendario", printed immediately before it) -- 13 numbered entries in three parts. This module transcribes it, one [band] branch per entry, the same discipline Rite_ef.Precedence_ef applies to RG 91's 28. CITATION DISCIPLINE: the 2002 Missal PDF carries BOTH the Normae universales AND the Institutio Generalis Missalis Romani, each independently numbered from 1. Every citation below therefore names its document: "Normae n. N" or "IGMR n. N". A bare "n. N" is ambiguous and must not appear. ENTRY VALUES ARE THE TABLE'S OWN NUMBER TIMES TEN (entry 1 -> 10, entry 13 -> 130). Identical to Precedence_ef's own convention and adopted for the identical reason: a future sub-rank (a movable/fixed split of one row, the shape RG 91 entry 14 forced on the EF side) becomes an ordinary position BETWEEN neighbours, with no rescale of every other branch. Do NOT express a sub-rank as a negative sentinel: Precedence_ef's own history records that as a defect, because a negative value avoids collision but inverts ORDERING. *) open Colitur_kernel let unclassified = max_int (* Not a Normae citation. The Tabula distinguishes universal-calendar entries (3, 7, 10) from proper-calendar ones (4, 8, 11), and nothing in Celebration.t marks that distinction except [layer]. Same mechanism, and same naming, as Precedence_ef.{universal_layer,indult_prefix}. [universal_layer] itself is exported (hand-authored sanctoral data is tagged with it) but [band] no longer tests [layer = universal_layer] directly anywhere in this chain: every temporal-origin candidate {!Rite_of.Temporal_of.temporal} constructs carries [layer = "temporal"] unconditionally (temporal_of.ml, [build]'s own hardcoded ~layer), never [universal_layer] -- so a positive "is this the universal layer" test would silently exclude Trinity Sunday, Corpus Christi, Christ the King, Mary Mother of God, Holy Family and the Baptism of the Lord, none of which are proper-calendar in the sense the Tabula's own 4/8/11 mean. Entries 3, 5 and 7 (all universal-or-temporal, never proper) therefore test [not (is_proper layer)] instead -- true for both [universal_layer] and ["temporal"], false only for a genuine ["proper:..."] candidate. Only entries 4 and 8 test [is_proper] itself. *) let universal_layer = "of-universal" let proper_prefix = "proper:" let is_proper layer = String.starts_with ~prefix:proper_prefix layer (* Days measured from Easter, so the Triduum and the Easter Octave are identified STRUCTURALLY rather than by slug. Temporal_of emits Good Friday and Holy Saturday as ordinary Lent week-6 ferias ("of-lent-6-friday"/"of-lent-6-saturday"), so there is no slug to key on -- the same situation Precedence_ef.band faces for RG 91 entry 2, resolved the same way. *) let easter_offset (ctx : Vocab_of.season Precedence.context) = let y = Date.year ctx.date in Date.to_rata ctx.date - Date.to_rata (Computus.gregorian_easter y) let slug_of (c : Vocab_of.rank Precedence.candidate) = Slug.to_string c.cel.slug (* Tabula I.1: "Triduum paschale Passionis et Resurrectionis Domini." HOLY THURSDAY IS DELIBERATELY NOT HERE. The Tabula's own entry 2 reads "Feriae Hebdomadae sanctae, a feria II ad feriam V inclusive" -- Monday through THURSDAY inclusive -- and the Paschal Triduum begins with the evening Mass of the Lord's Supper, an event INSIDE Holy Thursday's civil day. At colitur's civil-day granularity (one office per date, never a sub-day boundary) the day itself therefore stays with the Holy Week ferias at entry 2. This is the SAME granularity decision, on the precedence axis, that data/of/expected-divergences-litcal.sexp's own L1 records on the season axis (Normae nn. 28/30) -- one decision with two visible consequences, not two independent judgements. *) let is_triduum ctx = List.mem (easter_offset ctx) [ -2; -1; 0 ] (* Tabula I.2, five clauses in one entry. *) let entry_2_named = [ "of-nativity"; "of-epiphany"; "of-ascension"; "of-pentecost"; "of-ash-wednesday" ] let is_privileged_sunday (ctx : Vocab_of.season Precedence.context) c = ctx.weekday = Date.Sun && (match ctx.season with | Vocab_of.Advent | Vocab_of.Lent | Vocab_of.Easter -> true | Vocab_of.Christmas | Vocab_of.Ordinary_time -> false) && c.Precedence.origin = Precedence.Temporal (* "Feriae Hebdomadae sanctae, a feria II ad feriam V inclusive": Easter-6 (Monday) through Easter-3 (Thursday). Palm Sunday (Easter-7) is a Sunday of Lent, covered by [is_privileged_sunday] in the same entry. *) let is_holy_week_feria ctx = List.mem (easter_offset ctx) [ -6; -5; -4; -3 ] (* "Dies infra octavam Paschae": Easter+1 .. Easter+7. Easter Sunday itself is entry 1. *) let is_easter_octave_day ctx = let o = easter_offset ctx in o >= 1 && o <= 7 (* Tabula II.6: "Dominicae temporis Nativitatis et dominicae 'per annum'". The privileged Sundays (Advent, Lent, Easter) are entry 2 and are tested first, so this branch sees only the remaining two seasons. *) let is_ordinary_sunday (ctx : Vocab_of.season Precedence.context) c = ctx.weekday = Date.Sun && (match ctx.season with | Vocab_of.Christmas | Vocab_of.Ordinary_time -> true | Vocab_of.Advent | Vocab_of.Lent | Vocab_of.Easter -> false) && c.Precedence.origin = Precedence.Temporal (* Tabula II.9, three clauses. "Feriae Adventus a die 17 ad 24 decembris inclusive" -- the late-Advent ferias, which outrank an obligatory memorial where an ordinary Advent feria (entry 13) does not. *) let is_late_advent_feria (ctx : Vocab_of.season Precedence.context) = ctx.season = Vocab_of.Advent && Date.month ctx.date = 12 && Date.day ctx.date >= 17 && Date.day ctx.date <= 24 (* "Dies infra octavam Nativitatis": 26-31 December. The Nativity itself (25 December) is entry 2; 1 January is the Octave Day, a solemnity in the sanctoral, not a feria. *) let is_nativity_octave_day (ctx : Vocab_of.season Precedence.context) = Date.month ctx.date = 12 && Date.day ctx.date >= 26 && Date.day ctx.date <= 31 (* "Feriae Quadragesimae". Ash Wednesday and the Holy Week ferias are entry 2 and are tested first. *) let is_lenten_feria (ctx : Vocab_of.season Precedence.context) = ctx.season = Vocab_of.Lent let band (ctx : Vocab_of.season Precedence.context) (c : Vocab_of.rank Precedence.candidate) : int = let temporal = c.Precedence.origin = Precedence.Temporal in let layer = c.cel.layer in if temporal && is_triduum ctx then 10 (* Tabula I.1 *) else if temporal && (List.mem (slug_of c) entry_2_named || is_privileged_sunday ctx c || is_holy_week_feria ctx || is_easter_octave_day ctx) then 20 (* Tabula I.2 *) else if c.cel.rank = Vocab_of.Sollemnitas && not (is_proper layer) then 30 (* Tabula I.3 -- universal or temporal-origin, never proper; see the [is_proper] comment above for why this is not [is_universal] *) else if c.cel.rank = Vocab_of.Sollemnitas && is_proper layer then 40 (* Tabula I.4 *) else if c.cel.rank = Vocab_of.Festum && c.cel.subject = Subject.Lord && not (is_proper layer) then 50 (* Tabula II.5 *) else if is_ordinary_sunday ctx c then 60 (* Tabula II.6 *) else if c.cel.rank = Vocab_of.Festum && not (is_proper layer) then 70 (* Tabula II.7 -- BVM and saints; entry 5 already took subject = Lord *) else if c.cel.rank = Vocab_of.Festum && is_proper layer then 80 (* Tabula II.8 *) else if temporal && (is_late_advent_feria ctx || is_nativity_octave_day ctx || is_lenten_feria ctx) then 90 (* Tabula II.9 *) else if c.cel.rank = Vocab_of.Memoria_obligatoria && not (is_proper layer) then 100 (* Tabula III.10 -- universal or temporal-origin, never proper; see the [is_proper] comment above for why this is not [is_universal] *) else if c.cel.rank = Vocab_of.Memoria_obligatoria && is_proper layer then 110 (* Tabula III.11 *) else if c.cel.rank = Vocab_of.Memoria_ad_libitum then (* Tabula III.12. ONE undivided row: unlike entries 10/11 (and 3/4, 7/8), the Tabula draws no universal/proper split for optional memorials, so [layer] is deliberately not read here. The asymmetry is the source's. *) 120 else if temporal && c.cel.rank = Vocab_of.Feria then (* Tabula III.13, four clauses -- ordinary Advent ferias to 16 December, Christmas-time ferias from 2 January, paschal ferias after the Octave, and ferias "per annum". Every privileged feria (entries 2 and 9) is tested earlier in this chain, so what reaches here is exactly the residue those four clauses name, and no clause needs its own date arithmetic. *) 130 else unclassified (* Normae n. 60: "Attamen sollemnitas, quae impeditur a die liturgico, qui praecedentia gaudeat, ad proximiorem diem transferatur... Reliquae celebrationes eo anno omittuntur." An impeded SOLEMNITY is transferred; every other losing celebration is simply omitted for that year. [Commemorate] IS NEVER RETURNED, AND THAT IS THE RULE, NOT A GAP. The OF has no commemoration of a losing celebration in the EF's sense at all -- contrast RG 108-111, which the EF module implements at length. An optional memorial (Tabula III.12) is an ELECTION the celebrant may make, not a commemoration attached to another day's office, and must not be modelled as one: doing so would put a celebration in Liturgical_day.commemorations that no rubric ever puts there. Design 2026-08-24 ยง3 ("What will not survive contact") predicted exactly this; this function is where the prediction is kept. [Repose] is likewise never returned. It is EF vocabulary that the EF module itself emits from nothing. [winner] is deliberately unread. n. 60 keys the loser's fate on the LOSER's own grade alone -- unlike the EF's RG 92-95, where the winner's class decides whether a commemoration survives. Kept in the signature because Precedence.rules.disposition requires it, and a future proper-calendar rule might read it. *) let disposition ~(winner : Vocab_of.rank Precedence.candidate) ~(loser : Vocab_of.rank Precedence.candidate) : Precedence.disposition = ignore winner; match loser.cel.rank with | Vocab_of.Sollemnitas -> Precedence.Transfer | Vocab_of.Festum | Vocab_of.Memoria_obligatoria | Vocab_of.Memoria_ad_libitum | Vocab_of.Feria -> Precedence.Omit (* Normae n. 60's "Reliquae celebrationes eo anno omittuntur", read together with the whole Tabula: nothing in the OF is admitted as a commemoration of another day's office, so this returns the empty list unconditionally. NOT A STUB. This is [disposition]'s own rule expressed at the second of the two places the kernel asks about it: [disposition] never returns [Commemorate], so Precedence.resolve never offers this function a candidate it could admit even if it wanted to. Returning [] is therefore the only answer consistent with the rubric, and every losing candidate is reported in [resolution.omitted] with its reason -- nothing is dropped silently. All three parameters are unread, for that reason. The EF's own admit reads all of them (RG 108-113); the OF's has nothing to decide. *) let admit ~(observed : Vocab_of.rank Precedence.candidate) ~(temporal : Vocab_of.rank Precedence.candidate) (_ : (Vocab_of.rank Precedence.candidate * Precedence.privilege * int) list) : (Vocab_of.rank Precedence.candidate * Precedence.privilege) list = ignore observed; ignore temporal; [] (* RG 33's third omission trigger is EF-only. The OF has no vigil OFFICE that can be orphaned by its feast moving: Normae n. 11 gives solemnities First Vespers and n. 13 denies them to feasts, but neither creates a separate celebration that could survive or fail independently of the day it precedes. This is why the hook exists at all and why it returns [None] for every OTHER candidate -- there is no OF office this rule could ever apply to. "of-nativity-vigil" itself needs a narrower, structural argument, not that general one: Temporal_of.named (temporal_of.ml:230) DOES construct it as its own Sollemnitas-rank candidate on 24 December, independent of "of-nativity" on the 25th -- it is a genuine contestant in [band]/[resolve], not merely a Mass formulary with no life of its own on the precedence axis. What makes [None] safe here is that the feast it precedes can never be orphaned. The Nativity is Tabula entry 2 (band 20); the only entry that bands lower is entry 1, the Paschal Triduum (band 10, [is_triduum] above). Gregorian Easter falls in [22 March, 25 April] for EVERY year 1583-9999 -- measured exhaustively, not assumed (test_computus.ml's [test_gregorian_invariants_exhaustive]) -- so the Triduum's own band-10 window (Easter-2 .. Easter) is confined to 20 March-25 April and can never reach 25 December. Nothing else in the Tabula bands below entry 2. So "of-nativity" is unconditionally the observed office on its own day, its vigil can never be orphaned by a transfer or reduction, and [None] is the correct answer for it too -- not because it lacks standing as a candidate, but because the day it precedes can never lose. *) let vigil_feast (_ : Vocab_of.rank Precedence.candidate) : Slug.t option = None (* The assembled record Precedence.resolve consumes. Kept here rather than in rite_of.ml so a reader sees the four functions and their citations together. *) let rules : (Vocab_of.season, Vocab_of.rank) Precedence.rules = { band; disposition; admit; vigil_feast }