summaryrefslogtreecommitdiff
path: root/lib
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 /lib
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.
Diffstat (limited to 'lib')
-rw-r--r--lib/rites/rite_ef/precedence_ef.ml109
-rw-r--r--lib/rites/rite_ef/precedence_ef.mli29
2 files changed, 102 insertions, 36 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