summaryrefslogtreecommitdiff
path: root/test/test_precedence_ef.ml
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 /test/test_precedence_ef.ml
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 'test/test_precedence_ef.ml')
-rw-r--r--test/test_precedence_ef.ml148
1 files changed, 144 insertions, 4 deletions
diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml
index 7033978..3b2d5f6 100644
--- a/test/test_precedence_ef.ml
+++ b/test/test_precedence_ef.ml
@@ -25,9 +25,9 @@ let mk y m dd = match D.make ~year:y ~month:m ~day:dd with Ok t -> t | Error e -
actually occur together with it. *)
let ctx date = { P.date; season = T.season date; weekday = D.weekday date }
-let cand ?(origin = P.Temporal) ?(rank = V.Class1) ?(subject = Sub.Temporal) ?(layer = "temporal")
- slug =
- { P.cel = Cel.make ~slug:(S.of_string_exn slug) ~rank ~colour:Col.White ~subject ~layer ();
+let cand ?(origin = P.Temporal) ?(rank = V.Class1) ?(status = Cel.Feast) ?(subject = Sub.Temporal)
+ ?(layer = "temporal") slug =
+ { P.cel = Cel.make ~slug:(S.of_string_exn slug) ~rank ~status ~colour:Col.White ~subject ~layer ();
origin }
(* A candidate built from [Temporal_ef.temporal]'s own real output, not a
@@ -281,6 +281,136 @@ let test_all_souls_yields_to_sunday () =
"ef-time-after-pentecost-sunday-x"
(S.to_string resolution.P.observed.P.cel.Cel.slug)
+(* Task 8: [disposition] -- what happens to the day's LOSING candidate (RG
+ 92-95, 33, 94; register lines 316-325, 381-384). Table-driven like [band]'s
+ own [cases] above, one row per rule, each checked against a description of
+ which register clause it pins. [disposition] takes no context (see
+ precedence.mli's [rules.disposition]), so "is the winner a Sunday" is read
+ off the winner's own slug the same way [band] itself reads "is this a
+ vigil" off the loser's -- see precedence_ef.ml's [sunday_marker]. *)
+
+let string_of_disposition = function
+ | P.Omit -> "Omit"
+ | P.Commemorate P.Privileged -> "Commemorate(Privileged)"
+ | P.Commemorate P.Ordinary -> "Commemorate(Ordinary)"
+ | P.Transfer -> "Transfer"
+ | P.Repose -> "Repose"
+
+(* A II-class ordinary Sunday, built the same way [test_all_souls_yields_to_sunday]
+ builds its Sunday -- a hand-typed slug matching temporal_ef.ml's own
+ "ef-<season>-sunday-<n>" convention, since [disposition] only ever reads
+ this string, never the real computed date. *)
+let an_ordinary_sunday =
+ cand ~rank:V.Class2 "ef-time-after-pentecost-sunday-11"
+
+let disposition_cases =
+ [ (* RG 95 -- register line 323-325: only I-class feasts transfer; a
+ II-class feast loses to a I-class day and is COMMEMORATED, not
+ transferred. Paired with the next row (a I-class loser, same shape of
+ winner) so the discriminating factor is provably the LOSER's own
+ rank, not the winner's -- the brief's explicit "one without the other
+ proves nothing" pairing. *)
+ ( "RG95 II-class feast loses to I-class day -> Commemorate",
+ cand "ef-nativity",
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-saint",
+ "Commemorate(Ordinary)" );
+ ( "RG95 I-class feast loses to a higher I-class day -> Transfer",
+ cand "ef-nativity",
+ cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-local-i-class-feast",
+ "Transfer" );
+ (* RG 33 -- register line 383-384: a I/II-class vigil impeded by any
+ Sunday or a I-class feast is entirely OMITTED, not commemorated. The
+ vigil is sourced from [Temporal_ef.temporal]'s own real output (as
+ [of_temporal]'s existing callers above do), not a hand-typed
+ "ef-ascension-vigil", so a drift in temporal_ef's vigil-slug
+ convention cannot silently defeat this row the way a duplicated
+ literal could. This is the row the brief singles out as most likely
+ to pass vacuously if the fallback below happened to already be
+ [Omit] -- it is not: the fallback is [Commemorate] (see the next two
+ rows), so this genuinely exercises RG 33's own branch. *)
+ ( "RG33 II-class vigil loses to an ordinary Sunday -> Omit",
+ an_ordinary_sunday,
+ of_temporal (off 38) (* Ascension Vigil *),
+ "Omit" );
+ ( "RG33 II-class vigil loses to a I-class feast (non-Sunday) -> Omit",
+ cand "ef-immaculate-conception",
+ of_temporal (off 38),
+ "Omit" );
+ (* RG 33's own boundary, proved from both sides so the rule is shown to
+ gate on the WINNER too, not "any vigil is always omitted": *)
+ ( "RG33 boundary: vigil loses to an ordinary (non-Sunday, non-I-class) \
+ II-class day -> Commemorate, NOT Omit",
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-other-feast",
+ of_temporal (off 38),
+ "Commemorate(Ordinary)" );
+ ( "RG33 boundary: a III-class vigil (outside RG33's I/II-class scope) \
+ loses to a Sunday -> Commemorate, NOT Omit",
+ an_ordinary_sunday,
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil",
+ "Commemorate(Ordinary)" );
+ (* Brief: a Commemoration_only loser is ALWAYS Commemorate -- checked
+ here with a loser that ALSO carries a Class1 rank and a vigil-suffixed
+ slug losing to a Sunday, so this row only passes if the
+ Commemoration_only check is checked BEFORE both RG 33's omission and
+ RG 95's transfer, not after. *)
+ ( "Commemoration_only loser is always Commemorate, even if I-class and \
+ vigil-shaped, even losing to a Sunday",
+ an_ordinary_sunday,
+ cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~layer:PE.universal_layer
+ "ef-suppressed-vigil",
+ "Commemorate(Ordinary)" );
+ (* Totality: the lower ranks the RG 33/RG 95 branches never touch still
+ reach the RG 95 "commemorated or omitted" branch, not an
+ unhandled/exceptional case. *)
+ ( "III-class feast loses to a I-class day -> Commemorate",
+ cand "ef-nativity",
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3",
+ "Commemorate(Ordinary)" );
+ ( "IV-class feria loses to a II-class Sunday -> Commemorate",
+ an_ordinary_sunday,
+ cand ~rank:V.Class4 "ef-time-after-pentecost-1-sat",
+ "Commemorate(Ordinary)" )
+ ]
+
+(* Completes Task 7's carried fix (register line 334): on a real Sunday
+ landing on 2 November, All Souls does not merely lose (that was Task 7's
+ [band] fix, proved by [test_all_souls_yields_to_sunday] above) -- it must
+ be TRANSFERRED, not commemorated and not omitted. All Souls is I class
+ (RG 91 entry 8's own [rank] field, untouched by the Sunday-exception band
+ bump -- see precedence_ef.ml's comment on entry 8), so RG 95's rank
+ condition alone should route it to [Transfer]. *)
+let test_all_souls_disposition_is_transfer () =
+ let sunday = an_ordinary_sunday in
+ let all_souls = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls" in
+ Alcotest.(check string) "All Souls loses to a Sunday and transfers"
+ "Transfer"
+ (string_of_disposition (PE.disposition ~winner:sunday ~loser:all_souls))
+
+(* The same fact, proved end-to-end through [Precedence.resolve] with the
+ REAL [PE.band] and REAL [PE.disposition] wired together (Task 7's own
+ integration test above still stubs [disposition] to a constant [Omit],
+ which is exactly what this task must not leave true) -- All Souls must
+ land in [deferred], not [commemorations] or [omitted]. WHERE it is placed
+ (3 November, RG 96) is [Rite.transfer_target]'s job, out of this task's
+ scope; this only proves [resolve] hands it to the transfer path at all. *)
+let test_all_souls_transfers_end_to_end () =
+ let date = mk 2025 11 2 in
+ let day_ctx = ctx date in
+ let sunday =
+ { P.cel =
+ Cel.make ~slug:(S.of_string_exn "ef-time-after-pentecost-sunday-x") ~rank:V.Class2
+ ~colour:Col.Green ~subject:Sub.Temporal ~layer:"temporal" ();
+ origin = P.Temporal }
+ in
+ let all_souls = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls" in
+ let rules = { P.band = PE.band; disposition = PE.disposition; admit = (fun ~observed:_ cs -> cs) } in
+ let resolution = P.resolve rules day_ctx ~temporal:sunday ~sanctoral:[ all_souls ] in
+ Alcotest.(check (list string)) "All Souls is deferred (transferred), not omitted or commemorated"
+ [ "ef-all-souls" ]
+ (List.map (fun c -> S.to_string c.P.cel.Cel.slug) resolution.P.deferred);
+ Alcotest.(check int) "nothing commemorated" 0 (List.length resolution.P.commemorations);
+ Alcotest.(check int) "nothing omitted" 0 (List.length resolution.P.omitted)
+
let suite =
( "Precedence_ef",
List.map
@@ -289,4 +419,14 @@ let suite =
Alcotest.(check int) desc expect (PE.band (ctx date) c)))
cases
@ [ Alcotest.test_case "8 All Souls yields to a Sunday (resolve-level)" `Quick
- test_all_souls_yields_to_sunday ] )
+ test_all_souls_yields_to_sunday ]
+ @ List.map
+ (fun (desc, winner, loser, expect) ->
+ Alcotest.test_case desc `Quick (fun () ->
+ Alcotest.(check string) desc expect
+ (string_of_disposition (PE.disposition ~winner ~loser))))
+ disposition_cases
+ @ [ Alcotest.test_case "All Souls disposition is Transfer" `Quick
+ test_all_souls_disposition_is_transfer;
+ Alcotest.test_case "All Souls transfers end-to-end (resolve, real rules)" `Quick
+ test_all_souls_transfers_end_to_end ] )