diff options
Diffstat (limited to 'lib/rites/rite_ef/precedence_ef.ml')
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.ml | 239 |
1 files changed, 211 insertions, 28 deletions
diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index 7b64b83..c9e6a55 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -651,6 +651,48 @@ let nativity_octave_prefix = "ef-nativity-octave-day-" [universal_layer] -- private: nothing outside [privilege_of] needs it. *) let alp_feria_prefixes = [ "ef-advent-"; "ef-lent-"; "ef-passiontide-" ] +(* RG 80 (Caput X, "De Litaniis maioribus et minoribus", §A "De Litaniis + maioribus"; both photographic scans and the electronic transcription, + word for word, no scan-vs-transcription conflict to adjudicate -- + docs/research/rules-register.md's own citation, this task): "80. + Litaniae maiores assignatae sunt diei 25 aprilis; si vero eo die + occurrit dominica Paschatis vel feria II post Pascha, transferuntur in + sequentem feriam III." -- the Major Litanies are assigned to 25 April; + but if Easter Sunday or Easter Monday falls on that day, they are + transferred to the FOLLOWING TUESDAY. Both trigger shapes land on the + SAME target, Easter+2 (worked out from the text, not independently + stated by it): 25 April = Easter Sunday means the "following Tuesday" + is Easter+2; 25 April = Easter Monday means Easter itself is 24 April, + and the following Tuesday is again Easter+2. {!transfer_target}'s own + Litanies branch, below, computes that directly. + + RG 81, same division, immediately following: "81. De Litaniis + maioribus nihil fit in Officio, sed tantum in Missa. Earum autem + commemoratio non est habenda commemoratio 'de Tempore'." -- nothing is + done in the Office, only in the Mass, and its commemoration is not to + be reckoned a "de Tempore" one. This is why [major_litanies_slug]'s own + data/ef/adjustments.sexp entry is [Commemoration_only]: RG 91's table + enumerates "dies liturgici" (Office days), and this is explicitly + denied any Office standing at all -- the same reasoning {!band}'s own + top-of-function guard already gives every [Commemoration_only] + candidate (never a table row, never [observed]), which is exactly the + behaviour RG 81 requires here independently. + + [major_litanies_slug] is this codebase's OWN invented English slug: RG + 80/81/109(f) name the observance, never a computer key for it, and + lectio does not compute the Major Litanies at all (this task's own + measurement) -- nothing to adopt verbatim, unlike every other slug this + file cross-checks against temporal_ef.ml's own conventions. + data/ef/adjustments.sexp's own [Add] directive is the ONE other place + this exact string is written; a rename here with no matching rename + there silently stops every branch below from ever seeing a live + candidate again -- flagged the same way [rg110_companion_slug] already + flags its own three hand-maintained pairs. *) +let major_litanies_slug = "major-litanies" + +let is_major_litanies (c : Vocab_ef.rank Precedence.candidate) = + String.equal (Slug.to_string c.Precedence.cel.Celebration.slug) major_litanies_slug + (* RG 109 (docs/research/rules-register.md §4, "Commemorations"): the closed list of privileged commemorations, checked in the register's own lettered order. A candidate matching none of (a)-(f) is ordinary, per the @@ -741,23 +783,30 @@ let privilege_of (c : Vocab_ef.rank Precedence.candidate) : Precedence.privilege it under its own name. *) else if is_temporal && List.exists (fun p -> String.starts_with ~prefix:p slug) alp_feria_prefixes then Precedence.Privileged - (* (f) RG 109(f) (§4): "of the Major Rogations, in Mass" -- the - Major Litanies (25 April, RG 80) are STILL not computed anywhere in - this codebase (temporal_ef.ml's own comment on [temporal]'s Rogation - branch, CORRECTED final fix wave item 7: they did not, in fact, - "arrive with Plan 3's sanctoral" -- Plan 3 shipped without them, - register §6 tracks this as an open item with no plan yet committed to - build it), so no candidate this engine can currently construct - represents one. There is no existing slug - convention to anchor a check to, and guessing one risks silently - misclassifying whatever a future task does name it -- a wrong citation - is worse than a missing one, so this is left unimplemented and flagged - in the task report rather than guessed. Deliberately NOT matched by - anything above: the Minor Litanies/Rogations ("ef-rogation-monday"/ - "-tuesday", RG 87) temporal_ef.ml DOES compute are a different - observance RG 109(f) does not name (RG 88: the Minor Rogations change - nothing in the Office at all), so they correctly fall through to - "ordinary" below, not this category. *) + (* (f) RG 109(f) (Caput XVI, "De Commemorationibus", §4): "de Litaniis + maioribus, in Missa" -- of the Major Litanies, IN THE MASS (RG 81's + own restriction: never in the Office). NOW LIVE (this task, + ef-major-litanies): this branch used to be dead code -- "no candidate + this engine can currently construct represents one" -- because + nothing built the Major Litanies as a candidate at all. + data/ef/adjustments.sexp's own [major_litanies_slug] entry + ({!major_litanies_slug}'s own citation above has the full RG 80/81 + text) is exactly that candidate now. Read by slug alone, the same + convention every other one-off entry in this file uses + ([nativity_octave_prefix], [rg110_companion_slug], [annunciation_slug] + below) -- RG 109(f) names one specific, closed-list observance, not a + structural property [is_temporal]/[rank] could derive the way (c)/(d)/ + (e) above do for whole classes of temporal ferias. Deliberately NOT + matched by anything above it: the Minor Litanies/Rogations + ("ef-rogation-monday"/"-tuesday", RG 87) temporal_ef.ml DOES compute + are a different observance RG 109(f) does not name (RG 88: the Minor + Rogations change nothing in the Office at all, and [is_sunday_slug]/ + [rank = Class1]/[nativity_octave_prefix]/the Ember prefixes/ + [alp_feria_prefixes] never match their slugs either), so they + correctly fall through to "ordinary" below, not this category -- + {!privilege_cases}'s own negative row in test_precedence_ef.ml pins + this boundary. *) + else if is_major_litanies c then Precedence.Privileged else Precedence.Ordinary let disposition ~(winner : Vocab_ef.rank Precedence.candidate) @@ -793,6 +842,63 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate) exception for two commemorations invoking the identical BVM, the exact gap this fix closes. *) Precedence.Omit + else if + is_major_litanies loser + && (let wslug = Slug.to_string winner.Precedence.cel.Celebration.slug in + String.equal wslug "ef-easter-sunday" || String.equal wslug "ef-easter-1-monday") + then + (* RG 80 -- {!major_litanies_slug}'s own citation above has the full + text and the "both trigger shapes land on Easter+2" derivation. + data/ef/adjustments.sexp's own [major_litanies_slug] entry is + UNCONDITIONALLY Fixed at 25 April, so it is offered as a candidate + every year regardless of what 25 April turns out to be -- this is + where the two RETRACTED blockers this task's own brief names + (docs/research/rules-register.md's "the recorded blocker was + wrong") actually close: [disposition] already receives [~winner], + so a rite-local test on the WINNER's own slug alone (no kernel + signature change, no [context]/date needed here at all) is enough + to tell the 194 trigger years apart from the other 8,223 -- exactly + the "zero kernel surface" the retraction predicted. "ef-easter- + sunday" and "ef-easter-1-monday" are [Temporal_ef.named]'s/[temporal]'s + own slugs for Easter Sunday and Easter Monday respectively (this + file's own [entry_15_band]-adjacent branches above already trust + the same convention); Easter Monday's own reliability as a marker + (measured, register: 8,417 occurrences in 8,417 years, once per + year, never displaced, being a I-class octave day) is what makes + reading it off a bare slug string safe here, the same argument that + retracted the second blocker. + + Checked ahead of the [Commemoration_only] branch immediately below: + that branch's own "always Commemorate, nothing overrides it" would + otherwise fire first, and this candidate would wrongly commemorate + 25 April itself in exactly the 194 years RG 80 forbids it from + doing so. No earlier branch in this function can pre-empt it + either -- [is_bvm_office] above is keyed on Marian/BVM subjects + this candidate never carries ({!major_litanies_slug}'s own entry is + [subject = Saint], and its slug is not on {!marian_slugs}). + + [Transfer], not [Omit]: RG 80 does not say the Litanies simply + vanish in a trigger year, it says WHERE they move to -- and + {!Precedence.disposition}'s own [Transfer] constructor, together + with the placement machinery {!Calendar} already has for RG 96 + (calendar.ml's [place_transfers]/[resolve_with_injected]), is + exactly "move a losing candidate to another day and inject it + there" -- reused here rather than invented a second time, because + RG 80's own transfer is structurally the same operation, just with + its OWN named target instead of RG 96's generic forward search + ({!transfer_target}'s own Litanies branch, below, computes that + target directly rather than searching for it). A + [Commemoration_only] candidate transferring is a new shape for this + codebase, checked rather than assumed safe: {!Precedence.resolve}'s + own fold matches [Transfer | Repose] uniformly, with no [status] + test anywhere in it, and once re-offered at the target date this + candidate is still [Commemoration_only], so it is still held out of + the band contest there too (the same [forced_comm] partition, + {!Precedence.resolve}'s own top comment) -- it can never + accidentally become [observed] at its transfer target either, + matching RG 81's "nihil fit in Officio" for the same reason it + cannot become [observed] on 25 April itself. *) + Precedence.Transfer else if cel.Celebration.status = Celebration.Commemoration_only then (* Always -- checked before RG 33's omission and RG 95's transfer so neither can override it: a Commemoration_only entry can never win @@ -1509,7 +1615,46 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) 2026 (Holy Family, a II-class Sunday) has St Hyginus (Class3, commemoration-only) as its only competing candidate -- missalemeum shows him "displaced" (omitted), never commemorated; the - pre-fix code admitted him regardless. *) + pre-fix code admitted him regardless. + + CORROBORATED (ef-major-litanies task, fix round 1, F5): RG 111 + sits in Caput XVI, "De Commemorationibus", governing "tam pro + Missa quam pro Officio" per RG 106 -- read alone it could be + mistaken for Office-shaped, incidentally reused for the Mass. + Rubricae Generales Missalis Romani n. 434(b) (Part "VIII - De + diversis Missae partibus", "D) De orationibus", "I - De + orationibus in genere"), verified word for word, all three + documents: "in dominicis II classis, nulla alia admittitur + oratio, praeter commemorationem festi II classis, quae tamen + omittitur si commemoratio privilegiata facienda sit" -- + IDENTICAL IN ITS OPERATIVE CLAUSE to the one above -- not word + for word throughout: RG 111(b) opens "una tantum admittitur + commemoratio, SCILICET DE FESTO II CLASSIS", n. 434(b) recasts + that into the orations register as "NULLA ALIA ADMITTITUR ORATIO, + PRAETER COMMEMORATIONEM festi II classis", and only the trailing + "quae tamen omittitur si commemoratio privilegiata facienda sit" + is verbatim in both. That trailing clause is the one this + adjudication turns on. And n. 434 is not merely a different part + of the same document: the running heads show RG 111 under + "Rubricae generales" and n. 434 under "Rubricae generales Missalis + Romani" -- two distinct rubrical corpora bound in one volume, + which is the whole force of the corroboration. Explicitly scoped "post + orationem Missae" -- an independent, Mass-structure-rubric + confirmation of the exact same privilege-overrides-ordinary rule + this branch already implements, from a different part of the + same document. First load-bearing live witness for this branch's + "privileged wins even against a WORSE table position" half: the + Major Litanies (RG 109(f), major_litanies_slug above), whose own + [band] value is {!unclassified} (worse than every real table + entry, per that constant's own comment) yet still displaces St + Mark's real entry-16 table position on the four Sunday-conflict + years the Litanies' own data/ef/expected-divergences-missalemeum + .sexp M20 entry adjudicates -- every earlier witness for this + branch (Holy Family/Hyginus above; the RG16(a) admit_cases table) + happened to also be the day's own privileged Sunday/feast winning + OUTRIGHT (band already decided it before admit ran), not two + independently-offered LOSING candidates settled by this clause + alone. *) (match List.filter is_privileged sorted with | best :: _ -> [ drop_band best ] | [] -> ( @@ -1656,14 +1801,52 @@ let rec search_from (occupant : Date.t -> Vocab_ef.rank Celebration.t) (steps : directly tests the rubric's own words. *) let transfer_target (c : Vocab_ef.rank Precedence.candidate) (origin : Date.t) (occupant : Date.t -> Vocab_ef.rank Celebration.t) : Date.t = - let general_target = search_from occupant 0 (Date.add_days origin 1) in - let is_annunciation = Slug.to_string c.Precedence.cel.Celebration.slug = annunciation_slug in let easter = Computus.gregorian_easter (Date.year origin) in - if is_annunciation && Date.compare general_target easter > 0 then - (* Low Sunday = Easter + 7 (register §0, temporal_ef.ml's [off 7]); the - Monday after it = Easter + 8. Searched onward from there exactly - like the general case searches from [origin + 1] -- "only if that - day is itself blocked" (rite.mli) is [search_from]'s ordinary - behaviour, not a second mechanism. *) - search_from occupant 0 (Date.add_days easter 8) - else general_target + if is_major_litanies c then + (* RG 80 -- {!major_litanies_slug}'s own citation (precedence_ef.ml, + above [privilege_of]) has the full text and the "both trigger + shapes land on Easter+2" derivation. Checked BEFORE computing + [general_target] at all (unlike the Annunciation branch below, + which computes the general RG 96 walk first and only overrides it + conditionally) -- this candidate's target is never the general + walk's own result, so running {!search_from} for it would be + wasted work at best. + + "The following Tuesday", unconditionally: [search_from]'s ordinary + forward walk exists because a DISPLACED FEAST needs an unoccupied + (non-I/II-class) day to be fully celebrated as itself (RG 96's own + "next day that is not I or II class"). The Major Litanies are never + a feast -- RG 81: "nihil fit in Officio, sed tantum in Missa" -- + only ever a commemoration once they arrive, and a commemoration + needs no unoccupied day at all (RG 108-111 admit commemorations on + I-class days routinely, see [privilege_of]'s own (b) branch and + [admit]'s [Class1] case above). Running {!search_from} here would + therefore be actively WRONG, not merely unnecessary: Easter+2 is + itself I-class (within the Easter octave, {!band}'s own entry-10 + branch, {!is_blocking}'s own [Class1] test), so a search starting + there would walk PAST the exact date RG 80 names, looking for a + day the rubric never asked for. + + Total and terminating trivially (no search performed at all). + Strictly after [origin], {!Rite.t.transfer_target}'s own contract + (rite.mli), on both trigger shapes (this file's header, worked + examples): 25 April = Easter Sunday (origin = Easter itself = + Easter+0, target = Easter+2 = origin+2) or 25 April = Easter Monday + (Easter itself = 24 April = origin-1, target = Easter+2 = + origin+1) -- either way strictly forward. Not conditioned on + [occupant] at all, unlike every other branch in this function -- + deliberately: nothing about RG 80's own text makes the target + depend on what else is observed that year, only on Easter's own + date. *) + Date.add_days easter 2 + else + let general_target = search_from occupant 0 (Date.add_days origin 1) in + let is_annunciation = Slug.to_string c.Precedence.cel.Celebration.slug = annunciation_slug in + if is_annunciation && Date.compare general_target easter > 0 then + (* Low Sunday = Easter + 7 (register §0, temporal_ef.ml's [off 7]); the + Monday after it = Easter + 8. Searched onward from there exactly + like the general case searches from [origin + 1] -- "only if that + day is itself blocked" (rite.mli) is [search_from]'s ordinary + behaviour, not a second mechanism. *) + search_from occupant 0 (Date.add_days easter 8) + else general_target |
