diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-11 22:46:30 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-11 22:46:30 +0200 |
| commit | 07c87370d6a0de687b42a41d967135578f014bdc (patch) | |
| tree | 841b29952515e9fd662bc46c0b8457303fb2300d /test/test_precedence_ef.ml | |
| parent | 553dc44d2ba0e131e7f2ac79dc755641afcd6a1c (diff) | |
| download | colitur-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.ml | 148 |
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 ] ) |
