summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 22:46:30 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 22:46:30 +0200
commit07c87370d6a0de687b42a41d967135578f014bdc (patch)
tree841b29952515e9fd662bc46c0b8457303fb2300d /lib
parent553dc44d2ba0e131e7f2ac79dc755641afcd6a1c (diff)
downloadcolitur-07c87370d6a0de687b42a41d967135578f014bdc.tar.gz
colitur-07c87370d6a0de687b42a41d967135578f014bdc.zip
rite(ef): occurrence dispositions (RG 92-95, 33, 94)
Precedence_ef.disposition decides the loser's fate in an occurrence: a Commemoration_only celebration is always commemorated (it can never win or transfer); a I- or II-class vigil impeded by any Sunday or a I-class feast is entirely omitted (RG 33), checked before the generic rule below or the Nativity/Pentecost Vigil could wrongly transfer; any other I-class loser transfers (RG 95 -- only I class has the right of translation); everything else is commemorated, with the admit-or-omit decision left to RG 108-111's admission count (Task 9). RG 94 needs no code: resolve always compares a loser against the day's actual winner, never against a departed sibling, so no commemoration can ride along with a transferred feast in this design. This is the branch that completes Task 7's carried All Souls fix: once it loses to an occurring Sunday, its untouched Class1 rank routes it to Transfer via the generic rule, not a special case. Landing on 3 November is Rite.transfer_target's job, not wired up yet. Commemorate carries an interim Precedence.Ordinary privilege pending Task 9's RG 109 implementation, exposed as interim_privilege for that task to replace. Table-driven tests cover each rule, including RG 33's boundary from both sides and a Commemoration_only loser that is also Class1 and vigil-shaped to pin the branch ordering. Mutation-tested: disabling RG 33, either direction of RG 95's rank condition, or the Commemoration_only priority check each fail exactly the rows built to catch them.
Diffstat (limited to 'lib')
-rw-r--r--lib/rites/rite_ef/precedence_ef.ml95
-rw-r--r--lib/rites/rite_ef/precedence_ef.mli46
2 files changed, 141 insertions, 0 deletions
diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml
index 0716177..d8d64a8 100644
--- a/lib/rites/rite_ef/precedence_ef.ml
+++ b/lib/rites/rite_ef/precedence_ef.ml
@@ -214,3 +214,98 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc
"feria" that is also a vigil is not a feria RG 91 describes. *)
else if (not is_vigil) && rank = Class4 then 28
else unclassified
+
+(* Task 8: what happens to the day's LOSING candidate (docs/research/
+ rules-register.md §4, "Occurrence" RG 92-95 and "Vigils" RG 33, plus RG
+ 94). [band] above decides who wins; this decides the loser's fate, which
+ turns on the LOSER's own rank and status (RG 95), except RG 33's vigil
+ omission, which also has to read the winner. Nothing here ever returns
+ [Precedence.Repose]: that disposition denotes RG 100-102's *repositio*
+ (perpetual impediment from a proper/diocesan calendar), out of this
+ plan's scope -- see calendar.mli's own note that nothing in the EF
+ ruleset currently emits it. *)
+
+(* RG 33 (register line 383-384): a I- or II-class vigil falling on any
+ Sunday or a I-class feast is entirely omitted. Every Sunday slug this
+ rite's temporal cycle produces -- named (temporal_ef.ml's [named], e.g.
+ "ef-easter-sunday") or the generic "ef-<season>-sunday-<n>" fallback
+ ([sunday_slug]) -- contains this marker; nothing else [band] classifies
+ does. Not an RG citation itself -- see [universal_layer]'s note on this
+ file's own naming conventions -- exposed for the same reason as
+ {!vigil_suffix}: a future rename of temporal_ef's Sunday-slug format has
+ somewhere to be caught other than a silently-wrong RG 33 disposition. *)
+let sunday_marker = "-sunday"
+
+let contains_substring s ~needle =
+ let ls = String.length s and ln = String.length needle in
+ let rec at i = i + ln <= ls && (String.sub s i ln = needle || at (i + 1)) in
+ ln = 0 || at 0
+
+let is_sunday_slug slug = contains_substring slug ~needle:sunday_marker
+
+(* RG 33's "any Sunday or a I-class feast": every RG 91 entry that can ever
+ outrank a II-class vigil (entry 21) without itself being a Sunday is I
+ class by the table's own structure (entries 1-13), so [rank = Class1]
+ alone already covers every way a I-class vigil (entries 5, 9 -- Nativity,
+ Pentecost) can be impeded at all; the slug check is what a II-class vigil
+ impeded by an ordinary Sunday (entry 15, rank Class2) needs, since that
+ winner's own rank is not Class1. *)
+let impedes_vigil (winner : Vocab_ef.rank Precedence.candidate) =
+ let cel = winner.Precedence.cel in
+ cel.Celebration.rank = Vocab_ef.Class1
+ || is_sunday_slug (Slug.to_string cel.Celebration.slug)
+
+(* Not yet RG 109 (Task 9's job: the closed list of privileged commemorations
+ and RG 108-111's admission counts). Every [Commemorate] this function
+ returns carries this one placeholder rather than a silent default, so the
+ choice is visible and grep-able. [Ordinary] chosen over [Privileged]
+ deliberately: it grants no admission entitlement RG 111 has not earned,
+ so code that trusts this value before Task 9 replaces it under-privileges
+ a commemoration rather than over-privileges one -- the safer direction to
+ be wrong in. *)
+let interim_privilege = Precedence.Ordinary
+
+let disposition ~(winner : Vocab_ef.rank Precedence.candidate)
+ ~(loser : Vocab_ef.rank Precedence.candidate) : Precedence.disposition =
+ let open Vocab_ef in
+ let cel = loser.Precedence.cel in
+ 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
+ (Precedence.resolve holds it out of the band contest entirely, see
+ that module's [resolve]) and, per the brief, can never transfer
+ either. *)
+ Precedence.Commemorate interim_privilege
+ else if
+ (cel.Celebration.rank = Class1 || cel.Celebration.rank = Class2)
+ && is_vigil (Slug.to_string cel.Celebration.slug)
+ && impedes_vigil winner
+ then
+ (* RG 33. Checked before the generic Class1 -> Transfer rule below, or a
+ I-class vigil (Nativity, Pentecost) impeded on its own Sunday/
+ I-class-feast terms would wrongly transfer instead of vanishing. *)
+ Precedence.Omit
+ else if cel.Celebration.rank = Class1 then
+ (* RG 95: only I-class feasts have the right of translation. This is the
+ branch that completes Task 7's All Souls fix (register line 334, RG
+ 91 entry 8): All Souls is I class and not a vigil, so once it loses
+ to an occurring Sunday it reaches here and transfers -- to 3
+ November per the register, but WHERE it lands is
+ Rite.transfer_target's job (RG 96), not this function's; disposition
+ only says THAT it moves. *)
+ Precedence.Transfer
+ else
+ (* RG 95's other branch, for everything below I class: "aut
+ commemorantur aut penitus omittuntur" -- commemorated or wholly
+ omitted. Which of the two survives is RG 108-111's admission count
+ (Task 9's [admit]), not this function's decision; this only opens the
+ commemoration.
+
+ RG 94 (a fixed-day commemoration is not carried along with a
+ transferred feast) needs no code here: [Precedence.resolve] calls
+ this function once per loser, always against the day's actual
+ [observed] winner -- never against a fellow loser that itself
+ transferred away -- so no mechanism exists by which a commemoration
+ could ride along with a departing feast in the first place; there is
+ nothing to suppress. *)
+ Precedence.Commemorate interim_privilege
diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli
index 63fb0a1..9fe8789 100644
--- a/lib/rites/rite_ef/precedence_ef.mli
+++ b/lib/rites/rite_ef/precedence_ef.mli
@@ -56,3 +56,49 @@ val unclassified : int
including shapes the 1962 table itself does not describe (see
{!unclassified}). *)
val band : Vocab_ef.season Precedence.context -> Vocab_ef.rank Precedence.candidate -> int
+
+(** RG 33's marker: every Sunday slug this rite's temporal cycle produces
+ (temporal_ef.ml's [named] and [sunday_slug]) contains this substring;
+ nothing else {!band} classifies does. Also colitur's own convention, not
+ an RG citation -- see {!universal_layer} -- exposed for the same reason
+ as {!vigil_suffix}: a rename of temporal_ef's Sunday-slug format has
+ somewhere to be caught other than a silently-wrong RG 33 disposition. *)
+val sunday_marker : string
+
+(** The placeholder {!Precedence.privilege} every [Commemorate] disposition
+ below carries until Task 9 implements RG 109's closed list of privileged
+ commemorations and RG 108-111's admission counts. Exposed so Task 9 (and
+ any test wanting to assert on it explicitly) does not have to duplicate
+ the literal [Precedence.Ordinary]. *)
+val interim_privilege : Precedence.privilege
+
+(** [disposition ~winner ~loser]: RG 92-95, 33, 94 (docs/research/
+ rules-register.md §4, "Occurrence" and "Vigils"). What becomes of a
+ losing candidate, decided by the LOSER's own rank and status (RG 95),
+ except RG 33's vigil omission, which also reads the winner:
+ - a {!Celebration.status} of [Commemoration_only] is always
+ [Commemorate] (checked first: it can never win -- see
+ {!Precedence.resolve} -- and, by that same status's own definition,
+ already denotes an office with nothing left to translate, so it never
+ transfers either; not itself a further RG citation beyond RG 93's
+ general four-mechanism statement above);
+ - a [Class1] or [Class2] loser whose slug marks it a vigil ({!vigil_suffix})
+ is [Omit] when the winner is any Sunday ({!sunday_marker}) or itself
+ [Class1] (RG 33 -- entirely omitted, not merely commemorated);
+ - any other [Class1] loser is [Transfer] (RG 95 -- only I class has the
+ right of translation; this is also what moves All Souls, register
+ line 334, once it loses to an occurring Sunday -- WHERE it lands is
+ {!Rite.t.transfer_target}'s job, not this function's);
+ - everything else is [Commemorate], carrying {!interim_privilege} until
+ Task 9 replaces it with RG 109's real per-day computation.
+
+ Total over every winner/loser pair {!Precedence.resolve} or {!Calendar}
+ can construct: [Vocab_ef.rank] (RG 8) and {!Celebration.status} are both
+ closed variants, so the four cases above exhaust every representable
+ shape -- there is no fifth, "unclassified" case the way {!band} needs
+ one, because this function's own return type has no such slot to fall
+ into by accident. *)
+val disposition :
+ winner:Vocab_ef.rank Precedence.candidate ->
+ loser:Vocab_ef.rank Precedence.candidate ->
+ Precedence.disposition