(* RG 91's Table of Precedence, transcribed by Rite_ef.Precedence_ef.band. Table-driven, one row (hence one Alcotest.test_case) per RG 91 entry, so a misplaced or missing entry names itself in the failure output instead of failing anonymously (docs/research/rules-register.md ยง4). Each row's date is checked against the register to make sure it is not ALSO an instance of some other entry at the same band (the vacuous-test trap this project has caught before -- see the Advent-Ember-day note on entry 18 below). *) module P = Colitur_kernel.Precedence module Cel = Colitur_kernel.Celebration module S = Colitur_kernel.Slug module Col = Colitur_kernel.Colour module D = Colitur_kernel.Date module Sub = Colitur_kernel.Subject module Comp = Colitur_kernel.Computus module T = Rite_ef.Temporal_ef module V = Rite_ef.Vocab_ef module PE = Rite_ef.Precedence_ef let mk y m dd = match D.make ~year:y ~month:m ~day:dd with Ok t -> t | Error e -> failwith e (* [T.season] is the same function Calendar itself would use to build a context, so a row's [season]/[weekday] are exactly what the real engine would compute for that date, not a hand-picked value that might not 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) ?(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 hand-typed slug -- review finding 3: [band]'s Ember/vigil detection reads temporal_ef.ml's slug conventions, and a row that also hand-types the same literal proves nothing if that convention ever drifts (both sides would drift together, silently). Rows built with this instead fail loudly on such a drift, because they source the slug from the same place [band] itself is implicitly trusting. *) let of_temporal date = let day = T.temporal date in { P.cel = day.Colitur_kernel.Temporal.office; origin = P.Temporal } (* Every Easter-relative date below is anchored to this single computed Easter rather than a hand-typed calendar date, so an arithmetic slip in a test date cannot silently pass by accident. *) let easter = Comp.gregorian_easter 2026 let off n = D.add_days easter n (* (description, date, candidate, expected RG 91 entry). *) let cases = [ (* Entry 1 -- register line 327: Nativity, Easter Sunday, Pentecost Sunday. *) ("1 Nativity", mk 2026 12 25, cand "ef-nativity", 1); ("1 Easter Sunday", off 0, cand "ef-easter-sunday", 1); ("1 Pentecost Sunday", off 49, cand "ef-pentecost", 1); (* Entry 2 -- register line 328: Sacred Triduum. Thu-Sat of Holy Week, NOT entry 7 (which stops at Wednesday -- see entry 7 below). *) ("2 Holy Thursday", off (-3), cand "ef-holy-thursday", 2); ("2 Good Friday", off (-2), cand "ef-good-friday", 2); ("2 Holy Saturday", off (-1), cand "ef-holy-saturday", 2); (* Entry 3 -- register line 329. *) ("3 Epiphany", mk 2026 1 6, cand "ef-epiphany", 3); ("3 Ascension", off 39, cand "ef-ascension", 3); ("3 Trinity", off 56, cand "ef-trinity", 3); ("3 Corpus Christi", off 60, cand "ef-corpus-christi", 3); ("3 Sacred Heart", off 68, cand "ef-sacred-heart", 3); ("3 Christ the King", T.christ_the_king 2026, cand "ef-christ-the-king", 3); (* Entry 4 -- register line 330. Sanctoral-origin: neither feast is part of temporal_ef's movable cycle. *) ( "4 Immaculate Conception", mk 2026 12 8, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-immaculate-conception", 4 ); ("4 Assumption", mk 2026 8 15, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-assumption", 4); (* Entry 5 -- register line 331. *) ("5 Nativity Vigil", mk 2026 12 24, cand "ef-nativity-vigil", 5); ("5 Octave day (Circumcision)", mk 2026 1 1, cand "ef-circumcision", 5); (* Entry 6 -- register line 332. *) ("6 Advent Sunday", T.advent_start 2026, cand "ef-advent-sunday-1", 6); ("6 Lent Sunday", off (-42), cand "ef-lent-sunday-1", 6); ("6 Passion Sunday (I Passiontide)", off (-14), cand "ef-passion-sunday", 6); ("6 Palm Sunday (II Passiontide)", off (-7), cand "ef-palm-sunday", 6); ("6 Low Sunday", off 7, cand "ef-low-sunday", 6); (* Entry 7 -- register line 333: Ash Wednesday and Mon/Tue/Wed of Holy Week ONLY -- Thu-Sat are entry 2 above, not this entry. *) ("7 Ash Wednesday", off (-46), cand "ef-ash-wednesday", 7); ("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 7); ("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 7); ("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 7); (* Entry 8 -- register line 334. 2 Nov 2026 is a Monday (verified independently below the table), so this row is the plain case. The register's own qualifying case -- "yields to an occurring Sunday" -- gets its own row and its own end-to-end test after this table (2 Nov 2025 is a real Sunday). *) ("8 All Souls (non-Sunday)", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 8); (* Entry 8's qualifier: "(yields to an occurring Sunday)". 2 Nov 2025 is a Sunday, so this must NOT be 8 -- it must lose to entry 15 (16 = entry 15's own value + 1, the exact value precedence_ef.ml documents and justifies at entry 8's branch). The end-to-end resolve-level proof that the Sunday actually wins the day is [test_all_souls_yields_to_sunday] below; this row pins the specific integer [band] returns. *) ("8 All Souls (yields to a Sunday, 2 Nov 2025)", mk 2025 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 16); (* Entry 9 -- register line 335. *) ("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 9); (* Entry 10 -- register line 336: both range boundaries, to guard the off-by-one an inclusive Easter-offset window invites. *) ("10 Easter octave, day+1", off 1, cand "ef-easter-1-mon", 10); ("10 Easter octave, day+6", off 6, cand "ef-easter-1-sat", 10); ("10 Pentecost octave, day+50", off 50, cand "ef-pentecost-1-mon", 10); ("10 Pentecost octave, day+55", off 55, cand "ef-pentecost-1-sat", 10); (* Entry 11 -- register line 337. *) ( "11 Universal I-class feast", mk 2026 6 29, cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-ss-peter-paul", 11 ); (* Entry 12 -- register line 338. The one non-base-layer case the brief asks for explicitly: same date/rank/subject as 11, only the layer differs, so this row isolates the layer test as the deciding factor. *) ( "12 Proper I-class feast (non-base layer)", mk 2026 6 29, cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:"diocese-warsaw" "ef-local-patron", 12 ); (* Entry 13 -- register line 339. *) ( "13 Indult I-class feast", mk 2026 6 29, cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:(PE.indult_prefix ^ "local-grant") "ef-indult-feast-1", 13 ); (* Entry 14 -- register line 341, deliberately UNQUALIFIED (contrast entry 16, line 342, which explicitly says "not of the Lord"). *) ( "14 Feast of the Lord, II class", mk 2026 7 1, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "ef-precious-blood", 14 ); (* Entry 14, non-base layer: unlike 11-13/16/19/20/23/24, entry 14 draws no universal/proper/indult line at all, so this must STILL be 14, not 19 -- the exact restriction review finding 2 flagged and this row exists to keep from silently coming back. *) ( "14 Feast of the Lord, II class (non-base layer)", mk 2026 7 2, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:"diocese-warsaw" "ef-local-feast-of-the-lord", 14 ); (* Entry 15 -- register line 342: an ordinary Sunday not named at entry 6 -- Septuagesima is II class (RG 11-12 names only Advent/Lent/ Passiontide/Easter/Low/Pentecost as I class). *) ("15 II-class Sunday (Septuagesima)", off (-63), cand ~rank:V.Class2 "ef-septuagesima-sunday", 15); (* Entry 16 -- register line 342. *) ( "16 Universal II-class feast, not of the Lord", mk 2026 1 20, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:PE.universal_layer "ef-some-saint", 16 ); (* Entry 17 -- register line 343: days WITHIN the Nativity octave (26-28 Dec are Stephen/John/Innocents -- sanctoral, not this entry; 1 Jan is entry 5's Octave DAY, not this entry either). *) ("17 Nativity octave, 29 Dec", mk 2026 12 29, cand ~rank:V.Class2 "ef-nativity-octave-day-5", 17); ("17 Nativity octave, 31 Dec", mk 2026 12 31, cand ~rank:V.Class2 "ef-nativity-octave-day-7", 17); (* Entry 18 -- register line 343-344: Advent 17-23 Dec ferias AND the Ember days of Advent/Lent/September share this one entry. The second row is deliberately a Lent date (season Lent, NOT Advent) to prove the Ember-slug path fires on its own, not merely because it also happens to fall in the Dec 17-23 window -- the exact trap the brief warns about, worked the other way round: this Ember day must NOT be mistaken for an ordinary entry-22 Lent feria either. *) ("18 Advent 17-23 Dec feria", mk 2026 12 21, cand ~rank:V.Class2 "ef-advent-4-mon", 18); (* Sourced from Temporal_ef.temporal's own output (see [of_temporal]) rather than a hand-typed "ef-lent-ember-wed" -- closes review finding 3's coupling concern for the Ember prefixes specifically. *) ("18 Lent Ember Wednesday (from Temporal_ef.temporal)", off (-39), of_temporal (off (-39)), 18); (* Entry 19 -- register line 344. *) ( "19 Proper II-class feast", mk 2026 1 20, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:"diocese-warsaw" "ef-local-saint-2", 19 ); (* Entry 20 -- register line 345. *) ( "20 Indult II-class feast", mk 2026 1 20, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:(PE.indult_prefix ^ "local-grant-2") "ef-indult-feast-2", 20 ); (* Entry 21 -- register line 345 (RG 28-34). Two rows: the Ascension Vigil is the one II-class vigil temporal_ef already produces today (temporal-origin); the Assumption Vigil stands in for the sanctoral-origin case no task has loaded data for yet -- proving [band] does not gate this entry on [origin] (see precedence_ef.ml's file comment). *) (* Sourced from Temporal_ef.temporal's own output (see [of_temporal]) rather than a hand-typed "ef-ascension-vigil" -- closes review finding 3's coupling concern for [vigil_suffix]. *) ("21 Ascension Vigil (from Temporal_ef.temporal)", off 38, of_temporal (off 38), 21); ( "21 Assumption Vigil (sanctoral-origin)", mk 2026 8 14, cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-assumption-vigil", 21 ); (* Also review finding 3 / "worth doing": a UNIVERSAL-layer Class2 vigil whose subject is the Lord must still be 21, not 14 -- pins entry 14's [not is_vigil] guard even after finding 2 dropped its layer test. *) ( "21 Universal II-class vigil of the Lord", mk 2026 6 23, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "ef-precious-blood-vigil", 21 ); (* Entry 22 -- register line 347-348 (corrected: ends at Palm Sunday, not Passion Sunday). Both a Lent and a Passiontide feria, clear of Ash Wednesday, Holy Week and the Ember days. *) ("22 Lent feria", off (-41), cand ~rank:V.Class3 "ef-lent-1-mon", 22); ("22 Passiontide feria", off (-12), cand ~rank:V.Class3 "ef-passiontide-1-tue", 22); (* Entry 23 -- register line 349. NOTE the table's own order here is the REVERSE of 11/12 and 14/16/19/20 above: entry 23 (particular calendars) is numbered BELOW entry 24 (universal), so a proper III-class feast outranks a universal one -- transcribed as the register states it, not "corrected" to match the other classes. *) ( "23 Proper III-class feast (non-base layer)", mk 2026 6 30, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-saint-3", 23 ); (* Entry 24 -- register line 349. *) ( "24 Universal III-class feast", mk 2026 6 30, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3", 24 ); (* Entry 25 -- register line 350. *) ("25 Advent feria to 16 Dec", mk 2026 12 1, cand ~rank:V.Class3 "ef-advent-1-tue", 25); (* Entry 26 -- register line 350. *) ( "26 III-class vigil", mk 2026 8 9, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil", 26 ); (* Also worth doing: a NON-universal-layer Class3 vigil must still be 26, not 23 -- pins entry 23's [not is_vigil] guard. *) ( "26 III-class vigil (non-base layer)", mk 2026 8 10, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-patron-vigil", 26 ); (* Entry 27 -- register line 352: an otherwise-unoccupied IV-class Saturday. *) ( "27 Office of the BVM on Saturday", off 62, cand ~rank:V.Class4 "ef-time-after-pentecost-1-sat", 27 ); (* Entry 28 -- register line 352: the unqualified IV-class catch-all. *) ("28 IV-class feria", off 65, cand ~rank:V.Class4 "ef-time-after-pentecost-1-tue", 28); (* Not an RG 91 row at all: a I-class candidate marked as a vigil, which is not the Nativity or Pentecost (entries 5/9, the only I-class vigils the table names) and so has no entry to fall into. Proves the documented fallback -- not entry 11/12/13, which the [not is_vigil] guard exists specifically to keep this out of. *) ( "unclassified: I-class vigil outside Nativity/Pentecost", mk 2026 3 10, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-mystery-vigil", PE.unclassified ); (* Also worth doing: a temporal-origin Class1 candidate on a date none of entries 1/2/3/5/6/7/9/10 name. 15 Jul 2026 is a Wednesday, off=101 from Easter -- clear of every Easter-relative window this module checks, and not one of the fixed dates either. Without the [not is_temporal] guard on entries 11-13, this would wrongly reach 12 (its default layer, "temporal", is not [universal_layer] and does not carry [indult_prefix], so it reads as "proper" by the layer test alone -- precisely the bug the guard exists to prevent; see the [not is_temporal] guard's role in the entry-25 mutation test recorded in the task report). *) ("unclassified: I-class temporal candidate on an unnamed date", mk 2026 7 15, cand "ef-unnamed-day", PE.unclassified); (* RG 91's own vigil list (register lines 381-384) stops at III class -- there is no IV-class vigil for entry 28's ferial catch-all to absorb. *) ( "unclassified: IV-class candidate marked as a vigil", mk 2026 6 20, cand ~rank:V.Class4 "ef-second-mystery-vigil", PE.unclassified ) ] (* Review finding 1's end-to-end proof: on a real Sunday landing on 2 November, [Precedence.resolve] -- not just [band] in isolation -- observes the Sunday, not All Souls. This exercises the exact mechanism the finding named ("resolve observes the lowest band, so whenever 2 November falls on a Sunday, All Souls wins and the Sunday loses"), rather than only the integer [band] returns for the standalone row above. [disposition] and [admit] are stubs -- only [observed] is under test here. *) let test_all_souls_yields_to_sunday () = 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 = (fun c cd -> PE.band c cd); disposition = (fun ~winner:_ ~loser:_ -> P.Omit); admit = (fun ~observed:_ cs -> cs) } in let resolution = P.resolve rules day_ctx ~temporal:sunday ~sanctoral:[ all_souls ] in Alcotest.(check string) "the Sunday is observed, not All Souls" "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--sunday-" 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 (fun (desc, date, c, expect) -> Alcotest.test_case desc `Quick (fun () -> 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 ] @ 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 ] )