(* 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). *) (* CORRECTED, fix round 1 (coordinator finding 1): every expected [int] below is now the real RG 91 entry number TIMES TEN (entry 1 -> 10, entry 28 -> 280), matching [PE.band]'s own rescale -- see precedence_ef.ml's own [entry_14_fixed_band] comment for why the bare 1..28 scale was a real ordering bug, not a style choice. The description strings still say the bare entry number ("14 Feast of the Lord...") -- that names the RG 91 ROW, not the band integer, and is unaffected by the scale. *) let cases = [ (* Entry 1 -- RG 91 entry 1 (§4): Nativity, Easter Sunday, Pentecost Sunday. *) ("1 Nativity", mk 2026 12 25, cand "ef-nativity", 10); ("1 Easter Sunday", off 0, cand "ef-easter-sunday", 10); ("1 Pentecost Sunday", off 49, cand "ef-pentecost", 10); (* Entry 2 -- RG 91 entry 2 (§4): 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", 20); ("2 Good Friday", off (-2), cand "ef-good-friday", 20); ("2 Holy Saturday", off (-1), cand "ef-holy-saturday", 20); (* Entry 3 -- RG 91 entry 3 (§4). *) ("3 Epiphany", mk 2026 1 6, cand "ef-epiphany", 30); ("3 Ascension", off 39, cand "ef-ascension", 30); ("3 Trinity", off 56, cand "ef-trinity", 30); ("3 Corpus Christi", off 60, cand "ef-corpus-christi", 30); ("3 Sacred Heart", off 68, cand "ef-sacred-heart", 30); ("3 Christ the King", T.christ_the_king 2026, cand "ef-christ-the-king", 30); (* Entry 4 -- RG 91 entry 4 (§4). 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", 40 ); ("4 Assumption", mk 2026 8 15, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-assumption", 40); (* Entry 5 -- RG 91 entry 5 (§4). *) ("5 Nativity Vigil", mk 2026 12 24, cand "ef-nativity-vigil", 50); ("5 Octave day (Circumcision)", mk 2026 1 1, cand "ef-circumcision", 50); (* Entry 6 -- RG 91 entry 6 (§4). *) ("6 Advent Sunday", T.advent_start 2026, cand "ef-advent-sunday-1", 60); ("6 Lent Sunday", off (-42), cand "ef-lent-sunday-1", 60); ("6 Passion Sunday (I Passiontide)", off (-14), cand "ef-passion-sunday", 60); ("6 Palm Sunday (II Passiontide)", off (-7), cand "ef-palm-sunday", 60); ("6 Low Sunday", off 7, cand "ef-low-sunday", 60); (* Entry 7 -- RG 91 entry 7 (§4): 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", 70); ("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 70); ("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 70); ("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 70); (* Entry 8 -- RG 91 entry 8 (§4). 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", 80); (* Entry 8's qualifier: "(yields to an occurring Sunday)". 2 Nov 2025 is a Sunday, so this must NOT be 80 -- it must lose to entry 15 (151 = entry 15's own value, 150, + 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", 151); (* Entry 9 -- RG 91 entry 9 (§4). *) ("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 90); (* Entry 10 -- RG 91 entry 10 (§4): 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", 100); ("10 Easter octave, day+6", off 6, cand "ef-easter-1-sat", 100); ("10 Pentecost octave, day+50", off 50, cand "ef-pentecost-1-mon", 100); ("10 Pentecost octave, day+55", off 55, cand "ef-pentecost-1-sat", 100); (* Entry 11 -- RG 91 entry 11 (§4). *) ( "11 Universal I-class feast", mk 2026 6 29, cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-ss-peter-paul", 110 ); (* Entry 12 -- RG 91 entry 12 (§4). 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", 120 ); (* Entry 13 -- RG 91 entry 13 (§4). *) ( "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", 130 ); (* fix round 1 (coordinator finding 1), NEW: a synthetic I-CLASS candidate landing INSIDE Holy Family's own 7-13 January window -- the exact counter-example that found the original [-14] sentinel's inversion (Holy Family used to beat every I-class entry outright, not merely the fixed Baptism it was built to beat). Must still outrank the movable-14 entry below (110 < entry_14_movable_band), proving the fix, not merely the absence of the old bug's OWN witness (13 January). Not live against the shipped universal data (register §6.2) -- this is exactly the kind of I-class PROPER/ INDULT feast an overlay could add, entries 11-13's own [not is_temporal] tests notwithstanding. *) ( "11 SYNTHETIC: a universal I-class feast inside Holy Family's own window \ (fix round 1, coordinator finding 1) must still outrank the movable half", mk 2026 1 11, cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-synthetic-class1-probe", 110 ); (* Entry 14 -- RG 91 entry 14 (§4), deliberately UNQUALIFIED (contrast entry 16, 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", PE.entry_14_fixed_band ); (* 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 [PE.entry_14_fixed_band], 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", PE.entry_14_fixed_band ); (* Entry 14, MOVABLE half -- RG 91 entry 14's own text (§4, scan-verified): "Festa Domini II classis, PRIMUM MOBILIA, DEINDE FIXA" -- movable FIRST, then fixed. Holy Family (RG 17(b), temporal_ef.ml's own [holy_family_sunday]) is the one movable Class2 Lord feast this codebase builds -- sourced from [Temporal_ef.temporal]'s own real output ([of_temporal], the same review-finding-3 discipline entry 18's own Ember row above uses), not a hand-typed slug, so a rename or a dropped [~subject:Lord] on temporal_ef.ml's own side fails this row too, not only test_temporal_ef.ml's. Must be a value STRICTLY BETTER than the fixed half ([PE.entry_14_fixed_band] immediately above, not merely different) -- the two are structurally disjoint by [is_temporal] alone so they can never literally collide on any one candidate, but {!compare_by}'s own kernel-level tie-break (Slug.compare, deliberately NOT rubric-authoritative -- the exact anti-pattern RG 113's own fix already corrected once, this file's own header) is what would decide between them if they returned the SAME band on the one date they can coincide (13 January, when it is itself Holy Family's date) -- "commemoration-of-the-baptism-of-the- lord" sorts before "ef-time-after-epiphany-sunday-1" alphabetically, backwards from RG 91's own stated order. CORRECTED, fix round 1 (coordinator finding 1): [PE.entry_14_movable_band] used to be a negative sentinel, which beat every I-class entry too, not merely entry 14's own fixed half -- see the SYNTHETIC I-class probe row above, and [PE.entry_14_movable_band]'s own comment in precedence_ef.ml, for the fix and the counter-example that found it. *) ( "14 Feast of the Lord, II class, MOVABLE half (Holy Family, RG 17(b))", T.holy_family_sunday 2026, of_temporal (T.holy_family_sunday 2026), PE.entry_14_movable_band ); (* Entry 15 -- RG 91 entry 15 (§4): 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", 150); (* Entry 16 -- RG 91 entry 16 (§4). *) ( "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", 160 ); (* Entry 17 -- RG 91 entry 17 (§4): 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", 170); ("17 Nativity octave, 31 Dec", mk 2026 12 31, cand ~rank:V.Class2 "ef-nativity-octave-day-7", 170); (* Entry 18 -- RG 91 entry 18 (§4): 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", 180); (* 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)), 180); (* Entry 19 -- RG 91 entry 19 (§4). *) ( "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", 190 ); (* Entry 20 -- RG 91 entry 20 (§4). *) ( "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", 200 ); (* Entry 21 -- RG 91 entry 21 (§4, 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), 210); ( "21 Assumption Vigil (sanctoral-origin)", mk 2026 8 14, cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-assumption-vigil", 210 ); (* 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", 210 ); (* Entry 22 -- RG 91 entry 22 (§4) (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", 220); ("22 Passiontide feria", off (-12), cand ~rank:V.Class3 "ef-passiontide-1-tue", 220); (* Entry 23 -- RG 91 entry 23 (§4). 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", 230 ); (* Entry 24 -- RG 91 entry 24 (§4). *) ( "24 Universal III-class feast", mk 2026 6 30, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3", 240 ); (* Entry 25 -- RG 91 entry 25 (§4). *) ("25 Advent feria to 16 Dec", mk 2026 12 1, cand ~rank:V.Class3 "ef-advent-1-tue", 250); (* Entry 26 -- RG 91 entry 26 (§4). *) ( "26 III-class vigil", mk 2026 8 9, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil", 260 ); (* 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", 260 ); (* Task 11, issue (a): the sanctoral bootstrap (data/ef/sanctoral.sexp) names its vigils with lectio's OWN "vigil-of-X" PREFIX convention, not [PE.vigil_suffix]'s "-vigil" SUFFIX every row above uses -- exactly the mismatch Task 7's review predicted. These two rows use the real bootstrapped slugs verbatim (data/ef/sanctoral.sexp: 28 Jun, 9 Aug), proving [band] recognises the prefix convention too: without it, both would misfile at 160/240 (an ordinary feast of the same rank) instead of 210/260. *) ( "21 II-class vigil via the sanctoral data's own \"vigil-of-X\" prefix", mk 2026 6 28, cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "vigil-of-sts-peter-paul", 210 ); ( "26 III-class vigil via the sanctoral data's own \"vigil-of-X\" prefix", mk 2026 8 9, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "vigil-of-st-lawrence", 260 ); (* Entry 27 -- RG 91 entry 27 (§4): 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", 270 ); (* Entry 28 -- RG 91 entry 28 (§4): the unqualified IV-class catch-all. *) ("28 IV-class feria", off 65, cand ~rank:V.Class4 "ef-time-after-pentecost-1-tue", 280); (* 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 (§4, "Vigils / octaves / Rogations / Sunday classes") 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 ); (* Task B fix round 1 (coordinator finding 1): RG 91's table enumerates only "dies liturgici" -- a [Cel.Commemoration_only] candidate is never a row in the table at all, regardless of what rank it would otherwise band to. [Class1]/universal-layer is deliberate: absent the status guard this would band to entry 11, the FIRST, highest- priority real entry any sanctoral candidate can reach, so this proves the guard is checked BEFORE and overrides every rank-keyed branch, not only the low-priority ones an accidental placement might happen to catch. Real-world witness: "maurice-and-companions- martyrs" (data/ef/sanctoral.sexp, 22 September, Class3, Commemoration_only) used to band to entry 24 alongside the genuine Class3 feast "thomas-of-villanova" -- test_oracle.ml's own former M17 allow-list entry, deleted once this fix made the divergence it named disappear. *) ( "unclassified: a Commemoration_only candidate has no row in RG91's \ table at all, even at Class1", mk 2026 5 20, cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~layer:PE.universal_layer "ef-some-commemoration-only", 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:_ ~temporal:_ cs -> List.map (fun (c, p, (_ : int)) -> (c, p)) 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; §4). 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 -- §4, "Occurrence": 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" ); (* Fix round 1 (post-Task-9 review): RG 95 (§4, "Occurrence" and "Transfer/translation") restricts the right of translation to I-class FEASTS -- RG 91's own table lists Sundays as a separate row (entry 6) from feasts (entries 11-13) -- so an impeded I-class Sunday must NOT transfer, unlike the plain I-class feast row above: same [Class1] rank, same kind of winner, the ONLY difference is that this loser's slug carries [PE.sunday_marker]. RG 109(a) (§4) confirms this from the other direction: "of a Sunday" is a privileged commemoration category, which presupposes an impeded Sunday stays put rather than moving to another day the way a feast does. Sourced from [Temporal_ef.temporal]'s own real output (Advent I Sunday 2026, Class1, "ef-advent-sunday-1"), the same coupling-safety reason [of_temporal]'s other callers use it -- this is also a realistic shape: 8 December falls on an Advent Sunday in 2024, 2030 and 2041 (Immaculate Conception, RG 91 entry 4, outranking entry 6), and 24 December falls on Advent IV in 2023, 2028, 2034 and 2045 (the Nativity Vigil, also entry 5 outranking entry 6). *) ( "RG95/RG109(a): an impeded I-class SUNDAY does NOT transfer -- it is \ Commemorated and Privileged", cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-immaculate-conception", of_temporal (T.advent_start 2026), "Commemorate(Privileged)" ); (* RG 33 -- §4, "Vigils / octaves / Rogations / Sunday classes": a II- or III-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": winner is neither a Sunday nor I class, so RG 33's omission does not fire; a vigil, per RG 31's own text ("si impediuntur, commemorantur"), is ALWAYS commemorated once RG 33 does not omit it outright, regardless of RG 109's closed list. CORRECTED comment (final fix wave, item 2b): this row's own comment used to claim it "proves [the RG26 Class4-Omit branch's] own [not (is_vigil ...)] guard" -- that claim does NOT survive under the current [rank = Class4] gate (it may have been true under an earlier predicate). The vigil sourced here ({!of_temporal}, the real Ascension Vigil) is [Class2], not [Class4], so the RG26 branch's own [rank = Class4] test already excludes it on rank alone, with or without the [not (is_vigil ...)] guard -- verified: deleting that guard entirely leaves 257/257 green. This row still genuinely proves RG 31's own "if impeded, commemorated" vigil mandate (a vigil that RG 33 does not omit outright is commemorated, not omitted by some OTHER mechanism), which is real and worth keeping; it just does not discriminate the [not (is_vigil ...)] guard specifically. *) ( "RG33 boundary: vigil loses to an ordinary (non-Sunday, non-I-class) \ II-class day -> Commemorate, NOT Omit (RG31's own vigil mandate)", cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-other-feast", of_temporal (off 38), "Commemorate(Ordinary)" ); (* The GENUINE witness for the RG26 branch's [not (is_vigil ...)] guard (final fix wave, item 2b): no REAL vigil this engine constructs can exercise it, since RG 91 has no IV-class vigil at all (this file's own entry-27/28 comments) -- every real vigil is Class2 or Class3, already excluded by [rank = Class4] alone, the exact shape the row above is. This row is therefore deliberately SYNTHETIC: a temporal-origin loser, [Class4], whose slug ALSO carries {!PE.vigil_suffix} -- a shape RG 91 itself does not describe, but one {!Precedence.resolve} can still be asked to construct (see precedence_ef.ml's own comment on this guard). With the guard present, [is_vigil] excludes it from the RG26 branch, so it falls through to the ordinary [Commemorate] catch-all (RG 31's "if impeded, commemorated" mandate, the same rule the row above exercises for a realistic Class2 vigil). Deleting [not (is_vigil ...)] from that branch turns this row -- and only this row -- red: the branch would then fire on [rank = Class4] alone and return [Omit]. *) ( "SYNTHETIC (final fix wave, item 2b): a temporal-origin Class4 loser \ ALSO vigil-shaped is Commemorate (RG31's vigil mandate via the \ not-is_vigil guard), not Omit", an_ordinary_sunday, cand ~rank:V.Class4 "ef-synthetic-class4-vigil", "Commemorate(Ordinary)" ); (* CORRECTED 2026-08-12 (Task 16, primary-source-verified): the register previously (mis-)transcribed RG 33 as covering only I/II-class vigils, so this row's own title used to read "outside RG33's I/II-class scope" and expect Commemorate. The primary text ("Vigilia II AUT III classis penitus omittitur...") covers II OR III class -- St Lawrence's vigil (III class, RG 32) falling on ANY Sunday ("in dominica quavis") is entirely omitted, exactly like a II-class vigil. See {!PE.is_omissible_vigil}'s own comment for the full primary text and the register correction. *) ( "RG33 (corrected): a III-class vigil loses to a Sunday -> Omit, not \ Commemorate", an_ordinary_sunday, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil", "Omit" ); (* Task 11, issue (a): [disposition]'s own [is_vigil] check (the RG 33 omission test) is a SEPARATE call site from [band]'s -- both read the same private [is_vigil], but each needed its own witness, since a fix to one call site could in principle miss the other. Real bootstrapped slug (data/ef/sanctoral.sexp's "vigil-of-the-assumption", 14 Aug), not a hand-typed one, for the same coupling-safety reason [of_temporal] rows use real data elsewhere in this file. Before the fix this vigil was invisible to [is_vigil] entirely, so it would have fallen through to the ordinary Commemorate branch below instead of Omit -- the exact failure the task brief describes. *) ( "RG33 (prefix convention): a \"vigil-of-X\"-named II-class vigil loses \ to an ordinary Sunday -> Omit", an_ordinary_sunday, cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "vigil-of-the-assumption", "Omit" ); (* 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. Its expected privilege is [Privileged], not [Ordinary]: this loser's [rank] is [Class1] (the default [cand] leaves unless overridden, deliberately kept here for the branch-order proof above), and RG 109(b) (§4, "of a I-class day") makes any [Class1] commemoration privileged regardless of how it reached [Commemorate] -- Task 8's placeholder [interim_privilege] used to hide this (always [Ordinary]); Task 9's real [privilege_of] does not. This row is also this suite's ONLY witness for RG 109(b): a plain [Feast]-status [Class1] loser never reaches [Commemorate] at all (RG 95 sends it to [Transfer] instead, see the row above), so [Commemoration_only] is the only shape that can exercise it here (see the task report). *) ( "Commemoration_only loser is always Commemorate, even if I-class and \ vigil-shaped, even losing to a Sunday -- and RG109(b) makes it \ privileged", an_ordinary_sunday, cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~layer:PE.universal_layer "ef-suppressed-vigil", "Commemorate(Privileged)" ); (* Totality (SANCTORAL side): the lower ranks the RG 33/RG 95/Task-16 branches never touch still reach the RG 95 "commemorated or omitted" branch's [Commemorate] side, not an unhandled/exceptional case -- RG 111(c)/(d) admit an "ordinary" SAINT commemoration freely, with none of RG 109's closed-list restriction the temporal branch below has. *) ( "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)" ); (* Task 16 (primary-source-verified: RG 95 + RG 109's closed list + RG 113 -- see this branch's own comment in precedence_ef.ml for the full three-text argument): an ORDINARY, non-privileged TEMPORAL-cycle loser -- a bog-standard green-season feria of Time after Pentecost, none of RG 109(a)-(f) -- has NO standing to be commemorated at all when impeded; it is entirely omitted, not the "ordinary" commemoration a losing SAINT would get (contrast the SANCTORAL row immediately above, same rank, same kind of winner, opposite [Commemorate]/[Omit] outcome -- the discriminating factor is [origin], nothing else). Before this fix the engine wrongly commemorated the losing feria itself here; confirmed wrong against the missalemeum oracle (Task 16 report): every one of ~190 structurally identical days (an ordinary sanctoral feast impeding an ordinary temporal feria, 2026-2027) shows zero commemorations in an independent published EF calendar. *) ( "TASK16: an ORDINARY temporal feria loses to a II-class Sunday -> \ Omit, not Commemorate (RG109's closed list; contrast the sanctoral \ row above)", an_ordinary_sunday, cand ~rank:V.Class4 "ef-time-after-pentecost-1-sat", "Omit" ); (* Totality's other half: a SANCTORAL loser of the exact same rank as the row above still reaches [Commemorate], proving the branch above is gated on [origin] and not merely on rank -- without this row nothing here would distinguish "temporal losers are omitted" from "IV-class losers are omitted", which would be a much bigger (and wrong) claim. *) ( "TASK16 contrast: a SANCTORAL IV-class loser still reaches \ Commemorate(Ordinary)", an_ordinary_sunday, cand ~origin:P.Sanctoral ~rank:V.Class4 ~layer:PE.universal_layer "ef-some-minor-saint", "Commemorate(Ordinary)" ); (* Final fix wave, item 2(a): the RG 26 gate above is [rank = Class4], and until this row nothing in the suite discriminated that from the REFUTED predicate it replaced, [privilege_of loser = Ordinary] (precedence_ef.ml's own comment on this branch names that exact predicate as the wrong reading it corrects). The two predicates are extensionally identical over every candidate this engine actually constructs -- confirmed by enumerating the whole candidate space over 1583-9999: substituting the old predicate back leaves 257/257 green and produces byte-identical `colitur day` output for every sampled year. No REALISTIC row can tell them apart, so this one is deliberately SYNTHETIC: a temporal-origin loser, [Class3] (not [Class4]), whose slug matches none of RG 109(a)-(f) -- [privilege_of] therefore returns [Ordinary] for it, the SAME privilege value as the Class4 row above. The two gates now diverge: RG 26 ([rank = Class4]) says this loser is NOT a IV-class feria, so it falls through to the ordinary [Commemorate] branch (RG 24/25's mandate for a II/III-class feria); the refuted gate ([privilege_of loser = Ordinary]) tests only the privilege value, which is [Ordinary] here exactly as it is for the Class4 row, so it would wrongly return [Omit]. Reverting the branch's condition to [privilege_of loser = Ordinary] turns this row (and only this row, of the two) red -- see the final-fix-report for the exact Alcotest failure message this produces and its reversion. *) ( "SYNTHETIC (final fix wave, item 2a): a temporal-origin ORDINARY \ Class3 loser is Commemorate under RG26 (rank=Class4 gate), would be \ Omit under the refuted privilege_of=Ordinary gate", an_ordinary_sunday, cand ~rank:V.Class3 "ef-synthetic-ordinary-class3-feria", "Commemorate(Ordinary)" ); (* RG 16(a) (register §6.0, Caput III "De Dominicis"): "festum Domini I aut II classis, in dominica II classis occurrens, ... de dominica, proinde, nulla fit commemoratio" -- a Feast of the Lord, I or II class, impeding a II-class Sunday, leaves the Sunday with NO commemoration at all, unlike every other impeded II-class Sunday (contrast the very first row of this table, and [privilege_cases]'s own (a): both plain [Class1]/other-sanctoral winners still admit the Sunday, [Commemorate(Privileged)]). Two positive rows, I and II class, both real slugs/ranks/subjects this project's own audit confirmed against the calendarium (this task's own report): the Transfiguration (6 Aug, II class, "IN TRANSFIGURATIONE D. N. I. C.") and the Precious Blood (1 Jul, I class, "PRETIOSISSIMI SANGUINIS D. N. I. C."). *) ( "RG16(a): a Feast of the Lord, II class, impedes a II-class Sunday -> \ Omit, no commemoration at all", cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "transfiguration-of-our-lord", an_ordinary_sunday, "Omit" ); ( "RG16(a): a Feast of the Lord, I class, impedes a II-class Sunday -> \ Omit, no commemoration at all", cand ~origin:P.Sanctoral ~rank:V.Class1 ~subject:Sub.Lord ~layer:PE.universal_layer "precious-blood-of-our-lord-jesus-christ", an_ordinary_sunday, "Omit" ); (* Conjunct 1/4, WINNER subject: RG 16(a) names a feast "Domini", not any I-class feast. {!band} entries 11-13 admit any I-class feast ahead of a II-class Sunday with no subject test (unlike entry 14, see that entry's own comment) -- so a real I-class SAINT feast can win the day exactly as a Lord feast would, and must NOT trigger RG 16(a). Real slug/rank/subject from data/ef/sanctoral.sexp (24 June, I class, "NATIVITAS S. IOANNIS BAPTISTAE", subject Saint): 24 June falls on a Time-after-Pentecost Sunday in real years (e.g. 2029), so this is a live, not merely synthetic, shape -- {!band} entries 11-13 rank it ahead of the ordinary Sunday's own entry 15 regardless of subject. Dropping the [subject = Lord] conjunct would wrongly Omit the Sunday here too. *) ( "RG16(a) does NOT fire for a I-class feast of a SAINT (not the Lord) \ impeding a II-class Sunday -- stays Commemorate(Privileged)", cand ~origin:P.Sanctoral ~rank:V.Class1 ~subject:Sub.Saint ~layer:PE.universal_layer "nativity-of-st-john-the-baptist", an_ordinary_sunday, "Commemorate(Privileged)" ); (* Conjunct 2/4, LOSER rank: RG 16(a) names "Dominica II classis", not any Sunday. A I-class Sunday (Advent/Lent/Passiontide, Low Sunday, {!band} entry 6) can never actually reach here as a loser against a Class1-or-2 Lord winner in real banding -- CORRECTED (fix round 1): not because "entry 6's own band value (6) beats every entry such a winner could occupy, 3/11-14" (WRONG: 6 is not lower than 3, and the claim proves the opposite of what it was cited for anyway -- see precedence_ef.ml's own corrected comment on this same conjunct for the full argument). Against entries 11-14 the numeric claim genuinely holds (6 < 11-14); against entry 3 (Epiphany/Ascension/Trinity/ Corpus Christi/Sacred Heart/Christ the King) it is a STRUCTURAL fact instead -- every band-3 celebration is temporal-origin, and [Precedence.resolve] takes exactly one temporal candidate per day, so a band-3 Lord feast IS that date's own single temporal candidate, never a second one contesting a separately-produced Sunday. Either way, this is exactly why the primary text restricts itself to II class -- but [disposition] takes no [context] and cannot itself re-derive that fact, so the rank check is what actually enforces it here. Sourced from [Temporal_ef.temporal]'s own real output (Advent I Sunday 2026, Class1, "ef-advent-sunday-1"), the same row [disposition_cases]'s own RG95/RG109(a) entry above already uses. Dropping the [rank = Class2] conjunct on the loser would wrongly Omit this Sunday's commemoration too. *) ( "RG16(a) does NOT fire for a I-class Sunday, even losing to a Feast of \ the Lord -- stays Commemorate(Privileged)", cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "transfiguration-of-our-lord", of_temporal (T.advent_start 2026), "Commemorate(Privileged)" ); (* Conjunct 3/4, LOSER is a Sunday at all: RG 16(a) protects "de dominica" specifically, not every ordinary office a Feast of the Lord happens to impede. An ordinary sanctoral SAINT losing to the same Lord-subject Class2 winner as the row above must still reach the ordinary RG 95/111(c) commemorate-or-omit flow, not RG 16(a)'s special no-commemoration rule. Dropping [is_sunday_slug] (or its [is_temporal] companion) here would wrongly Omit this ordinary saint's commemoration too. *) ( "RG16(a) does NOT fire for an ordinary (non-Sunday) SAINT losing to a \ Feast of the Lord -- stays Commemorate(Ordinary)", cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "transfiguration-of-our-lord", cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-other-saint", "Commemorate(Ordinary)" ); (* Conjunct 4/4, WINNER rank: RG 16(a) says "I aut II classis", not any class. Every real [subject = Lord] entry this project's own sanctoral audit found is Class1 or Class2 (this task's own report), so this conjunct is not independently reachable on real data -- SYNTHETIC, the same defensive-but-not-load-bearing status this file already gives the RG26 branch's own [not is_vigil] guard above. Without the rank conjunct, [subject = Lord] alone would wrongly Omit this Sunday's commemoration under a hypothetical Class3 "feast of the Lord" too, which RG 16(a)'s own "I aut II classis" text does not cover. *) ( "SYNTHETIC: RG16(a) does NOT fire for a Class3 feast of the Lord \ (outside \"I aut II classis\") impeding a II-class Sunday -- stays \ Commemorate(Privileged)", cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Lord ~layer:PE.universal_layer "ef-synthetic-class3-lord-feast", an_ordinary_sunday, "Commemorate(Privileged)" ); (* PRIMARY authority -- CORRECTED, fix round 1 (coordinator finding 7): RG 95's own second paragraph, present in ALL THREE documents including the electronic transcription: "Si vero duo festa eiusdem Divinae Personae... simul occurrunt, fit de festo, quod in tabella praecedentiae superiorem obtinet locum et aliud omittitur" -- if two feasts of the SAME DIVINE PERSON occur together, the one higher in the table is kept, the OTHER OMITTED -- an occurrence-level rule closer to this exact collision than RG 112(a) below. CORROBORATION, RG 112(a) (docs/research/rules-register.md §4/§6.0, primary text, verified against all three documents): "112. Ad commemorationes et orationes quod attinet, haec insuper serventur: a) Officium, Missa aut commemoratio de aliquo festo vel mysterio unius Divinae Personae excludit commemorationem aut orationem de alio festo vel mysterio eiusdem Divinae personae" -- the Office, Mass or commemoration of some feast or MYSTERY of ONE Divine Person excludes a commemoration or oration of ANOTHER feast or mystery of the SAME Divine Person. Real witness, the ONE date these two candidates can ever coincide: Holy Family (RG 17(b)) as winner, the Commemoration of the Baptism of the Lord (13 January, data/ef/ sanctoral.sexp: Class2, subject Lord, status Feast) as loser, on the seven years 13 January is itself Holy Family's own date (2008, 2013, 2019, 2030, 2036, 2041, 2047). CORROBORATION 2, the Holy Family Mass propers' own more specific rubric, both photographic scans, word for word (real, and settles 13 January on its own regardless of RG 95/112(a)): "Si festum S. Familiae occurrerit die 13 ianuarii, Missa dicitur de festo S. Familiae, SINE COMMEMORATIONE BAPTISMATIS D.N.I.C., et sine commemoratione dominicae" -- if the feast of the Holy Family occurs on 13 January, the Mass is said of the Holy Family, WITHOUT commemoration of the Baptism, and without commemoration of the Sunday (RG 17(b)'s own general rule, restated). CORRECTED, fix round 1 (coordinator finding 2): a previous version of this comment claimed the electronic transcription's absence of this specific rubric was "exactly the transcription's own documented defect" -- WRONG, struck. That transcription (a 2006 web capture) carries almost no Mass-propers text of any kind (5 Introit occurrences in 26,322 lines against 61/402 in the two photographic scans; its own "Proprium de Tempore" page is a table of contents linking to PDFs the capture never pulled in) -- a coverage gap, not a silent drop from content it otherwise has. RG 112 itself, unlike this rubric, IS present in full in the transcription (all four sub-clauses, verbatim) -- checked directly, not assumed. *) ( "RG112(a): a feast/mystery of the Lord excludes a commemoration of \ ANOTHER feast/mystery of the Lord -- Omit, not Commemorate (Holy \ Family vs the Baptism of the Lord, 13 January)", of_temporal (T.holy_family_sunday 2019), cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "commemoration-of-the-baptism-of-the-lord", "Omit" ); (* Conjunct 1/2, LOSER subject: an ORDINARY saint losing to the SAME Lord-subject winner must still reach the ordinary commemorate flow, not RG 112(a)'s exclusion -- RG 112(a) excludes another mystery of the SAME Divine Person, not every commemoration a Lord feast happens to outrank. Dropping the loser's own [subject = Lord] test would wrongly Omit this ordinary saint's commemoration too. *) ( "RG112(a) does NOT fire when only the WINNER is a mystery of the Lord \ -- an ordinary saint stays Commemorate(Ordinary)", of_temporal (T.holy_family_sunday 2019), cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer "ef-synthetic-ordinary-saint", "Commemorate(Ordinary)" ); (* Conjunct 2/2, WINNER subject: a Class2 SAINT'S feast (not of the Lord) impeding an ordinary temporal candidate must not exclude an otherwise-due Lord commemoration either -- RG 112(a) is keyed on the WINNER's own subject too, not just the loser's. Real slug/rank from data/ef/sanctoral.sexp (25 January, Class3, "Conversion of St Paul") standing in for a same-rank saint winner; only the [subject] distinction is under test here, not a real-date collision (Holy Family's own subject is what makes this branch reachable at all -- an ordinary sanctoral saint never wins against Holy Family in practice, {!band}'s [entry_14_movable_band] sees to that, so this row is SYNTHETIC in the same sense the RG16(a) block's own Class3 row above is: defensive, not a live witness). *) ( "SYNTHETIC: RG112(a) does NOT fire when only the LOSER is a mystery \ of the Lord -- stays Commemorate(Ordinary)", cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer "conversion-of-st-paul", cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "commemoration-of-the-baptism-of-the-lord", "Commemorate(Ordinary)" ) ] (* Task 9: [privilege_of]'s RG 109 categories (§4, "Commemorations"), exercised through [PE.disposition]'s [Commemorate] payload -- [privilege_of] itself is private, so this is the only vantage point a test outside precedence_ef.ml has on it. Each row below is built to match ONLY the one category it names (see each row's own comment for why), closing the hazard flagged in the task brief ("a test day that is both a Sunday and a I-class day proves nothing about either"). Category (b), "of a I-class day", already has its sole witness above (the Commemoration_only row): a plain [Feast]-status [Class1] loser can never reach [Commemorate] at all in this ruleset (RG 95 routes it to [Transfer] instead), so no further row for (b) is added here -- see the task report. Category (f), "of the Major Rogations, in Mass", has no row at all: no candidate this codebase can currently construct represents one (see [privilege_of]'s own comment on (f)) -- the negative row below proves the one slug this engine DOES compute that could be mistaken for it (the Minor Rogations) is correctly NOT conflated with it, which is the strongest claim available without inventing an unfounded slug convention. *) let privilege_cases = [ (* (a) RG 109(a) (§4): "of a Sunday". [an_ordinary_sunday] is Class2, not Class1, not within the Nativity octave, not an Ember day, not a feria of Advent/Lent/Passiontide -- matches (a) alone. *) ( "(a) an ordinary Sunday commemoration is privileged", cand "ef-nativity", an_ordinary_sunday, "Commemorate(Privileged)" ); (* (c) RG 109(c) (§4): "of days within the Octave of the Nativity" -- sourced from [Temporal_ef.temporal]'s own output (29 Dec 2026, Class2, "ef-nativity-octave-day-5"), not a hand-typed slug, for the same coupling-safety reason the file's own [of_temporal] rows use it elsewhere. Not a Sunday, not Class1, not an Ember day, not an Advent/Lent/Passiontide feria slug. *) ( "(c) a day within the Nativity octave is privileged", cand "ef-nativity", of_temporal (mk 2026 12 29), "Commemorate(Privileged)" ); (* (d) RG 109(d) (§4): "of September Ember days" -- 23 Sep 2026 is the September Ember Wednesday (independently derived from [Temporal_ef]'s own third-Sunday-of-September rule: first Sunday of September 2026 is the 6th, +14 days = 20th, +3 = 23rd), sourced from [Temporal_ef.temporal] itself, Class2. Not a Sunday, not Class1, not within the Nativity octave, not a plain Advent/Lent/Passiontide feria slug either -- and, unlike the Advent/Lent Ember rows below, its own slug ("ef-september-ember-wed") never starts with any of (e)'s own [alp_feria_prefixes] ("ef-advent-"/"ef-lent-"/"ef-passiontide-"), so (d) is this candidate's ONLY route to [Privileged] -- a genuine, still-necessary distinction from (e), unlike the Advent/Lent Ember case below (fix round 1). *) ( "(d) a September Ember day is privileged", cand "ef-nativity", of_temporal (mk 2026 9 23), "Commemorate(Privileged)" ); (* (e) RG 109(e) (§4): "of ferias of Advent, Lent and Passiontide" -- two rows, one per season named, both from [Temporal_ef.temporal]'s own generic ferial fallback, neither a Sunday, Ember day, or within the Nativity octave. *) ( "(e) an Advent feria is privileged", cand "ef-nativity", of_temporal (mk 2026 12 1), "Commemorate(Privileged)" ); ( "(e) a Lent feria is privileged", cand "ef-nativity", of_temporal (off (-41)), "Commemorate(Privileged)" ); (* CHANGED, fix round 1 (F1/F2/F6): these two rows used to be titled "boundary: ... is NOT privileged (only September is, RG109(d))" and expected [Omit] (a Task-16-pass reading that treated RG 109(e)'s bare "feriis Adventus, Quadragesimae" as tacitly excluding the Advent and Lent Ember sub-days, by analogy with (d)'s own separate, explicit September carve-out). Review round 1 (F1) reproduced the real consequence directly -- 1900-12-21, an Advent Ember Friday, lost its own commemoration entirely, while an ordinary (lower-solemnity, non-Ember) Advent feria the same week kept its commemoration -- backwards on any reading, and traced it to this exact misclassification (F2). Corrected reading (precedence_ef.ml's own [privilege_of], (e) branch, carries the full argument): RG 91's TABLE needs an explicit "exceptis feriis Quatuor Temporum" at its own entries 22 and 25 to keep Ember days from being double-listed against their own entry 18 -- an exception that would be unnecessary drafting if "feriae Adventus"/"feriae Quadragesimae" did not ALREADY include their Ember sub-days by default. RG 109(e) carries no such "exceptis" clause, so its bare text is read at that same default, INCLUSIVE scope: the Advent and Lent Ember ferias ARE privileged under (e), not merely commemorable-but-ordinary. (d)'s own separate existence survives this reading intact -- September Ember days sit outside Advent/Lent/ Passiontide under ANY reading, so (d) remains the ONLY way they reach [Privileged], the point the row immediately above this one makes explicit. 16 Dec 2026 is the Advent Ember Wednesday (independently derived: Advent I 2026 is 29 Nov, +14 days = 13 Dec, +3 = 16 Dec); the Lent Ember Wednesday is the same date [off (-39)] already used by the entry-18 [band] row above. Both sourced from [Temporal_ef.temporal], not hand-typed, for the same coupling-safety reason every [of_temporal] row in this file uses it. *) ( "(e), corrected fix round 1: an Advent Ember day is ALSO privileged, \ not excluded from (e)", cand "ef-nativity", of_temporal (mk 2026 12 16), "Commemorate(Privileged)" ); ( "(e), corrected fix round 1: a Lent Ember day is ALSO privileged, \ not excluded from (e)", cand "ef-nativity", of_temporal (off (-39)), "Commemorate(Privileged)" ); (* Negative, RG 109(f)'s own boundary: the Minor Litanies/Rogations (Monday/Tuesday before Ascension, RG 87 -- [Temporal_ef.temporal] DOES compute these, unlike the Major Litanies RG 109(f) actually names, see [privilege_of]'s own comment) must NOT be mistaken for the Major Rogations RG 109(f) privileges: RG 88 says the Minor Rogations change nothing in the Office at all, so nothing about them is privileged either -- and (Task 16) being temporal+ordinary, a Minor Rogation day impeded by a saint is now omitted outright, matching the missalemeum oracle exactly (Task 16 report: 11 May 2026 and 12 May 2026, both Minor Rogation days impeded by a saint, show zero commemoration of the Rogation in the independent oracle). *) ( "boundary: a Minor Rogation day is NOT privileged (RG109(f) names \ the Major Litanies, not these) -- TASK16 omits it entirely", cand "ef-nativity", of_temporal (off 36), "Omit" ) ] (* Task 9: [PE.admit] -- RG 111's admission counts (§4, "Commemorations"), given commemorations ALREADY tagged with their real privilege (as [PE.disposition] now tags them -- see [privilege_cases] above). Every candidate/privilege pair here is built directly, not routed through [PE.disposition], so these rows isolate [admit]'s own selection logic from [privilege_of]'s classification -- the two are proved separately by design (unlike a test that only proves [admit] admits SOME correct-looking set without knowing whether it or [privilege_of] supplied the "correct" part). Checked on slug IDENTITY, not count (the brief: "'two admitted' proves nothing about *which* two"). *) (* Class2 dignity, tagged [Ordinary] explicitly (not via [privilege_of]) -- used as the higher-dignity, non-privileged half of every asymmetry pair below. *) let ordinary_hi = cand ~rank:V.Class2 "ef-ordinary-hi" (* Class3 dignity (LOWER than [ordinary_hi]), tagged [Privileged] explicitly -- pairing a lower-dignity privileged candidate against a higher-dignity ordinary one is what makes the II-class-Sunday-vs-other-II-class asymmetry observable: pure dignity and "privilege wins the slot" pick DIFFERENT winners from this exact pair. *) let privileged_lo = cand ~rank:V.Class3 "ef-privileged-lo" (* Class2 dignity (tied with [ordinary_hi], distinguishing rank from privilege alone), tagged [Privileged] -- the higher-dignity privileged candidate for the "two privileged due" row. *) let privileged_hi = cand ~rank:V.Class2 "ef-privileged-hi" (* Class4, the lowest dignity in play -- the third candidate for the III/IV-class "at most two" row, so which TWO of three survive is the thing under test, not merely how many. *) let ordinary_lowest = cand ~rank:V.Class4 "ef-ordinary-lowest" (* Class3, tagged [Ordinary] -- fix round 1, F7: the RG 111(b) rank-floor witness [admit_cases] was missing. [ordinary_hi] above is already Class2, so every existing II-class-Sunday row here passes whether or not [admit]'s "de festo II classis" filter is even present -- reverting that filter would only redden [test_oracle.ml], not this file, which is exactly the coverage gap the review round found. This candidate is the ONLY thing due on the Sunday row below, so a version of [admit] without the rank floor would (wrongly) admit it on pure "best available" dignity, same as it would have admitted [ordinary_hi]. *) let ordinary_class3 = cand ~rank:V.Class3 "ef-ordinary-class3" let observed_class1 = cand "ef-nativity" (* Class1 by [cand]'s own default. *) let observed_class2_sunday = an_ordinary_sunday (* Class2, slug carries "-sunday". *) let observed_class2_other = cand ~rank:V.Class2 "ef-other-class2-day" (* Class2, no "-sunday". *) let observed_class3 = cand ~rank:V.Class3 "ef-some-class3-day" let slugs_of admitted = List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) admitted (* [admit_cases] below (Task B, ef-rg16a) needs a precedence-order [int] on every comms triple -- {!Precedence.rules.admit}'s new parameter, RG 113 (docs/research/rules-register.md §4, "RG 113"). This table's own header comment states its purpose: isolate [admit]'s own selection/count logic from {!PE.band}'s occurrence-table classification, so every candidate here is built from a synthetic slug ("ef-ordinary-hi" etc.) that matches none of [band]'s 28 real branches -- calling the REAL [PE.band] on them would collapse every one to {!PE.unclassified} (the same tied value), destroying the table's ability to distinguish rows at all. [order_of_rank] is a TEST-ONLY stand-in, reproducing exactly the relative order the previous [dignity] function gave (RG 8's rank cardinal, lower first) -- so every existing expectation below still holds unchanged; only the tuple shape gained this third field. *) let order_of_rank = function V.Class1 -> 10 | V.Class2 -> 20 | V.Class3 -> 30 | V.Class4 -> 40 let po c p = (c, p, order_of_rank c.P.cel.Cel.rank) (* Real-data shapes for the fix round's own RG16(a)/[admit] interaction (below): a Feast of the Lord (subject Lord, Class2, sanctoral) as [observed], oracle-confirmed real slugs/ranks rather than hand-typed ones, the same coupling-safety reason [of_temporal] rows elsewhere in this file use real data. *) let lord_winner = cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "transfiguration-of-our-lord" (* Pope Sixtus II et al. (6 August, real slug/rank/status from data/ef/sanctoral.sexp): Class3, Commemoration_only, tagged [Ordinary] here directly (as every other [admit_cases] row does, isolating [admit]'s own selection logic from [privilege_of], per this table's own header comment). *) let sixtus = cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~layer:PE.universal_layer "pope-sixtus-ii-felicissimus-and-agapitus-martyrs" (* (description, observed, temporal, comms, expected). [temporal] -- {!Precedence.rules.admit}'s own new parameter, fix round 1 (RG16(a) task): the day's temporal-cycle candidate, independent of who is [observed]. Every row below except the last two repeats [observed] as [temporal] -- no RG16(a) displacement occurs in those rows (the pattern [PE.admit]'s pre-fix code implicitly, and wrongly, assumed held UNIVERSALLY), so the two values coinciding is the faithful shape, not a simplification that dodges the new parameter. The last two rows are where they genuinely differ -- see their own comment. *) let admit_cases = [ (* RG 111 (§4): "I class: none save one privileged." *) ( "I-class day, only an ordinary commemoration due -> none admitted", observed_class1, observed_class1, [ po ordinary_hi P.Ordinary ], [] ); ( "I-class day, ordinary + privileged both due -> only the privileged \ one, regardless of the ordinary one's higher dignity", observed_class1, observed_class1, [ po ordinary_hi P.Ordinary; po privileged_lo P.Privileged ], [ "ef-privileged-lo" ] ); ( "I-class day, two privileged due -> only the higher-dignity one (still \ just \"one\")", observed_class1, observed_class1, [ po privileged_lo P.Privileged; po privileged_hi P.Privileged ], [ "ef-privileged-hi" ] ); (* RG 111: "II-class Sundays: one (dropped if a privileged one is due)." *) ( "II-class Sunday, only an ordinary commemoration due -> it is admitted", observed_class2_sunday, observed_class2_sunday, [ po ordinary_hi P.Ordinary ], [ "ef-ordinary-hi" ] ); ( "II-class Sunday, ordinary (higher dignity) + privileged (lower \ dignity) both due -> the PRIVILEGED one is admitted, the ordinary \ one dropped despite outranking it", observed_class2_sunday, observed_class2_sunday, [ po ordinary_hi P.Ordinary; po privileged_lo P.Privileged ], [ "ef-privileged-lo" ] ); (* RG 111(b)'s own rank floor ("scilicet DE FESTO II CLASSIS"), fix round 1 F7: a Class3 ORDINARY candidate -- no privileged rival due, so the pre-fix-round code's "no privileged? take the best of what's left" fallback would (wrongly) admit it -- is admitted NOTHING. The slot is reserved for a II-class candidate specifically; a III-class ordinary one has no standing for it at all, unlike "other II class" below, which has no such restriction. *) ( "II-class Sunday, sole candidate is an ORDINARY Class3 (not \ \"de festo II classis\") -> admitted nothing, not the best available", observed_class2_sunday, observed_class2_sunday, [ po ordinary_class3 P.Ordinary ], [] ); (* RG 111: "other II class: one" -- no privilege override, the exact asymmetry the brief and precedence_ef.ml's own [admit] comment flag: same candidate pair as the II-class-Sunday row above, OPPOSITE observed day, OPPOSITE winner. *) ( "other II-class day, only an ordinary commemoration due -> it is \ admitted", observed_class2_other, observed_class2_other, [ po ordinary_hi P.Ordinary ], [ "ef-ordinary-hi" ] ); ( "other II-class day, same ordinary+privileged pair as the Sunday row \ above -> the ORDINARY one wins on pure dignity this time, the \ privileged one dropped", observed_class2_other, observed_class2_other, [ po ordinary_hi P.Ordinary; po privileged_lo P.Privileged ], [ "ef-ordinary-hi" ] ); (* RG 111: "III-IV class: at most two" -- three candidates due, top two by dignity admitted, the third (lowest dignity) dropped. *) ( "III-class day, three commemorations due -> the top two by dignity, \ not merely \"two of them\"", observed_class3, observed_class3, [ po ordinary_hi P.Ordinary; po privileged_lo P.Privileged; po ordinary_lowest P.Ordinary ], [ "ef-ordinary-hi"; "ef-privileged-lo" ] ); (* Fix round 1, item 2 (RG16(a) task review): the CRITICAL witness for [~temporal]. RG 16(a)'s own text -- the winning Feast of the Lord holds the Sunday's place "cum omnibus iuribus et privilegiis" -- means the day is STILL a "dominica II classis" for RG 111(b)'s own rank floor, even though [observed] is now the FEAST (Transfiguration, real slug/rank/subject), not the Sunday. [temporal] (a real Sunday candidate, {!an_ordinary_sunday}) is what reveals that; nothing in [observed] does, since its own slug carries no Sunday marker at all. Oracle-confirmed (missalemeum, 2023-08-06, a Sunday): "commemorations": [], Sixtus "displaced". Before this fix [observed_is_sunday] read [observed]'s own slug and got [false] here, wrongly taking the "other II class: one" branch below and admitting Sixtus (Class3) despite RG 111(b)'s rank floor. *) ( "RG16(a) interaction: a Feast of the Lord observed on a day whose \ TEMPORAL candidate is a II-class Sunday -> the RG111(b) rank floor \ still applies, admits nothing (Sixtus, Class3, has no standing)", lord_winner, an_ordinary_sunday, [ po sixtus P.Ordinary ], [] ); (* The control, same pair as the row above with [observed] = [temporal] (no displacement -- an ordinary WEEKDAY, not a Sunday): "other II class: one", no rank floor, Sixtus IS admitted. Oracle-confirmed (missalemeum, 2026-08-06, a Thursday): Sixtus in "commemorations". Proves the DISCRIMINATING factor is [temporal]'s own Sunday-ness, not merely "was the winner a Feast of the Lord" -- that alone is true of BOTH rows here, and only one of them excludes Sixtus. *) ( "control: the same Feast of the Lord on an ORDINARY weekday (temporal \ = observed, not a Sunday) -> \"other II class: one\", Sixtus IS \ admitted", lord_winner, lord_winner, [ po sixtus P.Ordinary ], [ "pope-sixtus-ii-felicissimus-and-agapitus-martyrs" ] ) ] (* Order independence (brief: "the admitted set must not depend on input order"): the SAME three candidates as the III-class row above, passed in the reverse order, must still admit the same top two -- exercised on this row specifically because it is the one where the sort actually has work to do (three distinct dignities, a real top-2 cut), unlike a two-candidate row where either order already happens to be sorted. *) let test_admit_order_independent () = let comms = [ po ordinary_hi P.Ordinary; po privileged_lo P.Privileged; po ordinary_lowest P.Ordinary ] in let forward = slugs_of (PE.admit ~observed:observed_class3 ~temporal:observed_class3 comms) in let reversed = slugs_of (PE.admit ~observed:observed_class3 ~temporal:observed_class3 (List.rev comms)) in Alcotest.(check (list string)) "reversed input admits the same candidates" forward reversed (* RG 113 teeth (Task B, ef-rg16a): two SAME-rank candidates whose slug order DISAGREES with their band/precedence order -- "aa-worse-precedence" sorts first alphabetically but is given the WORSE (higher) precedence int, "zz-better-precedence" sorts last alphabetically but the BETTER (lower) one. Both are [Class3] (same RG 8 "dignity", so a version of [admit] that silently reverted to sorting by [rank] then slug -- the exact pre-fix shape this task replaced -- would pick "aa-worse-precedence" here, the OPPOSITE of what this asserts. A real RG 91 [band] value never actually produces this exact pairing against these two synthetic slugs (see [admit_cases]'s own header on why this table uses synthetic, non-band- classifiable slugs) -- the precedence ints are supplied directly, the same [po]/explicit-order convention this file already uses, standing in for whatever real [band] would compute. This is what proves [admit] actually consults the passed-in precedence value rather than merely accepting one as a matter of new type-checking. *) let order_tiebreak_alpha_first_worse = cand ~rank:V.Class2 "aa-worse-precedence" let order_tiebreak_alpha_last_better = cand ~rank:V.Class2 "zz-better-precedence" (* [observed_class2_other]: "other II class: one" admits exactly ONE, by precedence order alone (no privilege override, no rank floor) -- unlike the III-class row [test_admit_order_independent] reuses, which admits TWO out of two offered here and so cannot distinguish "picked by precedence" from "picked both anyway". *) let test_admit_uses_precedence_not_slug_when_they_disagree () = let comms = [ (order_tiebreak_alpha_first_worse, P.Ordinary, 90); (order_tiebreak_alpha_last_better, P.Ordinary, 10) ] in Alcotest.(check (list string)) "the BETTER-precedence candidate is admitted even though it sorts LAST alphabetically" [ "zz-better-precedence" ] (slugs_of (PE.admit ~observed:observed_class2_other ~temporal:observed_class2_other comms)) (* The brief: "a case proving that what the limit drops is reported in omitted rather than vanishing" -- three end-to-end proofs, wired with the REAL [PE.band], [PE.disposition] and [PE.admit] together (not a stub, so [privilege_of]'s real classification is exercised too, not just [admit]'s selection logic in isolation as above). [rules] deliberately reused, not rebuilt per test, since it is always the same three real functions. *) let real_rules = { P.band = PE.band; disposition = PE.disposition; admit = PE.admit } (* I-class day, zero admitted: the strongest form of "does not vanish" -- EVERY commemoration due is dropped (RG 111: "none save one privileged", and the one loser here is ordinary), yet it must still appear in [omitted], not merely be absent from [commemorations]. *) let test_i_class_day_drops_into_omitted () = let date = mk 2026 12 25 in let day_ctx = ctx date in let nativity = of_temporal date in let saint = cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3" in let resolution = P.resolve real_rules day_ctx ~temporal:nativity ~sanctoral:[ saint ] in Alcotest.(check (list string)) "nothing admitted on a I-class day with only an ordinary loser due" [] (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations); Alcotest.(check (list (pair string string))) "the ordinary loser is reported omitted, not vanished" [ ("ef-some-saint-3", "omitted: admission limit reached") ] (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted) (* II-class Sunday, two ordinary losers due, RG 111's "one" admits the higher-dignity one and drops the other into [omitted]. *) let test_ii_class_sunday_drops_second_loser_into_omitted () = let date = mk 2025 11 9 (* an ordinary Time-after-Pentecost Sunday, not All Souls-adjacent. *) 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 saint_a = cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-saint" in let saint_b = cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3" in let resolution = P.resolve real_rules day_ctx ~temporal:sunday ~sanctoral:[ saint_a; saint_b ] in Alcotest.(check (list string)) "only the higher-dignity (Class2) loser is admitted" [ "ef-some-saint" ] (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations); Alcotest.(check (list (pair string string))) "the lower-dignity loser is reported omitted, not vanished" [ ("ef-some-saint-3", "omitted: admission limit reached") ] (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted) (* A genuinely privileged commemoration reaching [admit] through the REAL pipeline (register RG 109(e)): a Lent feria (Class3, temporal-origin) loses to a universal Class2 sanctoral feast on the same date, and [PE.disposition] tags it [Privileged] via [privilege_of] -- proving [privilege_of] and [admit] cooperate correctly end-to-end, not merely in the hand-tagged unit tests above. *) let test_privileged_lent_feria_admitted_end_to_end () = let date = off (-41) (* Lent I Monday, the same date the entry-22 [band] row uses. *) in let day_ctx = ctx date in let lent_feria = of_temporal date in let saint = cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-saint" in let resolution = P.resolve real_rules day_ctx ~temporal:lent_feria ~sanctoral:[ saint ] in Alcotest.(check string) "the Lent feast wins the day, not the sanctoral feast's own commemoration" "ef-some-saint" (S.to_string resolution.P.observed.P.cel.Cel.slug); Alcotest.(check (list (pair string string))) "the Lent feria is admitted, tagged Privileged" [ ("ef-lent-1-monday", "Privileged") ] (List.map (fun (c, p) -> (S.to_string c.P.cel.Cel.slug, match p with P.Privileged -> "Privileged" | P.Ordinary -> "Ordinary")) resolution.P.commemorations); Alcotest.(check int) "nothing omitted" 0 (List.length resolution.P.omitted) (* Fix round 1 (post-Task-9 review): the II-class-Sunday override (RG 111 "one, dropped if a privileged one is due") reached through the REAL pipeline, with both a privileged AND an ordinary commemoration due on the same day -- previously only proven at the isolated [admit] level ([admit_cases]'s hand-tagged rows above). RG 109(a)-(f)'s five reachable categories are all properties of a TEMPORAL-origin office, and only one temporal candidate exists per date, so a privileged AND an ordinary commemoration cannot both be due from the temporal side alone -- but category (b) is the exception: it is sanctoral and rank-based (a [Commemoration_only] entry carrying [Class1], the exact shape [disposition_cases]'s own (b) witness row uses), and a [Commemoration_only] entry is held out of the band contest entirely ({!Precedence.resolve}), so it is a loser regardless of what [observed] turns out to be. Paired with an ordinary sanctoral saint, both lose to an ordinary Class2 Sunday, giving [observed_is_sunday = true] with one [Privileged] and one [Ordinary] loser due at once -- no synthetic fixture needed. NOTE on what this test does and does not prove: the only reachable witness for RG 109(b) is [Class1] (the highest dignity), and any sanctoral loser that could ALSO beat this same Sunday by pure dignity would win the day outright instead of losing to it (any [Feast]-status [Class1] sanctoral candidate bands at entry 11-13, ahead of an ordinary Sunday's entry 15) -- so within this specific pipeline shape the privileged loser is unavoidably also the higher-dignity one, and this test cannot by itself distinguish "privilege overrides dignity" from "dignity alone happened to pick the same winner". That distinction is what [admit_cases]'s hand-tagged rows above prove (a LOWER-dignity privileged candidate still beats a HIGHER-dignity ordinary one on a II-class Sunday, the opposite of "other II class"'s own row). This test's job is narrower and complementary: proving the real pipeline ([PE.band], [PE.disposition], [PE.privilege_of] via [disposition], [PE.admit] together) actually reaches and exercises the override branch end-to-end, not merely in isolation. *) let test_ii_class_sunday_privileged_witness_admitted_end_to_end () = let date = mk 2026 7 5 (* an ordinary Time-after-Pentecost Sunday. *) in let day_ctx = ctx date in let sunday = of_temporal date in let privileged_witness = cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~layer:PE.universal_layer "ef-commemoration-only-b-witness" (* Class1 by [cand]'s own default -- RG 109(b). *) in let ordinary_saint = cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-some-saint" in let resolution = P.resolve real_rules day_ctx ~temporal:sunday ~sanctoral:[ privileged_witness; ordinary_saint ] in (* Checked against [sunday]'s own slug, not a hand-typed/guessed literal (its exact week number is not worth independently re-deriving here): this asserts identity with the real [Temporal_ef.temporal] candidate, proving the SUNDAY -- not either sanctoral loser -- is what wins the day, which the privilege assertions below presuppose. *) Alcotest.(check string) "the Sunday wins the day, not either sanctoral loser" (S.to_string sunday.P.cel.Cel.slug) (S.to_string resolution.P.observed.P.cel.Cel.slug); Alcotest.(check (list (pair string string))) "only the privileged (Commemoration_only, Class1) witness is admitted" [ ("ef-commemoration-only-b-witness", "Privileged") ] (List.map (fun (c, p) -> (S.to_string c.P.cel.Cel.slug, match p with P.Privileged -> "Privileged" | P.Ordinary -> "Ordinary")) resolution.P.commemorations); Alcotest.(check (list (pair string string))) "the ordinary saint is dropped into omitted, not vanished" [ ("ef-some-saint", "omitted: admission limit reached") ] (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted) (* RG 16(a) (register §6.0) reached end-to-end through the REAL pipeline ([PE.band], [PE.disposition] AND [PE.admit] together, not any one of them in isolation as this file's own hand-tagged rows test them separately): a real civil date this task's own blast-radius measurement names as a live instance of the bug this branch fixes -- 6 August 2028 is a Sunday (independently checked with `date -d 2028-08-06 +%A`, the same cross-check discipline [test_golden.ml]'s own header requires), Time after Pentecost week 9, and the Transfiguration (6 Aug, II class, "IN TRANSFIGURATIONE D. N. I. C.") falls on it. Before this fix `colitur day` emitted this Sunday as an admitted [+ef-time-after-pentecost-sunday-9] commemoration on that date (register §6.0's own reproduction). Fix round 1 (item 2): a THIRD candidate, Pope Sixtus II et al. (real slug/rank/status, 6 August, unrelated to the Sunday), is now also offered, so this test exercises BOTH fixes at once, the same real shape [test_golden.ml]'s own 2028-08-06 pin does: [observed] is the Transfiguration ({!band} entry 14 already did this correctly before this task); the Sunday is [disposition]-level [Omit] (RG 16(a)); Sixtus is [disposition]-level [Commemorate], but [admit] then excludes him too -- RG 111(b)'s rank floor, reached only because [admit] reads the day's Sunday-ness off [~temporal] (the [sunday] candidate passed to [P.resolve]), not off [observed] (the Transfiguration, whose own slug carries no Sunday marker). [commemorations] is EMPTY, and BOTH losers land in [omitted] with their own distinct reasons -- the Sunday via [disposition]'s own Omit ("omitted: yielded to a higher day"), Sixtus via [admit]'s cut ("omitted: admission limit reached") -- proving the two fixes operate through genuinely different mechanisms, not the same one coincidentally producing the same string. *) let test_rg16a_lord_feast_suppresses_sunday_end_to_end () = let date = mk 2028 8 6 in let day_ctx = ctx date in let sunday = of_temporal date in let transfiguration = cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "transfiguration-of-our-lord" in let sixtus = cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~layer:PE.universal_layer "pope-sixtus-ii-felicissimus-and-agapitus-martyrs" in let resolution = P.resolve real_rules day_ctx ~temporal:sunday ~sanctoral:[ transfiguration; sixtus ] in Alcotest.(check string) "the Transfiguration is observed, not the Sunday" "transfiguration-of-our-lord" (S.to_string resolution.P.observed.P.cel.Cel.slug); Alcotest.(check int) "no commemorations at all -- RG16(a)'s own \"nulla fit commemoratio\" AND RG111(b)'s rank floor, both live" 0 (List.length resolution.P.commemorations); Alcotest.(check (list (pair string string))) "both losers omitted, via two DIFFERENT mechanisms: the Sunday at disposition-level (RG16a), Sixtus at \ admit's own cut (RG111b)" [ ("ef-time-after-pentecost-sunday-9", "omitted: yielded to a higher day"); ("pope-sixtus-ii-felicissimus-and-agapitus-martyrs", "omitted: admission limit reached") ] (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted |> List.sort compare) (* End-to-end proof of BOTH fixes this task adds together, {!P.resolve} in full (band picks the winner, disposition and admit decide the loser's fate), not just the two isolated functions [cases]/[disposition_cases] already prove: 13 January 2019, the latest possible date Holy Family can fall on (Epiphany itself a Sunday) and one of the seven years it coincides with the fixed Commemoration of the Baptism of the Lord. {!band}'s [entry_14_movable_band] (RG 91 entry 14, "primum mobilia") must pick Holy Family as [observed], not the fixed Baptism candidate (which, absent that fix, would win outright via {!compare_by}'s own alphabetical tie-break at the shared literal 14 -- backwards from the primary text); {!PE.disposition}'s new RG 112(a) branch must then Omit the Baptism entirely, not merely demote it to a commemoration -- confirmed against BOTH the general rubric (RG 112(a)) and the Holy Family Mass propers' own more specific 13-January note (this file's own citation on the RG112(a) [disposition_cases] rows above), and independently against missalemeum's own JSON for this exact date (register §6.0): title "The Holy Family: Jesus, Mary & Joseph", commemorations [], the Baptism listed only under "displaced". *) let test_holy_family_excludes_baptism_end_to_end () = let date = T.holy_family_sunday 2019 in Alcotest.(check string) "sanity: this really is 13 January 2019" "2019-01-13" (D.to_iso8601 date); let day_ctx = ctx date in let holy_family = of_temporal date in let baptism = cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "commemoration-of-the-baptism-of-the-lord" in let resolution = P.resolve real_rules day_ctx ~temporal:holy_family ~sanctoral:[ baptism ] in Alcotest.(check string) "Holy Family is observed, not the fixed Baptism (RG91 e14: movable first)" "ef-time-after-epiphany-sunday-1" (S.to_string resolution.P.observed.P.cel.Cel.slug); Alcotest.(check string) "II class" "class-2" (V.rank_to_string resolution.P.observed.P.cel.Cel.rank); Alcotest.(check int) "no commemorations at all -- RG112(a)'s own exclusion, not merely dropped by an admission count" 0 (List.length resolution.P.commemorations); Alcotest.(check (list string)) "the Baptism is OMITTED, not merely uncommemorated" [ "commemoration-of-the-baptism-of-the-lord" ] (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.omitted) (* Fix round 1 (coordinator finding 1), end-to-end proof of the [entry_14_movable_band] fix itself: a genuine I-class candidate landing inside Holy Family's own 7-13 January window (a synthetic proper/indult feast, standing in for the diocesan-overlay shape {!band}'s own new comment names as the live risk) must be OBSERVED and must NOT transfer -- reproducing, at the [Precedence.resolve] level, the exact regression the coordinator demonstrated against the pre-fix [-14] sentinel ("the I-class feast displaced and transferred to the next day"). With the fix, {!band}'s entry 11 (110) beats {!PE.entry_14_movable_band} (139) outright, so the I-class candidate wins the day, and Holy Family itself -- losing, is_temporal, its own slug carrying the "-sunday" marker -- falls to RG 109(a)'s ordinary privileged-Sunday-commemoration path (disposition's final catch-all, [privilege_of]'s (a) branch), the same treatment ANY genuine feast outranking an ordinary Sunday gets: neither omitted nor transferred, a Privileged commemoration. *) let test_class1_feast_inside_holy_familys_window_end_to_end () = let date = mk 2026 1 11 in Alcotest.(check string) "sanity: this is Holy Family's own date in 2026" "2026-01-11" (D.to_iso8601 date); let day_ctx = ctx date in let holy_family = of_temporal date in let synthetic_class1 = cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-synthetic-class1-probe" in let resolution = P.resolve real_rules day_ctx ~temporal:holy_family ~sanctoral:[ synthetic_class1 ] in Alcotest.(check string) "the I-class candidate is observed, NOT Holy Family (RG91 e11 beats e14-movable)" "ef-synthetic-class1-probe" (S.to_string resolution.P.observed.P.cel.Cel.slug); Alcotest.(check (list string)) "nothing transfers -- the pre-fix bug moved the I-class feast to the next day" [] (List.map (fun c -> S.to_string c.P.cel.Cel.slug) resolution.P.deferred); (match resolution.P.commemorations with | [ (c, P.Privileged) ] -> Alcotest.(check string) "Holy Family itself is privileged-commemorated (RG109(a), a Sunday-shaped office \ outranked by a genuine feast), neither omitted nor transferred" "ef-time-after-epiphany-sunday-1" (S.to_string c.P.cel.Cel.slug) | other -> Alcotest.failf "expected exactly one Privileged commemoration (Holy Family), got %d" (List.length other)) (* Completes Task 7's carried fix (RG 91 entry 8, §4): 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:_ ~temporal:_ cs -> List.map (fun (c, p, (_ : int)) -> (c, p)) 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) (* Task 11: [PE.transfer_target] -- RG 96 ("the next following day that is not I or II class") plus its Annunciation exception. [occupant] is a synthetic callback ({!Colitur_kernel.Rite.t.transfer_target}'s own [occupant] parameter), not a real [Calendar]-driven one -- the CLI's own end-to-end proof (colitur day, All Souls landing on 3 Nov 2025 and the Annunciation landing on 5 Apr 2027, see test/cli.t and the task report) is what wires this against real data; these rows isolate the search function itself. *) (* [blocked] returns Class1 (blocking) for exactly the listed dates, Class4 (not blocking) everywhere else -- enough to exercise [is_blocking]'s own two-way test (RG 96 speaks of I OR II class; Class1 alone is enough to prove the blocking side, [test_transfer_target_terminates...] below adds nothing by varying it further). *) let occupant_blocking_on blocked_dates (d : D.t) : V.rank Cel.t = let blocking = List.exists (fun bd -> D.compare bd d = 0) blocked_dates in Cel.make ~slug:(S.of_string_exn "occupant") ~rank:(if blocking then V.Class1 else V.Class4) ~colour:Col.Green ~layer:"synthetic" () let occupant_always_blocking (_ : D.t) : V.rank Cel.t = Cel.make ~slug:(S.of_string_exn "occupant") ~rank:V.Class1 ~colour:Col.Green ~layer:"synthetic" () (* General RG 96 search: two consecutive blocked days past [origin], proving the search walks past MORE than one ineligible day rather than only trying [origin + 1] and stopping (the same shape Calendar's own synthetic fixture pins for the abstraction -- this pins it for the real EF search function). *) let test_transfer_target_general_multi_step_search () = let origin = mk 2026 1 10 in let occupant = occupant_blocking_on [ mk 2026 1 11; mk 2026 1 12 ] in let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-some-i-class-feast" in let target = PE.transfer_target c origin occupant in Alcotest.(check string) "lands on the first day past the blocked run" "2026-01-13" (D.to_iso8601 target) (* Coordinator review (fix round 1): RG 96 Attamen (a) (register-transcribed, primary-source-verified) makes the Annunciation exception CONDITIONAL on the general RG 96 walk carrying the feast past Easter Sunday -- NOT unconditional as the first transcription had it. The occupant here blocks every day from [origin + 1] through the day after Easter (26 March - 6 April 2026 inclusive), so the GENERAL target itself would land at 7 April -- after Easter (5 April) -- which is exactly the trigger condition, not merely "the Annunciation is impeded at all". *) let test_transfer_target_annunciation_starts_at_monday_after_low_sunday () = let origin = mk 2026 3 25 in let easter_2026 = Comp.gregorian_easter 2026 in let blocked_through_day_after_easter = let rec range a b = if D.compare a b > 0 then [] else a :: range (D.add_days a 1) b in range (D.add_days origin 1) (D.add_days easter_2026 1) in let occupant = occupant_blocking_on blocked_through_day_after_easter in let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer PE.annunciation_slug in let target = PE.transfer_target c origin occupant in let monday_after_low_sunday = D.add_days easter_2026 8 in Alcotest.(check string) "lands on the Monday after Low Sunday (Easter + 8), the general \ walk having crossed Easter itself" (D.to_iso8601 monday_after_low_sunday) (D.to_iso8601 target); Alcotest.(check bool) "NOT the general target (2 days after Easter, discriminates the branch)" true (D.compare target (D.add_days easter_2026 2) <> 0) (* RG 96's own qualifier on the exception -- "searching onward from there only if that day is itself blocked" (rite.mli) -- is [search_from]'s ORDINARY behaviour, not a second mechanism: same blocked run as above (forcing the general target past Easter, so the exception fires), PLUS the Monday after Low Sunday itself blocked, confirming the search continues exactly one more day from there. *) let test_transfer_target_annunciation_searches_onward_if_blocked () = let origin = mk 2026 3 25 in let easter_2026 = Comp.gregorian_easter 2026 in let monday_after_low_sunday = D.add_days easter_2026 8 in let blocked = let rec range a b = if D.compare a b > 0 then [] else a :: range (D.add_days a 1) b in range (D.add_days origin 1) (D.add_days easter_2026 1) @ [ monday_after_low_sunday ] in let occupant = occupant_blocking_on blocked in let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer PE.annunciation_slug in let target = PE.transfer_target c origin occupant in Alcotest.(check string) "searches onward one more day when that Monday is itself blocked" (D.to_iso8601 (D.add_days monday_after_low_sunday 1)) (D.to_iso8601 target) (* THE REGRESSION PIN (coordinator review): the bug an unconditional exception produced. 25 March 2057 is Lent III Sunday (I class, RG 91 entry 6), impeding the Annunciation; 26 March 2057 is an ordinary Lent feria (III class, well before Easter, 22 April 2057) -- the general RG 96 target. The general target does NOT fall after Easter, so the exception must NOT fire: the Annunciation lands on 26 March, not 13 April (Easter + 8), which is what the unconditional reading produced (verified by reverting the fix and re-running this exact test -- see the task report's mutation record). Uses the REAL [Temporal_ef.temporal] as [occupant] (not a synthetic stand-in), the same coupling-safety convention [of_temporal]'s callers use elsewhere in this file, so this is also effectively an end-to-end check of the real 2057 calendar shape, not just the search's own logic in isolation. *) let test_transfer_target_annunciation_not_overridden_when_general_target_precedes_easter () = let origin = mk 2057 3 25 in Alcotest.(check string) "25 March 2057 is a Sunday (Lent III)" "sunday" (D.weekday_to_string (D.weekday origin)); let occupant d = (T.temporal d).Colitur_kernel.Temporal.office in let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer PE.annunciation_slug in let target = PE.transfer_target c origin occupant in Alcotest.(check string) "lands on 26 March 2057 (the general RG96 target), NOT the \ Annunciation exception's Monday after Low Sunday" "2057-03-26" (D.to_iso8601 target) (* rite.mli's own obligations on [transfer_target] (Task 11 brief): the call must TERMINATE and its result must be STRICTLY AFTER [origin], even for a rite/data shape this function cannot have anticipated -- an occupant that reports every single day as blocking, forever. Calendar's own round guard (max_transfer_rounds) does not cover this: it bounds ROUNDS across a whole year, not the internal walk one call to this function makes (see precedence_ef.ml's own comment on [search_from] and [max_search_days]). Deliberately NOT pinning the exact returned date against the private [max_search_days] constant -- that would coalesce a behavioural contract (terminates, makes forward progress) with an internal tuning value this function is free to change; a generous, test-owned ceiling (1000 days, comfortably past any realistic bound) is enough to prove termination is genuine and not merely "didn't hang during this particular run". *) let test_transfer_target_terminates_under_pathological_occupant () = let origin = mk 2026 1 1 in let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-pathological-case" in let target = PE.transfer_target c origin occupant_always_blocking in Alcotest.(check bool) "strictly after origin" true (D.compare target origin > 0); Alcotest.(check bool) "terminates within a generous bound (proves the internal search is bounded, not merely lucky)" true (D.compare target (D.add_days origin 1000) <= 0) (* Coordinator review: [search_from] must not probe [occupant] past {!Date}'s own domain ceiling (31 December 9999). A SYNTHETIC occupant (like [occupant_always_blocking] above) can never actually discriminate this: it never calls [Computus.gregorian_easter] itself, so it cannot raise regardless of whether the domain guard exists -- a test built on one would only prove [search_from]'s unrelated step bound, not this fix. [occupant] here is instead the REAL [Temporal_ef.temporal] (no sanctoral layer needed: 29-31 Dec are ALREADY II class via [named]'s own Nativity- octave-day entries, so three real, unbroken blocking days already sit at the very end of the domain) -- exactly the shape that raises without the fix: 1 January of civil year 10000 is next, and [Computus.gregorian_easter 10000] does [Date.make ~year:10000 ...] and [failwith]s (the .ml's own [domain_max_date] comment; also how the reviewer reproduced the bug through the project's own overlay mechanism -- see the task report for that end-to-end reproduction). Mutation-verified: reverting the domain guard makes this test error with exactly that uncaught [Failure], not merely fail an assertion (see the task report). *) let test_transfer_target_does_not_raise_at_domain_ceiling () = let origin = mk 9999 12 28 in let occupant d = (T.temporal d).Colitur_kernel.Temporal.office in let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-domain-ceiling-case" in let target = PE.transfer_target c origin occupant in Alcotest.(check bool) "past 31 December 9999 (the guard engaged; nothing admissible remained \ in-domain, so the search gave up at the ceiling rather than crashing)" true (D.compare target (mk 9999 12 31) > 0) 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 @ 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)))) privilege_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 ] @ List.map (fun (desc, observed, temporal, comms, expect) -> Alcotest.test_case desc `Quick (fun () -> Alcotest.(check (list string)) desc expect (slugs_of (PE.admit ~observed ~temporal comms)))) admit_cases @ [ Alcotest.test_case "admit is order-independent (III-class, 3 candidates)" `Quick test_admit_order_independent; Alcotest.test_case "RG113: admit picks by precedence order, not slug, when they disagree" `Quick test_admit_uses_precedence_not_slug_when_they_disagree; Alcotest.test_case "I-class day: full drop reported in omitted, not vanished" `Quick test_i_class_day_drops_into_omitted; Alcotest.test_case "II-class Sunday: second loser dropped into omitted" `Quick test_ii_class_sunday_drops_second_loser_into_omitted; Alcotest.test_case "RG109(e) Lent feria privileged end-to-end" `Quick test_privileged_lent_feria_admitted_end_to_end; Alcotest.test_case "II-class Sunday override: RG109(b) witness admitted over an ordinary saint, end-to-end" `Quick test_ii_class_sunday_privileged_witness_admitted_end_to_end; Alcotest.test_case "RG16(a): a Feast of the Lord suppresses the Sunday's own commemoration entirely, \ end-to-end (2028-08-06)" `Quick test_rg16a_lord_feast_suppresses_sunday_end_to_end; Alcotest.test_case "RG112(a) + RG91 e14 \"primum mobilia\": Holy Family excludes the Baptism of the Lord \ entirely, end-to-end (13 Jan 2019)" `Quick test_holy_family_excludes_baptism_end_to_end; Alcotest.test_case "fix round 1, finding 1: a synthetic I-class candidate inside Holy Family's own window is \ observed and does not transfer (2026-01-11)" `Quick test_class1_feast_inside_holy_familys_window_end_to_end; Alcotest.test_case "transfer_target: general RG96 search walks past more than one blocked day" `Quick test_transfer_target_general_multi_step_search; Alcotest.test_case "transfer_target: Annunciation exception starts at Monday after Low Sunday" `Quick test_transfer_target_annunciation_starts_at_monday_after_low_sunday; Alcotest.test_case "transfer_target: Annunciation exception searches onward if that Monday is blocked" `Quick test_transfer_target_annunciation_searches_onward_if_blocked; Alcotest.test_case "transfer_target: Annunciation NOT overridden when the general target precedes Easter \ (2057 regression)" `Quick test_transfer_target_annunciation_not_overridden_when_general_target_precedes_easter; Alcotest.test_case "transfer_target: terminates and stays forward under a pathological occupant" `Quick test_transfer_target_terminates_under_pathological_occupant; Alcotest.test_case "transfer_target: does not raise probing past the domain ceiling" `Quick test_transfer_target_does_not_raise_at_domain_ceiling ] )