diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-26 23:40:17 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-26 23:40:17 +0200 |
| commit | 2f6d25aff88b4c76c61e36889f1c017b3cc8faee (patch) | |
| tree | baf3027229ef3b95eff2d7fdf0b479f8bdbb2a87 /lib/rites/rite_of/precedence_of.ml | |
| parent | 1c0137dee8ff3858707281366662891a5ff01044 (diff) | |
| download | colitur-2f6d25aff88b4c76c61e36889f1c017b3cc8faee.tar.gz colitur-2f6d25aff88b4c76c61e36889f1c017b3cc8faee.zip | |
fix(of): anticipate St Joseph onto 18 March per Normae n. 56(f)
W1: when 19 March (St Joseph) falls on Palm Sunday, colitur sent him
forward to Easter+9 instead of anticipating the solemnity backward to
18 March, the one rule in the whole Normae transfer machinery that
names an earlier date. Live in 16 of 1583-2100 (1595, 1606, 1617,
1690, 1758, 1769, 1780, 1815, 1826, 1837, 1967, 1978, 1989, 2062,
2073, 2084).
Blocked on a kernel contract: Rite.t.transfer_target required its
result be strictly later than the impeded date, an obligation argued
purely from the EF's RG 96. Traced against Calendar's actual placement
pass rather than assumed unsafe: the round loop re-resolves the whole
year fresh every round, and its two safety nets (~start/~stop; the
round-count guard) both test the target's own value with no comparison
against origin, so a bounded backward jump converges the same way a
bounded forward one does. The one requirement that stays load-bearing
is target <> origin: Precedence.resolve never deduplicates by slug, so
a candidate returning its own argument unchanged would collide with
itself in the same round's contest. Relaxed the obligation accordingly
(rite.mli) and added the fourth transfer_target branch in
Precedence_of, ahead of the existing three (Precedence_of.transfer_target,
Rule 0). calendar.ml needed no logic change, only two comments that
asserted "every genuine transfer moves forward" as a blanket fact.
Unpinned the two tests that asserted the old wrong (forward) behaviour
and replaced them with assertions of the correct one, plus a new
direct unit test of the backward branch across two independent years.
EF output is untouched: data/ef/ and lib/rites/rite_ef/ carry no diff,
and colitur day/readings for 2026, 1583 and 9999 are byte-identical
against a build of the prior commit. OF's own 2005-2050 window carries
none of the 16 affected years and shows zero unexpected diffs; the
full 1583-9999 domain still reaches a fixed point under the exhaustive
sweep.
Diffstat (limited to 'lib/rites/rite_of/precedence_of.ml')
| -rw-r--r-- | lib/rites/rite_of/precedence_of.ml | 84 |
1 files changed, 68 insertions, 16 deletions
diff --git a/lib/rites/rite_of/precedence_of.ml b/lib/rites/rite_of/precedence_of.ml index 0587f0a..5344374 100644 --- a/lib/rites/rite_of/precedence_of.ml +++ b/lib/rites/rite_of/precedence_of.ml @@ -300,9 +300,22 @@ let vigil_feast (_ : Vocab_of.rank Precedence.candidate) : Slug.t option = None let rules : (Vocab_of.season, Vocab_of.rank) Precedence.rules = { band; disposition; admit; vigil_feast } -(* Normae n. 60 and Normae n. 5 together. THREE rules, in this priority order - -- n. 60's own text defers to n. 5 explicitly ("servatis iis quae n. 5 - statuuntur"), so the two are one rule set, not two competing ones. +(* Normae n. 56(f), n. 60 and n. 5 together. FOUR rules, in this priority + order -- n. 60's own text defers to n. 5 explicitly ("servatis iis quae + n. 5 statuuntur"), and n. 56(f) names the single collision it overrides + outright, so all three are one rule set, not three competing ones. + + 0. ST JOSEPH ON PALM SUNDAY (Normae n. 56(f)): "Sollemnitas S. Ioseph, + ubi est de praecepto servanda, si cum Dominica in palmis de Passione + Domini occurrit, anticipatur sabbato praecedenti, die 18 martii" -- + ANTICIPATED BACKWARD to the preceding Saturday, 18 March, the one + named exception to every other rule here searching or landing + forward. RESOLVED (of-normae-56f/W1, 2026-08-26; previously KNOWN + UNIMPLEMENTED -- see this value's own .mli for the full history, + including why it was blocked on a kernel contract change and what + that change cost). Checked first, ahead of rule 1, because it names + a more specific collision (one candidate, one date shape) than any + rule below. 1. THE ANNUNCIATION IN HOLY WEEK (Normae n. 60): "Sollemnitas vero Annuntiationis Domini, quotiescumque occurrit aliquo die Hebdomadae @@ -328,15 +341,19 @@ let rules : (Vocab_of.season, Vocab_of.rank) Precedence.rules = whose occupant bands ABOVE 80 (entry 8 x 10). TERMINATION, which Rite.t.transfer_target requires the rite to guarantee: - the search is bounded at [max_search_days] steps AND at {!domain_max_date}, - and returns the last date visited -- never raises -- if either bound is - reached before a free day is found. That bound is STRUCTURAL -- it does - not appeal to the real calendar's own shape -- because Calendar's round - guard bounds ROUNDS, not the internal search of a single call, and an - unbounded walk would hang the caller outright before that guard was ever - consulted; the domain bound additionally prevents the walk from ever - calling [occupant] on a date {!Computus.gregorian_easter} cannot construct - (see {!domain_max_date}'s own comment). *) + rules 1-3's search is bounded at [max_search_days] steps AND at + {!domain_max_date}, and returns the last date visited -- never raises -- + if either bound is reached before a free day is found. That bound is + STRUCTURAL -- it does not appeal to the real calendar's own shape -- + because Calendar's round guard bounds ROUNDS, not the internal search of + a single call, and an unbounded walk would hang the caller outright + before that guard was ever consulted; the domain bound additionally + prevents the walk from ever calling [occupant] on a date + {!Computus.gregorian_easter} cannot construct (see {!domain_max_date}'s + own comment). Rule 0 needs no bound of its own: it runs no search at + all, computing [Date.add_days origin (-1)] directly, so it terminates + trivially -- the one shape {!Colitur_kernel.Rite.t.transfer_target}'s own + comment names as always safe regardless of direction. *) let max_search_days = 400 @@ -361,8 +378,26 @@ let max_search_days = 400 let domain_max_date = match Date.make ~year:9999 ~month:12 ~day:31 with Ok d -> d | Error e -> failwith e +(* NOTE, found while adding [st_joseph_slug] below, NOT fixed here -- + out of scope for the W1 task this touches: [annunciation_slug] does not + match the real shipped entry's own slug (data/of/calendar-2002.sexp has + "annunciation-of-the-lord", not "of-annunciation"), so Rule 1 below + never actually fires against real sanctoral data -- only against the + synthetic candidate test_precedence_of.ml's own [annunciation] builds + with the same placeholder slug. Whether this is output-visible on real + data was not established either way here; Rule 3's general forward + search is a plausible fallback that could reach the identical Easter+8 + target by coincidence in the one case tested + ([test_annunciation_in_easter_octave]'s own comment says as much for a + collision NOT in Holy Week), but that is not the same claim as "the two + rules always agree", and this was not checked for Holy Week itself. *) let annunciation_slug = "of-annunciation" +(* The real shipped slug (data/of/calendar-2002.sexp) -- St Joseph's own + Rule 0 below is matched against the actual sanctoral entry, not a + placeholder, precisely because the note above exists. *) +let st_joseph_slug = "joseph-husband-of-the-blessed-virgin-mary" + (* Tabula entries 1-8, scaled: a day is FREE to receive a transfer when its occupant bands strictly above entry 8. *) let entry_8_band = 80 @@ -373,9 +408,27 @@ let transfer_target (c : Vocab_of.rank Precedence.candidate) (origin : Date.t) let easter = Computus.gregorian_easter year in let offset d = Date.to_rata d - Date.to_rata easter in let is_holy_week d = offset d >= -7 && offset d <= -1 in - (* Rule 1. *) - if Slug.to_string c.cel.slug = annunciation_slug && is_holy_week origin then - Date.add_days easter 8 + let is_palm_sunday d = offset d = -7 in + (* Rule 0 (Normae n. 56(f)): "Sollemnitas S. Ioseph, ubi est de praecepto + servanda, si cum Dominica in palmis de Passione Domini occurrit, + anticipatur sabbato praecedenti, die 18 martii" -- St Joseph, impeded + by Palm Sunday, is ANTICIPATED BACKWARD to the preceding Saturday, not + carried forward like every other rule here. A fixed one-day + displacement, not a search: [origin] is Palm Sunday itself whenever + this fires (a Sunday, by construction), so "the preceding Saturday" + and "the day before [origin]" are the same date, and computing it this + way -- rather than via [offset]/[easter] -- makes that identity exact + rather than incidental. This is the rite.mli-documented EARLIER-target + shape (rite.mli's own [transfer_target] comment): a single named + collision, no internal search, hence trivially terminating on its own. + Checked ahead of Rules 1-3, most-specific first, the same order this + whole function already keeps. *) + if Slug.to_string c.cel.slug = st_joseph_slug && is_palm_sunday origin then + Date.add_days origin (-1) + else if + (* Rule 1. *) + Slug.to_string c.cel.slug = annunciation_slug && is_holy_week origin + then Date.add_days easter 8 else let season_of d = (Temporal_of.temporal d).Temporal.season in let privileged_sunday d = @@ -384,7 +437,6 @@ let transfer_target (c : Vocab_of.rank Precedence.candidate) (origin : Date.t) | Vocab_of.Advent | Vocab_of.Lent | Vocab_of.Easter -> true | Vocab_of.Christmas | Vocab_of.Ordinary_time -> false in - let is_palm_sunday d = offset d = -7 in let is_easter_sunday d = offset d = 0 in (* Rule 2. *) if |
