summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-25 19:47:52 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-25 19:47:52 +0200
commitebbf88dae0110e9cb00935861fae1857107260cc (patch)
tree42066cd9009f0ae486322e474389ff26c1edcbed
parentfc88c6de225592d8700fe1eb40d870ec8863b57d (diff)
downloadcolitur-ebbf88dae0110e9cb00935861fae1857107260cc.tar.gz
colitur-ebbf88dae0110e9cb00935861fae1857107260cc.zip
feat(of): disposition per Normae n. 60
An impeded solemnity transfers; every other loser is omitted for that year ('Reliquae celebrationes eo anno omittuntur'). Commemorate is never returned, and that is the rule rather than a gap: the OF has no commemoration of a losing celebration in the EF's sense, and an optional memorial is an election rather than a commemoration. A property over every rank pairing asserts it, so a later edit cannot quietly introduce one.
-rw-r--r--lib/rites/rite_of/precedence_of.ml32
-rw-r--r--lib/rites/rite_of/precedence_of.mli18
-rw-r--r--test/test_precedence_of.ml58
3 files changed, 107 insertions, 1 deletions
diff --git a/lib/rites/rite_of/precedence_of.ml b/lib/rites/rite_of/precedence_of.ml
index cb15d68..5ceaa11 100644
--- a/lib/rites/rite_of/precedence_of.ml
+++ b/lib/rites/rite_of/precedence_of.ml
@@ -168,3 +168,35 @@ let band (ctx : Vocab_of.season Precedence.context)
arithmetic. *)
130
else unclassified
+
+(* Normae n. 60: "Attamen sollemnitas, quae impeditur a die liturgico, qui
+ praecedentia gaudeat, ad proximiorem diem transferatur... Reliquae
+ celebrationes eo anno omittuntur." An impeded SOLEMNITY is transferred;
+ every other losing celebration is simply omitted for that year.
+
+ [Commemorate] IS NEVER RETURNED, AND THAT IS THE RULE, NOT A GAP. The OF
+ has no commemoration of a losing celebration in the EF's sense at all --
+ contrast RG 108-111, which the EF module implements at length. An optional
+ memorial (Tabula III.12) is an ELECTION the celebrant may make, not a
+ commemoration attached to another day's office, and must not be modelled as
+ one: doing so would put a celebration in Liturgical_day.commemorations that
+ no rubric ever puts there. Design 2026-08-24 ยง3 ("What will not survive
+ contact") predicted exactly this; this function is where the prediction is
+ kept.
+
+ [Repose] is likewise never returned. It is EF vocabulary that the EF module
+ itself emits from nothing.
+
+ [winner] is deliberately unread. n. 60 keys the loser's fate on the LOSER's
+ own grade alone -- unlike the EF's RG 92-95, where the winner's class
+ decides whether a commemoration survives. Kept in the signature because
+ Precedence.rules.disposition requires it, and a future proper-calendar rule
+ might read it. *)
+let disposition ~(winner : Vocab_of.rank Precedence.candidate)
+ ~(loser : Vocab_of.rank Precedence.candidate) : Precedence.disposition =
+ ignore winner;
+ match loser.cel.rank with
+ | Vocab_of.Sollemnitas -> Precedence.Transfer
+ | Vocab_of.Festum | Vocab_of.Memoria_obligatoria | Vocab_of.Memoria_ad_libitum
+ | Vocab_of.Feria ->
+ Precedence.Omit
diff --git a/lib/rites/rite_of/precedence_of.mli b/lib/rites/rite_of/precedence_of.mli
index dfee504..be4713d 100644
--- a/lib/rites/rite_of/precedence_of.mli
+++ b/lib/rites/rite_of/precedence_of.mli
@@ -38,3 +38,21 @@ val proper_prefix : string
same reason: a future sub-rank fits between neighbours without rescaling
the table. *)
val band : Vocab_of.season Precedence.context -> Vocab_of.rank Precedence.candidate -> int
+
+(** [disposition ~winner ~loser]: Normae n. 60. An impeded SOLEMNITY is
+ {!Colitur_kernel.Precedence.Transfer}red; every other losing celebration is
+ {!Colitur_kernel.Precedence.Omit}ted -- "Reliquae celebrationes eo anno
+ omittuntur".
+
+ NEVER RETURNS [Commemorate], AND THAT IS THE RULE RATHER THAN A GAP: the OF
+ has no commemoration of a losing celebration in the EF's sense. An optional
+ memorial is an election, not a commemoration. Consequently
+ {!Colitur_kernel.Liturgical_day.t.commemorations} is permanently empty for
+ this rite, and {!Colitur_kernel.Precedence.privilege} is meaningless here.
+
+ [winner] is unread: n. 60 keys the outcome on the loser's own grade alone,
+ unlike the EF's RG 92-95. *)
+val disposition :
+ winner:Vocab_of.rank Precedence.candidate ->
+ loser:Vocab_of.rank Precedence.candidate ->
+ Precedence.disposition
diff --git a/test/test_precedence_of.ml b/test/test_precedence_of.ml
index 64b182c..b947d11 100644
--- a/test/test_precedence_of.ml
+++ b/test/test_precedence_of.ml
@@ -262,10 +262,66 @@ let test_exhaustive_band_total () =
Alcotest.skip ()
else for y = 1583 to 9999 do check_year y done
+let disposition_cases =
+ [ ( "a losing solemnity transfers (Normae n. 60)",
+ mk ~slug:"of-advent-sunday-2" ~rank:Vocab_of.Sollemnitas ~origin:Precedence.Temporal (),
+ mk ~slug:"of-immaculate-conception" ~rank:Vocab_of.Sollemnitas ~origin:Precedence.Sanctoral (),
+ Precedence.Transfer );
+ ( "a losing feast is omitted, never commemorated",
+ mk ~slug:"of-lent-sunday-3" ~rank:Vocab_of.Sollemnitas ~origin:Precedence.Temporal (),
+ mk ~slug:"of-mark" ~rank:Vocab_of.Festum ~origin:Precedence.Sanctoral (),
+ Precedence.Omit );
+ ( "a losing obligatory memorial is omitted",
+ mk ~slug:"of-lent-2-monday" ~rank:Vocab_of.Feria ~origin:Precedence.Temporal (),
+ mk ~slug:"of-agnes" ~rank:Vocab_of.Memoria_obligatoria ~origin:Precedence.Sanctoral (),
+ Precedence.Omit );
+ ( "a losing optional memorial is omitted",
+ mk ~slug:"of-ordinary-time-15-tuesday" ~rank:Vocab_of.Feria ~origin:Precedence.Temporal (),
+ mk ~slug:"of-hilary" ~rank:Vocab_of.Memoria_ad_libitum ~origin:Precedence.Sanctoral (),
+ Precedence.Omit );
+ ( "a losing feria is omitted",
+ mk ~slug:"of-peter-and-paul" ~rank:Vocab_of.Sollemnitas ~origin:Precedence.Sanctoral (),
+ mk ~slug:"of-ordinary-time-13-monday" ~rank:Vocab_of.Feria ~origin:Precedence.Temporal (),
+ Precedence.Omit ) ]
+
+let test_disposition () =
+ List.iter
+ (fun (label, winner, loser, expected) ->
+ Alcotest.(check string) label
+ (Precedence.sexp_of_disposition expected |> Sexplib0.Sexp.to_string)
+ (Rite_of.Precedence_of.disposition ~winner ~loser
+ |> Precedence.sexp_of_disposition |> Sexplib0.Sexp.to_string))
+ disposition_cases
+
+(* The rule stated as a property rather than as five rows: across every
+ rank/rank pairing, disposition returns Transfer exactly when the loser is a
+ solemnity, and Omit otherwise -- never Commemorate, never Repose. *)
+let test_disposition_never_commemorates () =
+ List.iter
+ (fun lr ->
+ List.iter
+ (fun wr ->
+ let winner = mk ~slug:"of-winner" ~rank:wr ~origin:Precedence.Temporal () in
+ let loser = mk ~slug:"of-loser" ~rank:lr ~origin:Precedence.Sanctoral () in
+ match Rite_of.Precedence_of.disposition ~winner ~loser with
+ | Precedence.Transfer ->
+ Alcotest.(check bool) "only a solemnity transfers" true
+ (lr = Vocab_of.Sollemnitas)
+ | Precedence.Omit ->
+ Alcotest.(check bool) "a solemnity never merely omits" false
+ (lr = Vocab_of.Sollemnitas)
+ | Precedence.Commemorate _ ->
+ Alcotest.fail "OF has no commemorations (Normae n. 60)"
+ | Precedence.Repose -> Alcotest.fail "OF never reposes a celebration")
+ Vocab_of.ranks)
+ Vocab_of.ranks
+
let suite =
( "precedence-of",
[ Alcotest.test_case "Tabula part I (entries 1-4)" `Quick test_part1;
Alcotest.test_case "Tabula part II (entries 5-9)" `Quick test_part2;
Alcotest.test_case "Tabula part III (entries 10-13)" `Quick test_part3;
- Alcotest.test_case "band is total over the domain" `Slow test_exhaustive_band_total ]
+ Alcotest.test_case "band is total over the domain" `Slow test_exhaustive_band_total;
+ Alcotest.test_case "disposition (Normae n. 60)" `Quick test_disposition;
+ Alcotest.test_case "disposition never commemorates" `Quick test_disposition_never_commemorates ]
@ List.map QCheck_alcotest.to_alcotest [ prop_band_total ] )