summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/rites/rite_ef/precedence_ef.ml224
-rw-r--r--lib/rites/rite_ef/precedence_ef.mli20
-rw-r--r--lib/rites/rite_ef/temporal_ef.ml157
-rw-r--r--lib/rites/rite_ef/temporal_ef.mli11
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