summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 22:26:17 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 22:26:17 +0200
commit553dc44d2ba0e131e7f2ac79dc755641afcd6a1c (patch)
tree93ce2086d7de50eb05dbbe8dbb38f45cedc832c5
parent436ba75e27d2aa61b1c6035a22157b40f1a9834b (diff)
downloadcolitur-553dc44d2ba0e131e7f2ac79dc755641afcd6a1c.tar.gz
colitur-553dc44d2ba0e131e7f2ac79dc755641afcd6a1c.zip
rite(ef): fix entry 8's Sunday exception and entry 14's over-restriction
Review of 436ba75 found two calendar defects and one coupling risk. Entry 8 (All Souls) dropped register line 334's own qualifier, "(yields to an occurring Sunday)" -- it returned 8 unconditionally, so on 2 November falling on a Sunday (2025, 2031, 2036, 2042 in the 2005-2050 differential window), All Souls incorrectly outranked and observed over the Sunday. 2 November is always Time_after_pentecost and never coincides with any other entry's own date, so the only rival this exception ever has is an ordinary entry-15 II-class Sunday; on such a Sunday, band now returns one worse than entry 15's own value rather than the literal 8 -- strictly worse, not merely different, since an exact tie would fall to Precedence.resolve's slug tie-break, which for "ef-all-souls" against a Sunday slug would make All Souls win the tie anyway. Entry 8's own rank is untouched, so Task 8's disposition still sees a true I-class candidate to transfer. Entry 14 (Feasts of the Lord, II class) added a universal-layer restriction the register's line 341 does not carry -- contrast entry 16 at line 342, which explicitly says "not of the Lord"; RG 37c (register line 393) also speaks of "II-class feasts of the Lord" with no universal qualifier. Dropped the layer test: a proper or indult feast of the Lord now bands 14, not 19/20. Also exposes vigil_suffix and ember_prefixes from precedence_ef.mli, matching universal_layer/indult_prefix's existing "colitur convention, not an RG citation" treatment -- both were previously private literals duplicated in the test file, so a rename of temporal_ef.ml's slug format could have drifted silently past both sides agreeing with each other. Two test rows now build their candidate from Temporal_ef.temporal's own real output (entry 18's Lent Ember day, entry 21's Ascension Vigil) instead of a hand-typed slug, closing that specific coupling. Adds the three cheap rows review named as closing the remaining unpinned guards (a temporal-origin Class1 candidate on an unnamed date; a universal Class2 vigil of the Lord; a non-universal Class3 vigil), plus a resolve()-level test proving the Sunday is actually observed over All Souls, not just that band returns the right integer in isolation.
-rw-r--r--lib/rites/rite_ef/precedence_ef.ml109
-rw-r--r--lib/rites/rite_ef/precedence_ef.mli29
-rw-r--r--test/test_precedence_ef.ml102
3 files changed, 198 insertions, 42 deletions
diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml
index 50da4a7..0716177 100644
--- a/lib/rites/rite_ef/precedence_ef.ml
+++ b/lib/rites/rite_ef/precedence_ef.ml
@@ -9,22 +9,27 @@
without a separate exclusion for every later entry it pre-empts.
Two kinds of evidence decide an entry:
- - The temporal cycle's own office (Nativity, a Sunday, a feria, a vigil of
- the Lord) is identified structurally, from the context's date/season/
- weekday and the day's Easter offset -- never from its slug, which is
- just a label. [origin = Temporal] gates every such entry so a sanctoral
- candidate that happens to share a date (Immaculate Conception can never
- coincide with the movable cycle, but nothing stops a future rite bug
- from producing one) cannot be mistaken for the office itself.
- - A sanctoral feast's entry (11-13 I class, 14/16/19/20 II class, 23/24
- III class) is decided by its [rank] plus, per the brief's structural
- insight, its {!Celebration.t}.layer: a celebration whose layer is not
- the universal base is an overlay, hence "proper" or "indult" rather
- than the universal entry (see precedence_ef.mli). [origin = Sanctoral]
- gates these for the same reason: temporal-origin celebrations carry the
- literal layer id "temporal" (rite_ef/temporal_ef.ml's [build]), which is
- not [universal_layer] either, and would otherwise be misread as
- "proper" by the layer test alone.
+ - The temporal cycle's own office (Nativity, a Sunday, a feria, All Souls)
+ is identified structurally, from the context's date/season/weekday and
+ the day's Easter offset -- never from its slug, which is just a label.
+ [origin = Temporal] gates every such entry so a sanctoral candidate that
+ happens to share a date (Immaculate Conception can never coincide with
+ the movable cycle, but nothing stops a future rite bug from producing
+ one) cannot be mistaken for the office itself. All Souls (entry 8, the
+ one non-temporal-origin member of this group) additionally reads the
+ context's weekday for its own register-stated exception -- see entry 8
+ below.
+ - A sanctoral feast's entry is decided by its [rank], and -- except at
+ entry 14 (see its own comment below, where the register draws no such
+ line) -- per the brief's structural insight, also by its
+ {!Celebration.t}.layer: a celebration whose layer is not the universal
+ base is an overlay, hence "proper" or "indult" rather than the
+ universal entry (11-13 I class; 16/19/20 II class; 23/24 III class; see
+ precedence_ef.mli). [origin = Sanctoral] gates these for the same
+ reason: temporal-origin celebrations carry the literal layer id
+ "temporal" (rite_ef/temporal_ef.ml's [build]), which is not
+ [universal_layer] either, and would otherwise be misread as "proper" by
+ the layer test alone.
Vigils (21, 26) are the one shape neither of those two kinds fully
describes on their own: a II/III-class vigil can be temporal-origin (the
@@ -46,19 +51,33 @@ let unclassified = max_int
let is_indult layer = String.starts_with ~prefix:indult_prefix layer
let is_universal layer = String.equal layer universal_layer
-(* Ember days are identified by the temporal cycle's own slug convention
- (rite_ef/temporal_ef.ml's [ember]: "ef-<set>-ember-<day>"), not
- re-derived here: the September anchor in particular is one of the more
- contested dates in the 1962 calendar (temporal_ef.ml's own comment on
+(* Not an RG citation either -- see [universal_layer] above. Nothing in
+ {!Celebration.t} otherwise marks "this is a vigil, not an ordinary office
+ of the same rank" (see the file's top comment), so entries 21/26 read it
+ off the temporal cycle's own slug suffix (rite_ef/temporal_ef.ml's
+ [named], e.g. "ef-ascension-vigil"). Exposed so a future task naming a
+ sanctoral vigil (Task 10: Assumption, John Baptist, Peter & Paul,
+ Lawrence -- only Ascension exists today) uses the same suffix; a
+ differently-named vigil would band 16/24 instead of 21/26, silently. *)
+let vigil_suffix = "-vigil"
+
+let is_vigil slug = String.ends_with ~suffix:vigil_suffix slug
+
+(* Not an RG citation -- see [universal_layer]. Entry 18's Ember days are
+ identified by the temporal cycle's own slug convention (rite_ef/
+ temporal_ef.ml's [ember]: "ef-<set>-ember-<day>"), not re-derived here:
+ the September anchor in particular is one of the more contested dates in
+ the 1962 calendar (temporal_ef.ml's own comment on
[third_sunday_of_september]), and re-deriving it a second time would only
create a second place for that same uncertainty to drift. Only the
Advent, Lent and September sets are listed: RG 91 entry 18 names exactly
those three; the Whitsun (Pentecost) set is I class and falls inside the
- Pentecost octave, entry 10, matched below before this is ever reached. *)
-let is_ember_18 slug =
- String.starts_with ~prefix:"ef-advent-ember-" slug
- || String.starts_with ~prefix:"ef-lent-ember-" slug
- || String.starts_with ~prefix:"ef-september-ember-" slug
+ Pentecost octave, entry 10, matched below before this is ever reached.
+ Exposed for the same reason as [vigil_suffix]: a rename of temporal_ef's
+ format has somewhere to be caught other than a silently-wrong entry 18. *)
+let ember_prefixes = [ "ef-advent-ember-"; "ef-lent-ember-"; "ef-september-ember-" ]
+
+let is_ember_18 slug = List.exists (fun prefix -> String.starts_with ~prefix slug) ember_prefixes
let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedence.candidate) :
int =
@@ -68,7 +87,7 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc
let layer = cel.Celebration.layer in
let slug = Slug.to_string cel.Celebration.slug in
let is_temporal = c.Precedence.origin = Precedence.Temporal in
- let is_vigil = String.ends_with ~suffix:"-vigil" slug in
+ let is_vigil = is_vigil slug in
let date = ctx.Precedence.date in
let season = ctx.Precedence.season in
let weekday = ctx.Precedence.weekday in
@@ -77,6 +96,11 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc
(* Easter offset, the same convention as temporal_ef.ml's [days_between
easter d]: 0 is Easter itself, negative before, positive after. *)
let off = Date.to_rata date - Date.to_rata (Computus.gregorian_easter (Date.year date)) in
+ (* Named so entry 8's Sunday exception below can read "one worse than the
+ Sunday it must yield to" rather than a bare integer that happens to
+ equal entry 15's own value; entry 15's own branch returns this same
+ binding, not a second literal, so the two can never drift apart. *)
+ let entry_15_band = 15 in
let open Vocab_ef in
(* 1: Nativity, Easter Sunday, Pentecost Sunday (I class w/ octave). *)
if is_temporal && rank = Class1 && ((m = 12 && d = 25) || off = 0 || off = 49) then 1
@@ -101,8 +125,23 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc
(* 7: I-class ferias not above -- Ash Wednesday; Mon/Tue/Wed of Holy Week.
Thu-Sat of Holy Week are the Triduum, entry 2 above, not this entry. *)
else if is_temporal && rank = Class1 && (off = -46 || (off >= -6 && off <= -4)) then 7
- (* 8: All Souls. *)
- else if (not is_temporal) && rank = Class1 && m = 11 && d = 2 then 8
+ (* 8: All Souls -- register line 334's own text carries a qualifier this
+ transcription must honour: "yields to an occurring Sunday". 2 November
+ is always Time_after_pentecost (well clear of Advent/Lent/Passiontide
+ and of every other entry's own Easter-relative or fixed date), so a
+ Sunday landing on it is always an ordinary entry-15 II-class Sunday --
+ the one and only rival this exception ever has to lose to. On such a
+ Sunday this returns [entry_15_band + 1]: strictly worse than 15 (an
+ exact tie would fall to Precedence.resolve's slug tie-break, which
+ for "ef-all-souls" against a "ef-time-after-pentecost-sunday-*" slug
+ would make All Souls WIN -- the precise bug this guards against), but
+ otherwise not a citation to any other RG 91 row -- nothing else can
+ ever occur on 2 November to be confused with it. Entry 8's own [rank]
+ is untouched by this, so Task 8's disposition (RG 95: only I-class
+ feasts transfer) still sees the true I-class candidate it needs to
+ move to 3 November. *)
+ else if (not is_temporal) && rank = Class1 && m = 11 && d = 2 then
+ if is_sunday then entry_15_band + 1 else 8
(* 9: Vigil of Pentecost. *)
else if is_temporal && rank = Class1 && off = 48 then 9
(* 10: Days within the Octaves of Easter and Pentecost. *)
@@ -116,12 +155,16 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc
not the universal layer (11), and marked as an indult overlay (12's
"not indult" test having just failed). *)
else if (not is_temporal) && (not is_vigil) && rank = Class1 then 13
- (* 14: Feasts of the Lord, II class. *)
- else if (not is_temporal) && (not is_vigil) && rank = Class2 && is_universal layer
- && subject = Subject.Lord
- then 14
+ (* 14: Feasts of the Lord, II class -- register line 341, deliberately
+ UNQUALIFIED (contrast entry 16 at line 342, which explicitly says "not
+ of the Lord"; RG 37c, register line 393, speaks of "II-class feasts of
+ the Lord" replacing an occurring II-class Sunday with no universal
+ qualifier either). No layer test here, unlike 11/12/13 and 16/19/20:
+ the register does not split this entry into universal/proper/indult,
+ so a proper or indult feast of the Lord still bands 14, not 19/20. *)
+ else if (not is_temporal) && (not is_vigil) && rank = Class2 && subject = Subject.Lord then 14
(* 15: Sundays, II class (every Sunday not already named at 6). *)
- else if is_temporal && rank = Class2 && is_sunday then 15
+ else if is_temporal && rank = Class2 && is_sunday then entry_15_band
(* 16: II-class feasts of the universal Church, not of the Lord. *)
else if (not is_temporal) && (not is_vigil) && rank = Class2 && is_universal layer then 16
(* 17: Days within the Octave of the Nativity (26-28 Dec are Stephen,
diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli
index e8d4a11..63fb0a1 100644
--- a/lib/rites/rite_ef/precedence_ef.mli
+++ b/lib/rites/rite_ef/precedence_ef.mli
@@ -18,6 +18,24 @@ val universal_layer : string
(** See {!universal_layer}. *)
val indult_prefix : string
+(** Slug suffix marking a celebration as a vigil (RG 91 entries 21, 26),
+ e.g. "ef-ascension-vigil". Also colitur's own convention, not an RG
+ citation, exposed for the same reason as {!universal_layer}: only the
+ Ascension Vigil exists today (rite_ef/temporal_ef.ml); the Assumption,
+ John Baptist, Peter & Paul and Lawrence vigils arrive as sanctoral data
+ in a future task, and must use this same suffix or {!band} will band
+ them 16/24 (an ordinary feast of the same rank) instead of 21/26. *)
+val vigil_suffix : string
+
+(** Slug prefixes marking a celebration as one of RG 91 entry 18's three
+ Ember-day sets (Advent, Lent, September -- the Pentecost/Whitsun set is
+ I class and matched by entry 10 before this is ever consulted). Also
+ colitur's own convention mirroring rite_ef/temporal_ef.ml's own "ef-<set>
+ -ember-<day>" slug format, not re-derived from first principles; exposed
+ so a rename of that format has somewhere to be caught other than a
+ silently-wrong entry 18. *)
+val ember_prefixes : string list
+
(** Returned for a candidate shape RG 91's 28-entry table has no row for --
e.g. a [Class1] vigil that is not the Nativity or Pentecost (entries 5,
9 are the only I-class vigils the table names), or a [Class4] candidate
@@ -29,7 +47,12 @@ val unclassified : int
(** [band ctx c]: RG 91's Table of Precedence. Returns the table's own entry
number -- I class 1-13, II class 14-21, III class 22-26, IV class 27-28;
- lower wins (see {!Precedence.rules.band}). Total over every candidate
- {!Precedence.resolve} or {!Calendar} can construct, including shapes the
- 1962 table itself does not describe (see {!unclassified}). *)
+ lower wins (see {!Precedence.rules.band}) -- EXCEPT where the table's own
+ text states an exception: entry 8 (All Souls, register line 334) reads
+ "yields to an occurring Sunday", so on a Sunday this returns a value that
+ loses to entry 15 rather than the literal integer 8 (see the comment on
+ entry 8 in precedence_ef.ml for the exact value and why). Total over
+ every candidate {!Precedence.resolve} or {!Calendar} can construct,
+ including shapes the 1962 table itself does not describe (see
+ {!unclassified}). *)
val band : Vocab_ef.season Precedence.context -> Vocab_ef.rank Precedence.candidate -> int
diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml
index fb05582..7033978 100644
--- a/test/test_precedence_ef.ml
+++ b/test/test_precedence_ef.ml
@@ -30,6 +30,17 @@ let cand ?(origin = P.Temporal) ?(rank = V.Class1) ?(subject = Sub.Temporal) ?(l
{ P.cel = Cel.make ~slug:(S.of_string_exn slug) ~rank ~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. *)
@@ -76,8 +87,20 @@ let cases =
("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 7);
("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 7);
("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 7);
- (* Entry 8 -- register line 334. *)
- ("8 All Souls", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 8);
+ (* Entry 8 -- register line 334. 2 Nov 2026 is a Monday (verified
+ independently below the table), so this row is the plain case. The
+ register's own qualifying case -- "yields to an occurring Sunday" --
+ gets its own row and its own end-to-end test after this table (2 Nov
+ 2025 is a real Sunday). *)
+ ("8 All Souls (non-Sunday)", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 8);
+ (* Entry 8's qualifier: "(yields to an occurring Sunday)". 2 Nov 2025 is
+ a Sunday, so this must NOT be 8 -- it must lose to entry 15 (16 =
+ entry 15's own value + 1, the exact value precedence_ef.ml documents
+ and justifies at entry 8's branch). The end-to-end resolve-level
+ proof that the Sunday actually wins the day is
+ [test_all_souls_yields_to_sunday] below; this row pins the specific
+ integer [band] returns. *)
+ ("8 All Souls (yields to a Sunday, 2 Nov 2025)", mk 2025 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 16);
(* Entry 9 -- register line 335. *)
("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 9);
(* Entry 10 -- register line 336: both range boundaries, to guard the
@@ -101,11 +124,20 @@ let cases =
cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:(PE.indult_prefix ^ "local-grant")
"ef-indult-feast-1",
13 );
- (* Entry 14 -- register line 341. *)
+ (* Entry 14 -- register line 341, deliberately UNQUALIFIED (contrast
+ entry 16, line 342, which explicitly says "not of the Lord"). *)
( "14 Feast of the Lord, II class", mk 2026 7 1,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"ef-precious-blood",
14 );
+ (* Entry 14, non-base layer: unlike 11-13/16/19/20/23/24, entry 14 draws
+ no universal/proper/indult line at all, so this must STILL be 14, not
+ 19 -- the exact restriction review finding 2 flagged and this row
+ exists to keep from silently coming back. *)
+ ( "14 Feast of the Lord, II class (non-base layer)", mk 2026 7 2,
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:"diocese-warsaw"
+ "ef-local-feast-of-the-lord",
+ 14 );
(* Entry 15 -- register line 342: an ordinary Sunday not named at entry 6
-- Septuagesima is II class (RG 11-12 names only Advent/Lent/
Passiontide/Easter/Low/Pentecost as I class). *)
@@ -128,7 +160,10 @@ let cases =
about, worked the other way round: this Ember day must NOT be
mistaken for an ordinary entry-22 Lent feria either. *)
("18 Advent 17-23 Dec feria", mk 2026 12 21, cand ~rank:V.Class2 "ef-advent-4-mon", 18);
- ("18 Lent Ember Wednesday", off (-39), cand ~rank:V.Class2 "ef-lent-ember-wed", 18);
+ (* Sourced from Temporal_ef.temporal's own output (see [of_temporal])
+ rather than a hand-typed "ef-lent-ember-wed" -- closes review finding
+ 3's coupling concern for the Ember prefixes specifically. *)
+ ("18 Lent Ember Wednesday (from Temporal_ef.temporal)", off (-39), of_temporal (off (-39)), 18);
(* Entry 19 -- register line 344. *)
( "19 Proper II-class feast", mk 2026 1 20,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:"diocese-warsaw"
@@ -145,10 +180,20 @@ let cases =
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). *)
- ("21 Ascension Vigil (temporal-origin)", off 38, cand ~rank:V.Class2 "ef-ascension-vigil", 21);
+ (* Sourced from Temporal_ef.temporal's own output (see [of_temporal])
+ rather than a hand-typed "ef-ascension-vigil" -- closes review finding
+ 3's coupling concern for [vigil_suffix]. *)
+ ("21 Ascension Vigil (from Temporal_ef.temporal)", off 38, of_temporal (off 38), 21);
( "21 Assumption Vigil (sanctoral-origin)", mk 2026 8 14,
cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-assumption-vigil",
21 );
+ (* Also review finding 3 / "worth doing": a UNIVERSAL-layer Class2 vigil
+ whose subject is the Lord must still be 21, not 14 -- pins entry 14's
+ [not is_vigil] guard even after finding 2 dropped its layer test. *)
+ ( "21 Universal II-class vigil of the Lord", mk 2026 6 23,
+ cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
+ "ef-precious-blood-vigil",
+ 21 );
(* Entry 22 -- register line 347-348 (corrected: ends at Palm Sunday, not
Passion Sunday). Both a Lent and a Passiontide feria, clear of Ash
Wednesday, Holy Week and the Ember days. *)
@@ -172,6 +217,11 @@ let cases =
( "26 III-class vigil", mk 2026 8 9,
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil",
26 );
+ (* Also worth doing: a NON-universal-layer Class3 vigil must still be 26,
+ not 23 -- pins entry 23's [not is_vigil] guard. *)
+ ( "26 III-class vigil (non-base layer)", mk 2026 8 10,
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-patron-vigil",
+ 26 );
(* Entry 27 -- register line 352: an otherwise-unoccupied IV-class
Saturday. *)
( "27 Office of the BVM on Saturday", off 62,
@@ -187,16 +237,56 @@ let cases =
( "unclassified: I-class vigil outside Nativity/Pentecost", mk 2026 3 10,
cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-mystery-vigil",
PE.unclassified );
+ (* Also worth doing: a temporal-origin Class1 candidate on a date none of
+ entries 1/2/3/5/6/7/9/10 name. 15 Jul 2026 is a Wednesday, off=101
+ from Easter -- clear of every Easter-relative window this module
+ checks, and not one of the fixed dates either. Without the
+ [not is_temporal] guard on entries 11-13, this would wrongly reach 12
+ (its default layer, "temporal", is not [universal_layer] and does not
+ carry [indult_prefix], so it reads as "proper" by the layer test
+ alone -- precisely the bug the guard exists to prevent; see the
+ [not is_temporal] guard's role in the entry-25 mutation test recorded
+ in the task report). *)
+ ("unclassified: I-class temporal candidate on an unnamed date", mk 2026 7 15, cand "ef-unnamed-day", PE.unclassified);
(* RG 91's own vigil list (register lines 381-384) stops at III class --
there is no IV-class vigil for entry 28's ferial catch-all to absorb. *)
( "unclassified: IV-class candidate marked as a vigil", mk 2026 6 20,
cand ~rank:V.Class4 "ef-second-mystery-vigil", PE.unclassified )
]
+(* Review finding 1's end-to-end proof: on a real Sunday landing on 2
+ November, [Precedence.resolve] -- not just [band] in isolation -- observes
+ the Sunday, not All Souls. This exercises the exact mechanism the finding
+ named ("resolve observes the lowest band, so whenever 2 November falls on
+ a Sunday, All Souls wins and the Sunday loses"), rather than only the
+ integer [band] returns for the standalone row above. [disposition] and
+ [admit] are stubs -- only [observed] is under test here. *)
+let test_all_souls_yields_to_sunday () =
+ let date = mk 2025 11 2 in
+ let day_ctx = ctx date in
+ let sunday =
+ { P.cel =
+ Cel.make ~slug:(S.of_string_exn "ef-time-after-pentecost-sunday-x") ~rank:V.Class2
+ ~colour:Col.Green ~subject:Sub.Temporal ~layer:"temporal" ();
+ origin = P.Temporal }
+ in
+ let all_souls = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls" in
+ let rules =
+ { P.band = (fun c cd -> PE.band c cd);
+ disposition = (fun ~winner:_ ~loser:_ -> P.Omit);
+ admit = (fun ~observed:_ cs -> cs) }
+ in
+ let resolution = P.resolve rules day_ctx ~temporal:sunday ~sanctoral:[ all_souls ] in
+ Alcotest.(check string) "the Sunday is observed, not All Souls"
+ "ef-time-after-pentecost-sunday-x"
+ (S.to_string resolution.P.observed.P.cel.Cel.slug)
+
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 )
+ cases
+ @ [ Alcotest.test_case "8 All Souls yields to a Sunday (resolve-level)" `Quick
+ test_all_souls_yields_to_sunday ] )