diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-11 22:26:17 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-11 22:26:17 +0200 |
| commit | 553dc44d2ba0e131e7f2ac79dc755641afcd6a1c (patch) | |
| tree | 93ce2086d7de50eb05dbbe8dbb38f45cedc832c5 /lib/rites/rite_ef | |
| parent | 436ba75e27d2aa61b1c6035a22157b40f1a9834b (diff) | |
| download | colitur-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/rites/rite_ef')
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.ml | 109 | ||||
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.mli | 29 |
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 |
