summaryrefslogtreecommitdiff
path: root/lib/rites
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rites')
-rw-r--r--lib/rites/rite_ef/precedence_ef.ml161
-rw-r--r--lib/rites/rite_ef/precedence_ef.mli20
2 files changed, 175 insertions, 6 deletions
diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml
index 374e9a4..c488596 100644
--- a/lib/rites/rite_ef/precedence_ef.ml
+++ b/lib/rites/rite_ef/precedence_ef.ml
@@ -1039,6 +1039,145 @@ 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:
+ - [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 appended, uncapped.
+ - 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) -- same
+ lookup, same append.
+ Both 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 the two conditions is ever true
+ for a given [observed]/[normal] pair; [List.filter_map] over both
+ sources together is simply the shortest way to express "check every
+ place a trigger slug could legitimately appear", not evidence they are
+ expected to co-occur).
+
+ PREPENDED, not appended, per RG 110(c)'s own explicit ordering directive
+ ("ante omnes alias commemorationes") -- the one clause in this rule that
+ actually specifies WHERE, restated here for shape (a)/(b) too rather than
+ drawing an unwarranted distinction between the two: no live data in this
+ codebase ever has a THIRD, unrelated commemoration alongside a RG 110
+ pair to make the ordering choice visible in real output (the companion is
+ always the only OTHER item, or the only item, in [normal] already), so
+ this is closer to a defensive convention than a tested outcome -- flagged
+ honestly rather than claimed as independently verified.
+
+ 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 each append. *)
+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
+ let trigger_slugs =
+ Slug.to_string observed.Precedence.cel.Celebration.slug
+ :: List.map (fun (c, _) -> Slug.to_string c.Precedence.cel.Celebration.slug) normal
+ in
+ let companions =
+ List.filter_map
+ (fun trigger_slug ->
+ match rg110_companion_slug trigger_slug with
+ | None -> None
+ | Some companion_slug ->
+ if already_has companion_slug then None else rg110_find_companion comms companion_slug)
+ trigger_slugs
+ in
+ companions @ 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,8 +1212,16 @@ 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
@@ -1127,10 +1274,12 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate)
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