diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-13 12:44:15 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-13 12:44:15 +0200 |
| commit | dc00d1f730b23fa99cd15ff142ecba515a9fe0b7 (patch) | |
| tree | e40f7f003008b33104723a6d8856168bbdea7633 /lib/rites | |
| parent | 59fbda87d565ad9b98c2493e9703390cd988609d (diff) | |
| parent | e45ec1ccce68d9edb9655004c21861d6bfb33a9d (diff) | |
| download | colitur-dc00d1f730b23fa99cd15ff142ecba515a9fe0b7.tar.gz colitur-dc00d1f730b23fa99cd15ff142ecba515a9fe0b7.zip | |
Merge branch 'ef-holyname-rg110': the Holy Name of Jesus, RG 110
Builds the Most Holy Name of Jesus with the fallback RG 17(a) and the
calendarium both specify -- 'secus die 2 ianuarii', 'vel, ea deficiente,
die 2 ianuarii' -- which was unimplemented, so in 3619 of the domain's
8417 years a II-class feast did not exist at all. Also retags it as a
feast of the Lord per RG 91 entry 14.
Builds RG 110's inseparable Peter/Paul commemoration ('in numero
orationum computando, pro unica habeantur') across the three calendarium
pairs, and adds the 30 June companion, which was missing from lectio's
source and from missalemeum's output alike -- corroborated in both
photographic scans plus RG 124(c), and added through adjustments.sexp so
the bootstrap's provenance stands.
14627 days change, in four shapes, none unclassified. The register's
earlier 852-day estimate for RG 110 reconciles exactly as the Lent Ember
subset of one shape: 288 + 284 + 280.
RG 110(c)'s ordering was implemented backwards at first and corrected:
'huic orationi additur altera' attaches the companion to the trigger's
own oration, so it follows it, where (a) and (b) attach to the day's and
so precede everything. The 'ante omnes alias commemorationes' half
remains unimplemented and is unreachable by construction -- shape (c)
needs the trigger to lose while still being admitted, which only a
I-class day or a II-class Sunday allows, and both RG 111 caps are one.
Records a fifth way the validation layers can be green while wrong:
nothing anywhere compares commemoration order.
Diffstat (limited to 'lib/rites')
| -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 |
