diff options
Diffstat (limited to 'lib/rites/rite_ef')
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.ml | 224 | ||||
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.mli | 20 | ||||
| -rw-r--r-- | lib/rites/rite_ef/temporal_ef.ml | 157 | ||||
| -rw-r--r-- | lib/rites/rite_ef/temporal_ef.mli | 11 |
4 files changed, 401 insertions, 11 deletions
diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index 374e9a4..5a2d307 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -514,7 +514,18 @@ let privilege_of (c : Vocab_ef.rank Precedence.candidate) : Precedence.privilege let is_temporal = c.Precedence.origin = Precedence.Temporal in let open Vocab_ef in (* (a) RG 109(a) (§4): "of a Sunday" -- the same slug marker RG 33's - [impedes_vigil] already reads to answer "is this candidate a Sunday". *) + [impedes_vigil] already reads to answer "is this candidate a Sunday". + + FRAGILITY, named here because this is the decision point (fix-round + re-review, F5): [is_sunday_slug] is a SUBSTRING match, so the Holy Name + of Jesus obtains this privilege on its Sunday shape only because its + slug happens to read "ef-holy-name-sunday". That is a naming accident, + not a cited rule. The real warrant is RG 17's own closing paragraph + (quoted in full at temporal_ef.ml's [holy_name_sunday]): "Haec festa + locum tenent dominicae occurrentis cum omnibus iuribus et privilegiis" + -- these feasts hold the occurring Sunday's place WITH ALL ITS RIGHTS + AND PRIVILEGES, of which RG 109(a)'s privilege is one. A slug rename + would silently drop it while every test stayed green. *) if is_sunday_slug slug then Precedence.Privileged (* (b) RG 109(b) (§4): "of a I-class day" -- the candidate's own rank. In this codebase's current disposition rules the ONLY way a @@ -1039,6 +1050,191 @@ let rec take n = function still the pair, not the triple; only the INPUT carries [band]. *) let drop_band (c, p, (_ : int)) = (c, p) +(* RG 110 (docs/research/rules-register.md §4, Caput XIV, "De dierum + liturgicorum occurrentia perpetua"), full text, both photographic scans, + word for word: "110. In Officio et Missa S. Petri semper fit + commemoratio S. Pauli, et vicissim. Haec commemoratio dicitur + inseparabilis; et duae orationes adeo in unam coalescere censentur ut, in + numero orationum computando, pro unica habeantur. Proinde: a) in Officio + S. Petri aut S. Pauli, oratio alterius Apostoli additur, ad Laudes et ad + Vesperas, sub unica conclusione, orationi diei, absque antiphona et + versu; b) in Missa S. Petri aut S. Pauli, oratio alterius Apostoli + additur, sub unica conclusione, orationi diei; c) quoties vero oratio + unius Apostoli addenda est ad modum commemorationis, huic orationi + additur altera immediate, ante omnes alias commemorationes." -- in the + Office and Mass of St Peter, a commemoration of St Paul is ALWAYS made, + and vice versa. This commemoration is called INSEPARABLE, and the two + orations are held to coalesce into one so much that, in counting the + number of orations [RG 111's own admission cap], they are counted AS + ONE. Accordingly: (a)/(b) in the Office/Mass OF Peter or Paul [i.e. when + one of them is the day's own observed office], the other Apostle's + oration is added, under a single conclusion, to the oration of the day; + (c) whenever the oration of ONE Apostle must be added AS A + COMMEMORATION [i.e. when one of them is not the day's own office but is + itself only being admitted as an ordinary/privileged commemoration + elsewhere], the OTHER is added to it immediately, BEFORE ALL OTHER + COMMEMORATIONS. + + Three real pairs in this codebase's data, all three confirmed on both + photographic scans (register §4): 25 January's [conversion-of-st-paul] + + [peter]; 22 February's [chair-of-st-peter] + [paul]; 30 June's + [in-commemoratione-sancti-pauli-apostoli] + [commemoration-of-st-peter] + (data/ef/adjustments.sexp's own `Add` directive -- lectio has no + equivalent entry at all, a genuine upstream data gap, not merely a + colitur bootstrap miss). 29 June ([sts-peter-paul]) needs none: it is + already the JOINT feast, not one Apostle's office alone. [peter]/[paul] + already existed as ordinary [Commemoration_only] sanctoral candidates + before this task (Task 10's bootstrap) -- part of RG 110's own machinery, + just wired into the ORDINARY, CAPPED RG 111 admission contest instead of + this rule's own UNCAPPED one, which is the defect this closes: measured + (1583-9999, `tools/`'s own throwaway scan, reproduced and discarded per + this task's own report) at 3,533 years where [chair-of-st-peter] is + observed but [paul] loses the day's single non-Sunday-II-class slot to a + competing privileged Advent/Lent/Ember feria (RG 109(e)/(d)), and a + FURTHER 593 years of shape (c) below, where [chair-of-st-peter] itself + loses outright to an ordinary (ordinary, i.e. non-I-class) II-class + Sunday and is admitted only AS a commemoration -- [paul] was entirely + absent in every one of those 593 years too, before this fix. 25 + January/30 June never show shape (c) at all: both trigger candidates are + [Class3], below RG 111(b)'s "de festo II classis" floor, so neither can + ever be admitted as a mere ordinary commemoration on a II-class Sunday + in the first place -- confirmed by the same scan, 0 instances either + way, not assumed absent. + + [rg110_companion_slug] reads ONLY the candidate's own slug -- the same + convention every other not-an-RG-citation lookup in this file uses + ({!universal_layer}, {!vigil_suffix}, {!nativity_octave_prefix}) -- not + [subject]/[rank]/anything else, because "which slug pairs with which" is + RG 110's own closed, three-pair list, not a general property [band]/ + [disposition] could derive structurally the way (say) [is_sunday_slug] + derives Sunday-ness from a naming convention. A rename of any of the six + slugs involved (either side of a pair) has nowhere else to be caught but + here -- flagged the same way {!annunciation_slug} already is. *) +let rg110_companion_slug (slug : string) : string option = + if slug = "chair-of-st-peter" then Some "paul" + else if slug = "conversion-of-st-paul" then Some "peter" + else if slug = "in-commemoratione-sancti-pauli-apostoli" then Some "commemoration-of-st-peter" + else None + +(* Looks [companion_slug] up directly in [comms] -- the SAME full candidate + pool {!admit} itself sorts from, before any cap is applied, so this finds + the companion regardless of whether the NORMAL admission process would + have kept it (it never would: {!band} always ranks a [Commemoration_only] + candidate at {!unclassified}, worse than literally any real table entry, + so a companion competing on its own merits never survives a contested + slot -- RG 110's entire point is that it should not have to). Returns the + ORIGINAL triple's candidate/privilege pair, unrebuilt, for the same + physical-equality reason {!drop_band} and this function's own top comment + already document -- reusing the [privilege] {!disposition}/{!privilege_of} + already computed for it rather than inventing a new tag here. *) +let rg110_find_companion comms companion_slug = + List.find_map + (fun (c, p, (_ : int)) -> + if Slug.to_string c.Precedence.cel.Celebration.slug = companion_slug then Some (c, p) else None) + comms + +(* RG 110's own uncapped addition, applied to [normal] -- the day's ALREADY- + COMPUTED, ORDINARILY-CAPPED admission result (every branch of {!admit} + below, unchanged otherwise). Two triggers, covering RG 110(a)/(b) and + RG 110(c) respectively, in this function's own comment above -- and, + fix round 1, TWO DIFFERENT ORDERINGS, not one, because the two shapes' + own primary texts say different things about where the companion goes: + + - [observed] itself is one of the three trigger slugs (shape (a)/(b): + Peter's or Paul's own Office/Mass IS today's day) -- the companion is + looked up directly and PREPENDED to [normal]. (a)/(b)'s own text, + "oratio alterius Apostoli additur ... orationi diei" (the OTHER + Apostle's oration is added ... TO THE DAY'S OWN oration), has nothing + in [normal] to order the companion AGAINST in the first place: the + day's own oration is [observed], not a member of this list, so + leading the list is simply where an item with nothing to be "before" + or "after" inside it goes -- not itself a citation for a POSITION + the primary text does not describe a list position for at all. + - one of [normal]'s OWN members is a trigger slug (shape (c): Peter's or + Paul's own office lost the day outright but is itself being admitted + as an ordinary/privileged commemoration of whatever else won) -- the + companion is looked up the same way, but SPLICED IN IMMEDIATELY AFTER + its own trigger, not prepended to the whole list. RG 110(c)'s own + text, word for word: "quoties vero oratio unius Apostoli addenda est + ad modum commemorationis, HUIC ORATIONI ADDITUR ALTERA immediate, ante + omnes alias commemorationes" -- "huic orationi" (dative, "to THIS + oration") refers back to "oratio unius Apostoli" (the trigger's OWN + oration, the one just named as "addenda... ad modum commemorationis") + -- so "additur altera" (the OTHER is added) means the companion is + added TO the trigger's own oration, i.e. FOLLOWS it, not precedes it; + the pair as a BLOCK then precedes "omnes ALIAS commemorationes" + ("alias" = OTHER, unrelated ones -- cannot include the pair's own + first member). CORRECTED, fix round 1: the original version of this + function prepended in BOTH shapes uniformly, citing (c)'s own "ante + omnes alias" for shape (a)/(b) too by analogy -- which is fine for + WHERE the pair sits relative to unrelated commemorations, but wrongly + also reordered the PAIR'S OWN internal order in shape (c), producing + `paul, chair-of-st-peter` where the primary text requires + `chair-of-st-peter, paul` (verified against BOTH photographic scans, + word for word, no divergence to adjudicate). Confirmed live: every one + of the 593 shape-(c) days measured in this task's own report emitted + the companion FIRST before this fix; RG 111's own admission branches + never build a second, unrelated commemoration alongside a shape-(c) + trigger in today's data (RG 111(b) admits exactly one candidate on a + II-class Sunday, the only day shape (c) ever occurs), so this + splice-after-trigger behaviour is Total the same way {!take}/ + {!drop_band} are, but has no OTHER live witness to also prove the + "ante omnes alias" half of (c) against. + + Both shapes can never fire together on one call ({!Precedence.resolve} + only ever calls {!Precedence.rules.admit} once per date, and all three + trigger slugs are distinct FIXED calendar dates -- 25 January, 22 + February, 30 June -- so at most one of [observed]/[normal]'s own members + is ever a trigger for a given call). + + A companion already present in [normal] on its own merits (structurally + unreachable today -- {!unclassified}'s own comment above -- but not + provably so for every future rite/data shape) is not added a second + time: [List.exists] guards both branches. + + NEW BLIND SPOT, fix round 1: no validation layer in this codebase + compares commemoration ORDER at all -- the lectio differential does not + compare commemorations (its own "limit 1"); the oracle's own + [identity_diff] sorts both sides into a multiset before comparing; + and [describe] (test_golden.ml) sorts its own comms field too, for the + same reason presence/identity checks should not be order-sensitive by + accident. (CORRECTED, fix-round re-review: this previously also named + {!Colitur_kernel.Record}. Record.t has NO commemorations field at all -- + see record.mli -- so it neither sorts nor carries them.) This function's own [admit_cases] unit + test (test_precedence_ef.ml) is therefore the ONLY place in this + codebase's test suite where commemoration order is asserted at all -- + flagged here, and in CLAUDE.md's own "know what each layer cannot see" + section, as a genuine, permanent limit, not merely this fix's own gap. *) +let rg110_additions (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege * int) list) + ~(observed : Vocab_ef.rank Precedence.candidate) + (normal : (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list) : + (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list = + let already_has slug = + List.exists (fun (c, _) -> Slug.to_string c.Precedence.cel.Celebration.slug = slug) normal + in + match rg110_companion_slug (Slug.to_string observed.Precedence.cel.Celebration.slug) with + | Some companion_slug when not (already_has companion_slug) -> ( + (* Shape (a)/(b): prepend -- see this function's own comment above. *) + match rg110_find_companion comms companion_slug with + | Some companion -> companion :: normal + | None -> normal) + | _ -> + (* Shape (c): splice each trigger's companion in immediately after it + -- {!List.concat_map} rather than a fold, so a trigger with no + companion found (defensive; {!rg110_find_companion} returning + [None]) simply passes through unchanged, and a member that is not + a trigger at all ([rg110_companion_slug] returns [None]) is never + touched. *) + List.concat_map + (fun ((c, _) as pair) -> + match rg110_companion_slug (Slug.to_string c.Precedence.cel.Celebration.slug) with + | Some companion_slug when not (already_has companion_slug) -> ( + match rg110_find_companion comms companion_slug with + | Some companion -> [ pair; companion ] + | None -> [ pair ]) + | _ -> [ pair ]) + normal + let admit ~(observed : Vocab_ef.rank Precedence.candidate) ~(temporal : Vocab_ef.rank Precedence.candidate) (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege * int) list) : @@ -1073,14 +1269,22 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) is_sunday_slug (Slug.to_string temporal.Precedence.cel.Celebration.slug) in let open Vocab_ef in - match (observed_rank, day_is_sunday) with - | Class1, _ -> + (* RG 110's own uncapped additions are layered on AFTER this match -- + {!rg110_additions}'s own top comment -- never inside any one branch: + the three trigger slugs can each reach [observed] via any rank/Sunday + combination the table admits (a Class2 fixed feast, here, but nothing + in {!rg110_additions} itself assumes that), so computing it once, + uniformly, over whatever this match already decided is both simpler + and safer than duplicating the same lookup into every branch. *) + let normal = + match (observed_rank, day_is_sunday) with + | Class1, _ -> (* RG 111: "I class: none save one privileged." Ordinary commemorations never get a slot at all on a I-class day, no matter how many are due; at most one privileged one does, the highest-precedence one (RG 113: {!band}'s own table order) if several are. *) (match List.filter is_privileged sorted with [] -> [] | best :: _ -> [ drop_band best ]) - | Class2, true -> + | Class2, true -> (* RG 111(b), primary text, RE-VERIFIED word for word against the scan (final fix wave; this sentence is the sole textual basis for the shipped rank-floor fix below, and the register's own §4 "RG 111" @@ -1121,16 +1325,18 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) match List.filter (fun (c, _, _) -> c.Precedence.cel.Celebration.rank = Class2) sorted with | [] -> [] | best :: _ -> [ drop_band best ])) - | Class2, false -> + | Class2, false -> (* RG 111: "other II class: one" -- no privilege-override clause here, unlike the Sunday case immediately above, so the day's one slot goes to whichever candidate outranks the rest by RG 113's own table-of-precedence order ({!band}), privileged or not. *) (match sorted with [] -> [] | best :: _ -> [ drop_band best ]) - | (Class3 | Class4), _ -> - (* RG 111: "III-IV class: at most two" -- by RG 113's table order, same - as the non-Sunday II-class case, just with room for two. *) - List.map drop_band (take 2 sorted) + | (Class3 | Class4), _ -> + (* RG 111: "III-IV class: at most two" -- by RG 113's table order, same + as the non-Sunday II-class case, just with room for two. *) + List.map drop_band (take 2 sorted) + in + rg110_additions comms ~observed normal (* Task 11: RG 96 -- where an impeded I-class feast lands (docs/research/ rules-register.md §4, "Transfer/translation"). [band] decides who is diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli index 6a96c4e..765b573 100644 --- a/lib/rites/rite_ef/precedence_ef.mli +++ b/lib/rites/rite_ef/precedence_ef.mli @@ -243,6 +243,26 @@ val september_ember_prefix : string - [observed] a [Class3] or [Class4] day: at most two, by {!band}'s table order alone. + ADDED, ef-holyname-rg110 task: RG 110 (docs/research/rules-register.md + §4, Caput XIV) -- "In Officio et Missa S. Petri semper fit + commemoratio S. Pauli, et vicissim... pro unica habeantur" -- whenever + [observed], or one of the candidates the FOUR cases above already admit, + is one of RG 110's own three trigger slugs (25 January's + [conversion-of-st-paul], 22 February's [chair-of-st-peter], 30 June's + [in-commemoratione-sancti-pauli-apostoli]), its companion Apostle + ([peter]/[paul]/[commemoration-of-st-peter] respectively) is looked up + directly in [comms] and PREPENDED to whatever the four cases above + already produced -- UNCAPPED, i.e. added on top of, not competing for, + any of those cases' own admission limits ("pro unica habeantur": the + pair is held as ONE for RG 111's own counting purposes, so it never + consumes a second ordinary/privileged slot). This is layered on AFTER + the four cases above decide the day's ordinarily-capped result, uniformly + regardless of which of the four produced it -- see the .ml's own + [rg110_additions] for the full citation, the measured blast radius, and + why the two lookups (observed itself, and each already-admitted + candidate) together cover both of RG 110's own textual triggers, + (a)/(b) and (c). + 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 diff --git a/lib/rites/rite_ef/temporal_ef.ml b/lib/rites/rite_ef/temporal_ef.ml index 1ce9de9..bbd88ec 100644 --- a/lib/rites/rite_ef/temporal_ef.ml +++ b/lib/rites/rite_ef/temporal_ef.ml @@ -104,6 +104,56 @@ let christ_the_king y = sunday_on_or_before (mk y 10 31) 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), @@ -533,15 +583,103 @@ let temporal d = 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 -> ( + | 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) @@ -608,7 +746,19 @@ let temporal d = 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. *) + 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 @@ -633,6 +783,9 @@ let anchors y = ("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 diff --git a/lib/rites/rite_ef/temporal_ef.mli b/lib/rites/rite_ef/temporal_ef.mli index 68fe64e..5e85111 100644 --- a/lib/rites/rite_ef/temporal_ef.mli +++ b/lib/rites/rite_ef/temporal_ef.mli @@ -21,6 +21,17 @@ val christ_the_king : int -> Date.t fallback for it), so this needs none. *) val holy_family_sunday : int -> Date.t +(** RG 17(a): the Sunday falling 2-5 January in civil year [y], the Most Holy + Name of Jesus's own primary date -- [None] when no such Sunday exists + that year (3,619 of the 8,417 years in [1583, 9999]), unlike + {!holy_family_sunday}'s window, which never is. *) +val holy_name_sunday : int -> Date.t option + +(** RG 17(a)'s own "secus die 2 ianuarii" fallback: 2 January in civil year + [y]. Only actually carries the feast when {!holy_name_sunday} [y] is + [None] for that year -- {!temporal} is what applies that condition. *) +val holy_name_fallback_date : int -> Date.t + (** The named temporal days: I-class feasts of the Lord, vigils, and days within the Octave of the Nativity. Returns (season, slug, colour, rank). Carries no week of its own -- {!temporal} computes it uniformly via {!week} for |
