(* EF (1962) temporal cycle. Every boundary and rank rule cites its Rubricae Generales paragraph; see docs/research/rules-register.md §3-§4. *) open Colitur_kernel open Vocab_ef let mk y m d = match Date.make ~year:y ~month:m ~day:d with | Ok t -> t | Error e -> failwith ("temporal_ef: " ^ e) let weekday_index d = match Date.weekday d with | Date.Sun -> 0 | Date.Mon -> 1 | Date.Tue -> 2 | Date.Wed -> 3 | Date.Thu -> 4 | Date.Fri -> 5 | Date.Sat -> 6 (* The Sunday on or before [d]. *) let sunday_on_or_before d = Date.add_days d (-(weekday_index d)) (* RG 20 (Caput III, "De Dominicis"), primary-source-verified (final fix wave): "Dominica I Adventus ea est, quae cadit die 30 novembris vel est ipsi proximior" -- Advent I Sunday is that which falls on 30 November or is nearest to it. CORRECTED citation: this comment previously cited RG 71 for this placement rule -- WRONG, RG 71 (cited on [season] below) states only Advent's own season BOUNDARY ("a I Vesperis dominicae I Adventus..."), not which Sunday opens it; the register's own RG 71 entry is a boundary citation, and the only "nearest 30 November" text there before this fix was UNLYC nn. 39-42, the MODERN form's rule, not this one's. Equivalently the fourth Sunday before Christmas, i.e. three weeks before the last Sunday on or before 24 December. *) let advent_start y = Date.add_days (sunday_on_or_before (mk y 12 24)) (-21) let year_start = advent_start let before a b = Date.compare a b < 0 let on_or_after a b = Date.compare a b >= 0 (* RG 71-77. Tested in chronological order within the civil year. *) let season d = let y = Date.year d in let easter = Computus.gregorian_easter y in let advent_this = advent_start y in let christmas_this = mk y 12 25 in let jan14 = mk y 1 14 in let septuagesima_sunday = Date.add_days easter (-63) in let ash_wednesday = Date.add_days easter (-46) in let passion_sunday = Date.add_days easter (-14) in let paschal_end = Date.add_days easter 55 in if on_or_after d advent_this && before d christmas_this then Advent (* RG 71 *) else if on_or_after d christmas_this then Christmastide (* RG 72-73: 25-31 Dec *) else if before d jan14 then Christmastide (* RG 72-73: 1-13 Jan inclusive *) else if before d septuagesima_sunday then Time_after_epiphany (* RG 77: from 14 Jan *) else if before d ash_wednesday then Septuagesima (* RG 73 *) else if before d passion_sunday then Lent (* RG 74 *) else if before d easter then Passiontide (* RG 75; Holy Saturday included *) else if Date.compare d paschal_end <= 0 then Paschaltide (* RG 76 *) else Time_after_pentecost (* RG 77 *) (* RG 17(d) (Caput III, "De Dominicis"), PRIMARY-SOURCE-VERIFIED (final fix wave, closing the item register §6 previously carried as "oracle-backed, not yet primary-verified"): "festum D. N. Iesu Christi Regis, celebrandum dominica ultima mensis octobris" -- the feast of Our Lord Jesus Christ the King is to be celebrated on the LAST SUNDAY OF OCTOBER. NOT the OF's last Sunday before Advent -- a genuine EF/OF divergence, not merely a citation gap. *) let christ_the_king y = sunday_on_or_before (mk y 10 31) (* RG 17(b) (Caput III, "De Dominicis"), primary text, verified against BOTH photographic scans (missale-romanum-1962.pdf and "Missale Romanum 1962_text.pdf" -- the electronic transcription, 1962-06-23,_SS_Ioannes _XXIII,_Missale_Romanum,_LT.pdf, carries the SAME text here, so this one paragraph is not itself a transcription-vs-scan divergence): "17. Dominica excludit, per se, assignationem perpetuam festorum. Excipiuntur: a) festum Ssmi Nominis Iesu, celebrandum dominica quae occurrit a die 2 ad 5 ianuarii (secus die 2 ianuarii); b) festum S. Familiae Iesu, Mariae, Ioseph, celebrandum dominica prima post Epiphaniam; ... Haec festa locum tenent dominicae occurrentis cum omnibus iuribus et privilegiis: de dominica, proinde, nulla fit commemoratio" -- a Sunday of itself excludes the perpetual assignment of feasts to it; EXCEPTED: (a) the Most Holy Name of Jesus, celebrated on the Sunday falling 2-5 January (otherwise 2 January); (b) the HOLY FAMILY of Jesus, Mary and Joseph, celebrated on the FIRST SUNDAY AFTER EPIPHANY; ... these feasts hold the place of the occurring Sunday with ALL its rights and privileges: of the Sunday, therefore, NO commemoration is made -- the identical "cum omnibus iuribus et privilegiis: nulla fit commemoratio" formula RG 16(a) already uses for a FIXED-date Lord feast impeding a Sunday (register §6.0), stated here for the Sunday-ASSIGNED feasts RG 17 itself lists by letter. UNLIKE (a)'s own narrow 2-5 January window (which CAN be empty of a Sunday -- the calendarium's own January table carries an explicit "vel ea deficiente, die 2 ianuarii" fallback for it, scan-verified), (b)'s 7-13 January window can NEVER be empty: [sunday_on_or_before (mk y 1 6)] is, by construction, at most 6 days before 6 January, so adding 7 always lands in [7, 13] regardless of which weekday 6 January falls on (every one of the 7 possible cases is exercised by test_temporal_ef.ml's own [test_holy_family]). The calendarium's own text for (b), both in RG 17 itself and in its January table entry ("Dominica I post Epiphaniam: S. Familiae, Iesu, Mariae, Ioseph, II classis"), carries no fallback clause of any kind -- consistent with there being no gap for one to cover. Formula IDENTICAL to [week_origin]'s own [Time_after_epiphany] case below (the first Sunday after Epiphany) -- not re-derived a second time, just named here for its own citation and so [temporal] can test a specific date against it without reaching into [week_origin]'s implementation. *) let holy_family_sunday y = Date.add_days (sunday_on_or_before (mk y 1 6)) 7 (* RG 17(a) (Caput III, "De Dominicis"; full text on [holy_family_sunday]'s own citation above): "festum Ss.mi Nominis Iesu, celebrandum dominica quae occurrit a die 2 ad 5 ianuarii (secus die 2 ianuarii)" -- the feast of the Most Holy Name of Jesus, celebrated on the Sunday falling 2-5 January, OTHERWISE [on] 2 January [itself]. [None] here means no such Sunday exists that year -- [holy_name_fallback_date]'s own fallback branch is for exactly that case; see its own citation for why the window CAN be empty (unlike (b)'s 7-13 January window, which never is). Formula: the Sunday on or before 5 January is in [2,5] iff it is not before 2 January -- the window is only 4 days wide, so there is at most one Sunday in it either way. *) let holy_name_sunday y = let s = sunday_on_or_before (mk y 1 5) in if on_or_after s (mk y 1 2) then Some s else None (* RG 17(a)'s own "secus die 2 ianuarii" clause, and the calendarium's January table, BOTH photographic scans, word for word (docs/research/ rules-register.md; also confirmed in the Mass propers' own heading for this feast, both scans, Caput "Sanctissimi Nominis Iesu": "DOMINICA a die 2 ad diem 5 ianuarii occurrenti, VEL, SI HAEC DEFECERIT, DIE 2 IANUARII" -- the SUNDAY occurring 2-5 January, OR, IF THIS IS LACKING, [on] 2 January): *"Dominica inter octavam Nativitatis Domini et Epiphaniam, vel, EA DEFICIENTE, die 2 ianuarii: Sanctissimi Nominis Iesu, II classis"* -- the Sunday between the Octave of the Nativity and Epiphany, or, THAT FAILING, 2 January. Unlike [holy_family_sunday]'s 7-13 January window (which can never be empty of a Sunday), this 2-5 January window CAN be, and is in 3,619 of the 8,417 years in [1583, 9999] (a plain weekday check, independently cross-checked against `date -d`) -- [temporal]'s own dispatch applies this fallback date, tagged exactly like the Sunday shape (subject Lord, the same Latin name), only when [holy_name_sunday y] is [None] for that date's own civil year. *) let holy_name_fallback_date y = mk y 1 2 (* The ONE Latin title both of RG 17(a)'s two shapes (the Sunday and the 2 January fallback) carry -- ONE feast, RG 91's own "primum mobilia, deinde fixa" language notwithstanding (see [temporal]'s own fallback-branch comment for why both shapes band identically at entry 14's MOVABLE half, not split across the two halves by year). Bound once so the two [temporal] branches that use it cannot silently drift apart, the same discipline [holy_family_sunday] already gets from being named instead of re-derived. Both photographic scans, word for word, twice over (the calendarium's own January table AND the Mass propers' own running header/heading, this file's own citations on [holy_name_sunday]/[holy_name_fallback_date] above): "Sanctissimi Nominis Iesu". No English name, deliberately -- the SAME "would read the oracle it is compared against" reasoning [holy_family_sunday]'s own [names] comment below gives for Holy Family. *) let holy_name_names = Colitur_kernel.Names.of_list [ (Colitur_kernel.Lang.of_string_exn "la", "Sanctissimi Nominis Iesu") ] let same a b = Date.compare a b = 0 (* Named temporal days: the I-class feasts of the Lord (RG 91 entries 1 and 3), the Vigil and Octave Day of the Nativity and the Vigil of Pentecost (RG 28-34, RG 91 entries 5 and 9), the I-class Sundays of Passiontide and Low Sunday (RG 91 entry 6), Ash Wednesday (RG 91 entry 7), and the days within the Octave of the Nativity (RG 63-70, RG 91 entry 17). Returns (season, slug, colour, rank). Deliberately NOT week: an earlier version carried an explicit week option here, hand-set on some branches (Passion/Palm Sunday, Easter, Low Sunday, Pentecost and its Vigil, Christ the King) and left at [None] on others (Ascension and its Vigil, Corpus Christi, Sacred Heart) even though those sit inside a numbered season run just the same -- a manual-convention bug the guarding property could not even detect (see test_temporal_ef.ml's history). [temporal] now calls [week] itself for every day, named or not, which makes "a named day inside a run carries that run's week" hold by construction instead of by remembering to set it here. *) let named d = let y = Date.year d in let easter = Computus.gregorian_easter y in let off n = Date.add_days easter n in let m = Date.month d and dd = Date.day d in if m = 12 && dd = 25 then Some (Christmastide, "ef-nativity", Colour.White, Class1) else if m = 12 && dd = 24 then (* RG 91 entry 5: the Vigil of the Nativity is I class. lectio has no slug for it, so this key has no lectionary entry until Plan 4 fills it (CORRECTED, final fix wave, item 7 -- this is the lectionary/reading- citations bootstrap, Plan 4, not the sanctoral one, Plan 3, which already shipped in this branch). *) Some (Advent, "ef-nativity-vigil", Colour.Violet, Class1) else if m = 12 && (dd = 29 || dd = 30 || dd = 31) then (* Days within the Octave of the Nativity; 26-28 Dec are Stephen, John and the Innocents, hence sanctoral (Plan 3). Colitur slugs -- lectionary gap. *) Some (Christmastide, Printf.sprintf "ef-nativity-octave-day-%d" (dd - 24), Colour.White, Class2) else if m = 1 && dd = 1 then (* RG 91 entry 5: 1 Jan is the Octave Day of the Nativity, the same table entry as the Nativity vigil above. *) Some (Christmastide, "ef-circumcision", Colour.White, Class1) else if m = 1 && dd = 6 then Some (Christmastide, "ef-epiphany", Colour.White, Class1) else if same d (off (-46)) then Some (Lent, "ef-ash-wednesday", Colour.Violet, Class1) (* RG 91 entry 7 *) else if same d (off (-14)) then Some (Passiontide, "ef-passion-sunday", Colour.Violet, Class1) (* RG 91 entry 6 *) else if same d (off (-7)) then Some (Passiontide, "ef-palm-sunday", Colour.Violet, Class1) (* RG 91 entry 6 *) else if same d easter then Some (Paschaltide, "ef-easter-sunday", Colour.White, Class1) else if same d (off 7) then Some (Paschaltide, "ef-low-sunday", Colour.White, Class1) (* RG 91 entry 6 *) else if same d (off 38) then (* RG 91 entry 21: II-class vigil. It is also Rogation Wednesday; [named] emits the higher-ranked vigil (entry 21 outranks any Rogation-day ferial rank). CORRECTED (final fix wave, item 7): this comment previously said the Rogation commemoration itself "waits for RG 108-111" -- the precedence framework and RG 108-111 both exist now (this branch), but no candidate for the Rogation Wednesday's own observance is constructed here or anywhere else, so there is nothing for RG 108-111 to admit; see the fuller comment on the Rogation branch further down in [temporal] for the current, still-real gap. *) Some (Paschaltide, "ef-ascension-vigil", Colour.White, Class2) else if same d (off 39) then Some (Paschaltide, "ef-ascension", Colour.White, Class1) else if same d (off 48) then Some (Paschaltide, "ef-pentecost-vigil", Colour.Red, Class1) (* RG 91 entry 9 *) else if same d (off 49) then Some (Paschaltide, "ef-pentecost", Colour.Red, Class1) else if same d (off 56) then Some (Time_after_pentecost, "ef-trinity", Colour.White, Class1) else if same d (off 60) then Some (Time_after_pentecost, "ef-corpus-christi", Colour.White, Class1) else if same d (off 68) then Some (Time_after_pentecost, "ef-sacred-heart", Colour.White, Class1) else if same d (christ_the_king y) then Some (Time_after_pentecost, "ef-christ-the-king", Colour.White, Class1) else None let days_between a b = Date.to_rata b - Date.to_rata a (* Floor division. OCaml's [/] truncates toward zero, so a date before a season's week origin would round up into week 1 instead of falling out of the numbering: Ash Wednesday is 4 days before the Lent I origin, and -4/7 = 0 would make it week 1. *) let floor_div a b = if a >= 0 then a / b else ((a + 1) / b) - 1 (* The Sunday on which week 1 of a season begins. Every origin is a Sunday, so week numbers are constant Sunday-to-Saturday. Christmastide has no numbered weeks. Time after Epiphany counts from the first Sunday after Epiphany -- which itself falls 7-13 January and is therefore inside Christmastide (RG 72-73), so the season's own days start part-way through week 1. Time after Pentecost counts from Pentecost, making Trinity Sunday the first Sunday after Pentecost. *) let week_origin s y = let easter = Computus.gregorian_easter y in match s with | Advent -> Some (advent_start y) | Christmastide -> None | Time_after_epiphany -> Some (Date.add_days (sunday_on_or_before (mk y 1 6)) 7) | Septuagesima -> Some (Date.add_days easter (-63)) | Lent -> Some (Date.add_days easter (-42)) (* Lent I Sunday *) | Passiontide -> Some (Date.add_days easter (-14)) | Paschaltide -> Some easter | Time_after_pentecost -> Some (Date.add_days easter 49) (* Pentecost *) (* [week] is total and defined for every date, named or not: a named day inside a numbered season run carries that run's week by calling this same function, not by a separately hand-set value (see [named]'s docstring above). It naturally returns [None] for Christmastide (no season-wide numbering) and for the handful of proper-Mass days between Ash Wednesday and Lent I that precede any run's origin. *) let week d = let s = season d in match week_origin s (Date.year d) with | None -> None | Some origin -> let n = floor_div (days_between origin d) 7 in let n = match s with Time_after_pentecost -> n | _ -> n + 1 in if n < 1 then None else Some n (* Christmastide has no numbered weeks ([week_origin] returns [None]), so the generic -- ferial fallback below would collapse every feria to literal week "0" -- and because colitur's Christmastide spans 25 Dec - 13 Jan (RG 72-73, the deliberate divergence from lectio), the same weekday recurs two or three times across that span, producing duplicate slugs within a single liturgical year (register finding 1). Four sub-stretches, each given a key that cannot collide with the others: - 26-28 Dec (between the Nativity and its Octave days, which are named above): lectio has no narrower key here either, so this keeps its existing "ef-christmas-0-" key unchanged -- nothing to lose by changing it, and nothing gained. - 2-5 Jan (between the Octave Day and Epiphany): lectio *also* collapses this to "ef-christmas-0-", indistinguishable from the stretch above in lectio's own data. colitur cannot preserve a distinction lectio doesn't make, so this becomes "ef-christmas-1-" -- a colitur-only key and a lectionary gap for the Plan 4 bootstrap to fill (CORRECTED, final fix wave, item 7: the lectionary bootstrap is Plan 4, not Plan 3 -- see Slug.ml's own corrected comment), exactly like the Nativity vigil and octave-day keys above. - 7-13 Jan, split in two by the *actual* first-Sunday-after-Epiphany origin ([week_origin Time_after_epiphany], which by construction always falls somewhere in this window -- see that function's own comment): - from the origin Sunday through 13 Jan: this genuinely is week 1 of Time after Epiphany, just still inside Christmastide by season (RG 72-73). lectio keys it "ef-time-after-epiphany-1-", the same key [sunday_slug] already gives the Sunday in this window -- and it is the *same computation* the ordinary Time-after-Epiphany ferial fallback below will give the rest of that same Sunday-to-Saturday week once the season turns on 14 Jan, so this cannot collide with it (a fixed 7-day week has each weekday once). - 7 Jan through the day *before* the origin (0-6 days, only present when Epiphany does not fall on a Saturday): these genuinely precede week 1 -- treating them as week 1 too, as a naive calendar-range read of lectio's behaviour would, collides with the days named just above, because they are exactly 7 days before them for whichever weekdays they cover (verified empirically: reusing "week 1" here produced duplicates in most years, not merely an edge case). No lectio key to preserve either way, so this is its own colitur-only "ef-christmas-2-" -- a further lectionary gap. *) let christmastide_feria_slug d = let y = Date.year d in let m = Date.month d and dd = Date.day d in let w = Date.weekday_to_string (Date.weekday d) in if m = 12 && dd >= 26 && dd <= 28 then Some (Printf.sprintf "ef-christmas-0-%s" w) else if m = 1 && dd >= 2 && dd <= 5 then Some (Printf.sprintf "ef-christmas-1-%s" w) else if m = 1 && dd >= 7 && dd <= 13 then match week_origin Time_after_epiphany y with | Some origin when Date.compare d origin >= 0 -> Some (Printf.sprintf "ef-time-after-epiphany-1-%s" w) | _ -> Some (Printf.sprintf "ef-christmas-2-%s" w) else None (* Sunday slugs. These are lectionary keys: they use [season_slug_word], and for Christmastide they keep lectio's keys even though colitur's season differs (spec §4.4 -- slugs are opaque keys, not truth). *) let sunday_slug d = if Date.weekday d <> Date.Sun then None else let y = Date.year d in let m = Date.month d and dd = Date.day d in let s = season d in match s with | Christmastide -> if m = 12 && dd >= 26 then Some "ef-christmas-sunday-0" else if m = 1 && dd >= 7 && dd <= 13 then (* 1st Sunday after Epiphany (Holy Family). Season is Christmastide per RG 72-73; the key stays lectio's. *) Some "ef-time-after-epiphany-sunday-1" else if m = 1 && dd >= 2 && dd <= 5 then (* Most Holy Name of Jesus. Colitur slug -- lectionary gap; confirm the placement against MR1962 while coding (register §6). *) Some "ef-holy-name-sunday" else None | Time_after_pentecost -> ( (* Reuse [week] rather than recomputing the Pentecost-relative week number locally, so the two can never drift apart (see test_week_sunday_slug_agree). Only the last Sunday and the resumed tail are genuinely special. *) match week d with | None -> None | Some n -> let last_sunday = Date.add_days (advent_start y) (-7) in if same d last_sunday then (* The last Sunday before Advent always keeps the 24th (Last) Mass. *) Some "ef-time-after-pentecost-sunday-24" else if n > 23 then (* Surplus Sundays resume the Sundays after Epiphany that Septuagesima cut short -- the highest-numbered ones, so the 6th sits just before the Last. *) let total = match week last_sunday with Some t -> t | None -> n in Some (Printf.sprintf "ef-time-after-epiphany-sunday-%d" (n - total + 7)) else Some (Printf.sprintf "ef-time-after-pentecost-sunday-%d" n)) | _ -> ( match week d with | Some n -> Some (Printf.sprintf "ef-%s-sunday-%d" (season_slug_word s) n) | None -> None) let id = "ef" (* The third Sunday of September: the Ember week's anchor. [cited] PRIMARY-SOURCE-VERIFIED (register §3a): MR1962, "De anno et eius partibus", under the heading "Quatuor Tempora" (not a numbered RG paragraph, which is why an earlier paragraph-number search missed it): "Quatuor Tempora celebrantur quarta et sexta feria ac sabbato post tertiam dominicam Adventus, post primam dominicam Quadragesimae, post dominicam Pentecostes, post dominicam tertiam septembris." -- the Ember Days are kept on the Wednesday, Friday and Saturday after Advent III, after Lent I, after Pentecost, [and] after the third Sunday of September -- confirming all four of this module's anchors, including this specific contested one. This specific date-derivation rule was one of the more contested points in the 1962 calendar: pre-1955 practice tied the September Ember days to the week following the Exaltation of the Holy Cross (14 Sept) instead. The two rules only disagree when 1 September is a Monday -- 2025 is such a year, and confirms the third-Sunday reading empirically too (24/26/27 September against an independent oracle, vs the Holy-Cross rule's 17/19/20). An earlier version of this comment said the scan contained no numbered-paragraph statement of either rule and left the citation at the rank rules only (RG 91 entries 18/22) -- WRONG, corrected once the nominative heading "Quatuor Tempora" was found rather than the genitive "Quatuor Temporum" the original search used; register §3a records the correction, because a false "not in the source" note is worse than no note. *) let third_sunday_of_september y = let sep1 = mk y 9 1 in let first_sunday = Date.add_days sep1 ((7 - weekday_index sep1) mod 7) in Date.add_days first_sunday 14 (* Ember days: Wednesday, Friday and Saturday after the anchoring Sunday. RG 91 entry 18 makes the Advent, Lent and September sets II class; entry 22 excepts the Lenten set from the III-class Lenten ferias. The Whitsun set falls inside the I-class Pentecost octave and takes its rank. The September and Advent sets match lectio's own Ember slugs. The Lent and Whitsun (Pentecost) sets do not -- lectio has no Ember slug for either, so "ef-lent-ember-*" and "ef-pentecost-ember-*" are colitur-only keys and a lectionary gap for the Plan 4 bootstrap to fill (spec §4.4; CORRECTED, final fix wave, item 7 -- Plan 4, not Plan 3, is the lectionary bootstrap), the same status as the Nativity vigil and the Rogation days below. *) let ember d = let y = Date.year d in let easter = Computus.gregorian_easter y in let sets = [ (third_sunday_of_september y, "september", Class2, Colour.Violet); (Date.add_days (advent_start y) 14, "advent", Class2, Colour.Violet); (Date.add_days easter (-42), "lent", Class2, Colour.Violet); (Date.add_days easter 49, "pentecost", Class1, Colour.Red) ] in List.find_map (fun (anchor, name, rank, colour) -> let day_of = function 3 -> Some "wed" | 5 -> Some "fri" | 6 -> Some "sat" | _ -> None in let n = days_between anchor d in if n >= 3 && n <= 6 then match day_of n with | Some w -> Some (Printf.sprintf "ef-%s-ember-%s" name w, rank, colour) | None -> None else None) sets (* RG 91 entry 7: Ash Wednesday (named above) and Monday-Wednesday of Holy Week are I-class ferias -- the primary text reads "feria IV cinerum et II, III et IV Hebdomadae sanctae", i.e. explicitly stops at Wednesday (PRIMARY-SOURCE-VERIFIED, final fix wave: confirmed word for word against the scan). Thursday to Saturday of Holy Week are the Sacred Triduum, RG 91 entry 2 -- ranked even above entry 7, not a mere feria -- but the Sacred Triduum has NO PROPER OFFICE of its own in this codebase (CORRECTED, final fix wave, item 7: this comment previously said their "own named offices are a Plan 3 sanctoral addition"; WRONG on two counts -- Plan 3 shipped, in this branch, without adding them, AND a proper office for I-class FERIAS was never a sanctoral matter to begin with, RG 21's own definition of "feria" excludes Sundays/feasts, not the other way round). `Temporal_ef.temporal 2026-04-02/03/04` (Holy Thursday/Good Friday/Holy Saturday) still resolve today to the ordinary Passiontide ferial fallback's own generic slugs, "ef-passiontide-2-{thursday,friday,saturday}" -- register §6 records this as its own open item now. This gives them the same I-class rank via the generic ferial path regardless. RG 91 entry 10: the weekdays within the privileged Octaves of Easter and Pentecost are I class too. *) let privileged_feria d = let easter = Computus.gregorian_easter (Date.year d) in let n = days_between easter d in (n >= -6 && n <= -1) || (n >= 1 && n <= 6) || (n >= 50 && n <= 55) (* RG 117 enumerates the five colours (white, red, green, violet, black); RG 127 assigns green and RG 128 violet to the seasons de Tempore below. RG 119 (register §3b, primary-source-verified 2026-08-11 -- this comment was stale until Task 16 noticed the correction had not been copied down here): white "a festo Nativitatis Domini usque ad expletum tempus Epiphaniae" and "a Missa Vigiliae paschalis usque ad Missam vigiliae Pentecostis exclusive" -- exactly Christmastide and Paschaltide below. *) let season_colour = function | Advent | Septuagesima | Lent | Passiontide -> Colour.Violet (* RG 128 *) | Christmastide | Paschaltide -> Colour.White (* RG 119 *) | Time_after_epiphany | Time_after_pentecost -> Colour.Green (* RG 127 *) (* Gaudete (Advent III) and Laetare (Lent IV) are rose: RG 131, "may be used... for the Office and Mass of that Sunday only" -- an indult over the season's violet, not a season colour of its own. *) let is_rose_sunday d s = let y = Date.year d in match s with | Advent -> same d (Date.add_days (advent_start y) 14) | Lent -> same d (Date.add_days (Computus.gregorian_easter y) (-21)) | _ -> false (* RG 91 entry 28, "Feriae IV classis", is an unqualified catch-all: any feria not placed by a more specific entry above defaults to IV class. That is what a per annum or Septuagesima feria falls back to here -- and also an ordinary Paschaltide weekday (e.g. a Rogation day) outside the privileged octave, since the table has no entry of its own for Paschaltide ferias. *) let ferial_rank d s = if privileged_feria d then Class1 else match s with | Advent -> if Date.month d = 12 && Date.day d >= 17 then Class2 (* RG 91 e18 *) else Class3 (* e25 *) | Lent | Passiontide -> Class3 (* RG 91 e22 *) | _ -> Class4 (* RG 91 e28 *) let weekday_word d = Date.weekday_to_string (Date.weekday d) let temporal d = let y = Date.year d in let easter = Computus.gregorian_easter y in let s = season d in let weekday = Date.weekday d in let build ?(subject = Colitur_kernel.Subject.Temporal) ?(names = Colitur_kernel.Names.empty) ~season ~slug ~colour ~rank ~week () = let office = Colitur_kernel.Celebration.make ~slug:(Slug.of_string_exn slug) ~rank ~colour ~subject ~names ~layer:"temporal" () in { Colitur_kernel.Temporal.season; week; weekday; office } in match named d with | Some (season, slug, colour, rank) -> build ~season ~slug ~colour ~rank ~week:(week d) () | None -> ( (* Rogations (the Minor Litanies only -- RG 87, Monday and Tuesday before Ascension). The Major Litanies (25 April, RG 80) are a fixed date and are STILL not computed (CORRECTED, final fix wave, item 7: this comment previously said "they arrive with Plan 3's sanctoral" -- Plan 3 shipped, in this branch, without them; register §6 tracks this as a plain open item, with no plan committed to build it yet). The Wednesday here is the Ascension vigil (see Task 11), which happens to also fall on Rogation Wednesday -- the vigil (higher RG 91 entry) is what [named] emits for that date; the Rogation Wednesday's own commemoration is not separately constructed (a real gap, not a forward dependency: the precedence framework and RG 108-111 both exist now, but nothing wires a Rogation-Wednesday candidate into the contest for this specific date the way Monday and Tuesday get one below). RG 88: "de Litaniis minoribus nihil fit in Officio" -- the Office (hence the day's rank) is unchanged by the Rogation; only the Mass is proper. No RG 91 table entry elevates these days, so they take the ordinary ferial rank of their season via [ferial_rank] rather than a fixed class. lectio has no Rogation slug at all, so "ef-rogation-monday"/"-tuesday" are colitur-only keys and a lectionary gap for Plan 4 (CORRECTED, final fix wave, item 7 -- the lectionary bootstrap is Plan 4, not Plan 3), like the Ember and Nativity-vigil keys above. *) let rogation = days_between easter d in if rogation = 36 || rogation = 37 then build ~season:s ~slug:(if rogation = 36 then "ef-rogation-monday" else "ef-rogation-tuesday") ~colour:Colour.Violet ~rank:(ferial_rank d s) ~week:(week d) () else match ember d with | Some (slug, rank, colour) -> build ~season:s ~slug ~colour ~rank ~week:(week d) () | None -> ( match sunday_slug d with | Some slug -> let colour = if is_rose_sunday d s then Colour.Rose else season_colour s in (* RG 11-12: Sundays of Advent, Lent, Passiontide, Easter, Low Sunday and Pentecost are I class; all others II. The I-class ones are already named above -- Passiontide has only two Sundays and both are named, so no Passiontide Sunday ever reaches this fallback -- leaving II class here except the remaining Advent and Lent Sundays. *) let rank = match s with Advent | Lent -> Class1 | _ -> Class2 in (* RG 17(b) (this file's own [holy_family_sunday], full citation there): the ONE Sunday a year this branch must NOT leave [subject = Temporal] (this function's own default) -- the Holy Family, whose slug/rank/colour are otherwise EXACTLY what this branch already computes for the 7-13 January Sunday (an accident this task's own oracle-strengthening work exposed: rank/colour alone could never distinguish "Holy Family" from "an ordinary, unnamed Sunday", since they happen to coincide). Every OTHER Sunday this branch ever builds -- including the narrower Holy Name of Jesus window, RG 17(a), still unbuilt as its own named day, register §6 -- keeps [Temporal] and no name, unaffected. [names] -- fix round 1 (coordinator finding 3): a LATIN name, not English. The oracle's own observed-identity axis (test_oracle.ml) reads only [en], so this is deliberately invisible to it -- setting an ENGLISH name here would mean reading missalemeum's own title text (the oracle this exact axis is compared against) to decide colitur's own "ground truth" name, precisely the "expected value promoted from actual output" vacuity flavour this project's own review process watches for. Latin has no such circularity: the calendarium's own January table, both photographic scans, word for word: "Dominica I post Epiphaniam: S. Familiae, Iesu, Mariae, Ioseph, II classis" -- and the Mass propers' own heading (also both scans): "SANCTAE FAMILIAE IESU, MARIAE, IOSEPH, II classis", the exact string used here, the same genitive-title convention test_names.ml's own worked example already uses for Easter ("Dominica Resurrectionis"). Every other temporal-cycle candidate still carries no name at all (register §6.2's own open item on Holy Name of Jesus, RG 17(a)) -- this is a targeted addition for the one day this task built, not a claim that the gap is closed generally. *) (* RG 17(a) (this file's own [holy_name_sunday], full citation there): the SECOND Sunday a year this branch must not leave [subject = Temporal] -- added alongside Holy Family's own check above, not replacing it (the two windows, 2-5 and 7-13 January, never overlap, so at most one of the two conditions is ever true for a given [d]). Guarded on [holy_name_sunday y = Some d] rather than only "is [d] in [2,5] January": every OTHER Sunday in that range must stay unnamed, and in a year where a DIFFERENT day of the window is the one true Sunday, [d] itself is never reached by [sunday_slug] as a Sunday to begin with (weekday alone already excludes it) -- but writing the check this way, against the SAME independently-computed date [holy_name_sunday] returns rather than a bare month/ day range, is what test_temporal_ef.ml's own anchor test below cross-checks against. ADDED, fix round 1 (coordinator finding F4): the Sunday shape's "takes the Sunday's own place, no commemoration of it" treatment is settled at RG LEVEL, not only in Holy Name's own Mass propers (this file's own [holy_name_sunday] citation already has that quote) -- RG 17's own CLOSING paragraph, immediately after its own lettered list (a)-(e), scan-verified: "Haec festa locum tenent dominicae occurrentis cum omnibus iuribus et privilegiis; de dominica, proinde, nulla fit commemoratio" -- "these feasts" (plural, covering the WHOLE list (a)-(e), Holy Name included) "hold the place of the occurring Sunday with all rights and privileges; of the Sunday, therefore, no commemoration is made". A second, independent primary source for the same conclusion the propers-level quote already gives, not a new claim. FRAGILE DERIVATION, noted per fix round 1 (coordinator finding F5): the mechanism that actually grants a LOSING Holy-Name-Sunday its RG 109(a) privilege when outranked (Precedence_ef.privilege_of's own (a) branch, {!Precedence _ef.is_sunday_slug}) reads it off the "-sunday" SUBSTRING in the slug ("ef-holy-name-sunday") -- a naming convention, not a citation of RG 17's own text quoted immediately above, which is the actual warrant ("these feasts hold the place of the occurring Sunday WITH ALL ITS RIGHTS AND PRIVILEGES" -- RG 109(a)'s own "of a Sunday" privilege is one of those rights). Right answer, fragile path: a future rename of this slug family would silently drop the privilege with no compiler or test failure pointing here. No live witness exists to test it either way (no fixed-date candidate ever outranks Holy Name in today's data -- test_precedence_ef.ml's own synthetic [test_class1_feast_inside_holy_name_window_end_to_end] proves the SYNTHETIC case only). *) let subject, names = if same d (holy_family_sunday y) then ( Colitur_kernel.Subject.Lord, Colitur_kernel.Names.of_list [ (Colitur_kernel.Lang.of_string_exn "la", "Sanctae Familiae Iesu, Mariae, Ioseph") ] ) else if holy_name_sunday y = Some d then (Colitur_kernel.Subject.Lord, holy_name_names) else (Colitur_kernel.Subject.Temporal, Colitur_kernel.Names.empty) in build ~subject ~names ~season:s ~slug ~colour ~rank ~week:(week d) () | None -> (* RG 17(a)'s own fallback, "secus die 2 ianuarii" (full citation on [holy_name_fallback_date] above): ONLY when [holy_name_sunday y] is [None] for [d]'s own civil year -- i.e. 2-5 January genuinely has no Sunday that year -- does 2 January itself carry the feast; every other year, 2 January is an ordinary Christmastide feria, exactly what [christmastide_feria_slug] below already computes for it. Checked here, ahead of that generic fallback, the same way [named]'s own fixed dates are checked ahead of everything ferial -- NOT folded into [named] itself, because unlike every one of [named]'s ~20 entries this one is CONDITIONAL on a per-year fact ([holy_name_sunday y] = [None]) that [named]'s bare [Date.t -> ... option] shape has no way to express without threading [subject] through every one of its other branches too -- the same reason Holy Family, immediately above, is not one of [named]'s outputs either (that branch's own comment). Season Christmastide (RG 72-73: 2 January is always within it); rank/colour/subject/names identical to the Sunday shape immediately above -- ONE feast, {!holy_name_names}'s own comment on why both shapes share it. Band-classified at entry 14's MOVABLE half regardless of which shape fired this year (Precedence_ef.band; both shapes are built through this same [temporal] function, hence [origin = Temporal] either way) -- RG 91's "primum mobilia, deinde fixa" split is a property of the TABLE ENTRY a feast occupies, and RG 17(a) names ONE feast with a fallback clause, not two differently-classified feasts that happen to share a Mass formulary; the Mass propers' own heading (docs/research/rules-register.md) states both shapes under the identical title for exactly this reason. *) if same d (holy_name_fallback_date y) && holy_name_sunday y = None then build ~subject:Colitur_kernel.Subject.Lord ~names:holy_name_names ~season:Christmastide ~slug:"ef-holy-name" ~colour:Colour.White ~rank:Class2 ~week:(week d) () else ( match christmastide_feria_slug d with | Some slug -> build ~season:s ~slug ~colour:(season_colour s) ~rank:(ferial_rank d s) ~week:(week d) () | None -> (* The days between Ash Wednesday and Lent I have proper Masses and belong to no numbered week. *) let after_ashes = days_between easter d in if after_ashes >= -45 && after_ashes <= -43 then build ~season:s ~slug:(Printf.sprintf "ef-lent-after-ashes-%s" (weekday_word d)) ~colour:Colour.Violet ~rank:Class3 ~week:None () else let colour = (* The Pentecost octave weekdays are red, not Paschaltide's white. *) if days_between easter d >= 50 && days_between easter d <= 55 then Colour.Red (* RG 128(b) (docs/research/rules-register.md §3b), primary text: "...a dominica in Septuagesima usque ad Vigiliam paschalem, EXCEPTIS: ... MISSA SIVE CHRISMATIS SIVE IN CENA DOMINI FERIA V HEBDOMADAE SANCTAE; ..." -- violet runs Septuagesima to the Easter Vigil EXCEPT (among others) "the Mass, whether of the Chrism or in Cena Domini [Holy Thursday], on Thursday of Holy Week" -- named as a WHOLE-MASS exception (unlike Palm Sunday's "blessing and procession of palms", which the SAME sentence carves out as only PART of that day, register §3b's own RG126 note on the not-yet-modelled per-action nuance), so this is a clean whole-day colour fact, not a per-action one the day/colour model cannot express. RG 122, fix round 1 (F9), states the same fact affirmatively rather than as an exception to RG 128's violet: "Demum adhibetur color albus, feria V Hebdomadae sanctae, in Missa Chrismatis et in Missa in Cena Domini" -- white is used, finally [among the White section's own list], on Thursday of Holy Week, in the Mass of Chrism and in the Mass in Cena Domini. Task 16, found via the missalemeum oracle comparison: every other Triduum day's oracle colour SET includes violet as one option (Good Friday "bv", Holy Saturday "vw" -- RG 132's black is a separate, ALREADY-flagged gap, register §3b, not touched here), but Holy Thursday's is white ALONE -- confirming this specific day, and only this one, needs the exception coded. *) else if days_between easter d = -3 then Colour.White else season_colour s in let week_n = week d in let slug = Printf.sprintf "ef-%s-%d-%s" (season_slug_word s) (Option.value week_n ~default:0) (weekday_word d) in build ~season:s ~slug ~colour ~rank:(ferial_rank d s) ~week:week_n ()))) (* Independent restatement of [named]'s fixed and Easter-relative dates, paired with the slug each should carry, for civil year [y]. Deliberately NOT derived from [named] itself -- consumed by [Validate]'s anchor- agreement check (design spec §5.7), which exists precisely to catch an accidental single-site drift (e.g. Ascension's [off 39] silently becoming [off 40]) that both sides moving together would hide. [holy_family_sunday] is NOT one of [named]'s own outputs (its own citation above explains why -- [temporal] applies it as a targeted [subject] override inside [sunday_slug]'s branch, not through [named]'s 4-tuple), but it is exactly the same kind of independently-computed anchor this list exists to guard -- an accidental drift in [holy_family_sunday]'s own [+7] would silently move Holy Family without this guard catching it, same as any other entry here. Holy Name of Jesus (RG 17(a)) is the SAME shape, restated per-year rather than as a single fixed pair: exactly ONE of its two slugs exists for any given [y] ([holy_name_sunday y] is [Some] xor [None]), so exactly one entry -- "ef-holy-name-sunday" at that Sunday, or "ef-holy-name" at [holy_name_fallback_date y] -- is appended below, matching whichever shape [temporal] itself will actually build that year. An erosion of EITHER branch is still caught: {!Validate.run} calls [anchors] for both the civil year a date falls in and the one before it (a liturgical year straddles two), so across any two consecutive years both shapes are exercised regardless of which one civil year [y] itself happens to land on. *) let anchors y = let easter = Computus.gregorian_easter y in let off n = Date.add_days easter n in [ ("ef-nativity", mk y 12 25); ("ef-nativity-vigil", mk y 12 24); ("ef-nativity-octave-day-5", mk y 12 29); ("ef-nativity-octave-day-6", mk y 12 30); ("ef-nativity-octave-day-7", mk y 12 31); ("ef-circumcision", mk y 1 1); ("ef-epiphany", mk y 1 6); ("ef-time-after-epiphany-sunday-1", holy_family_sunday y); ("ef-ash-wednesday", off (-46)); ("ef-passion-sunday", off (-14)); ("ef-palm-sunday", off (-7)); ("ef-easter-sunday", off 0); ("ef-low-sunday", off 7); ("ef-ascension-vigil", off 38); ("ef-ascension", off 39); ("ef-pentecost-vigil", off 48); ("ef-pentecost", off 49); ("ef-trinity", off 56); ("ef-corpus-christi", off 60); ("ef-sacred-heart", off 68); ("ef-christ-the-king", christ_the_king y) ] @ (match holy_name_sunday y with | Some d -> [ ("ef-holy-name-sunday", d) ] | None -> [ ("ef-holy-name", holy_name_fallback_date y) ]) (* Compile-time check that this module satisfies the kernel's rite contract. *) module _ : Colitur_kernel.Temporal.RITE = struct let id = id type season = Vocab_ef.season type rank = Vocab_ef.rank let vocab = Vocab_ef.vocab let year_start = year_start let temporal = temporal end