summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 23:40:17 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 23:40:17 +0200
commit2f6d25aff88b4c76c61e36889f1c017b3cc8faee (patch)
treebaf3027229ef3b95eff2d7fdf0b479f8bdbb2a87 /test
parent1c0137dee8ff3858707281366662891a5ff01044 (diff)
downloadcolitur-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 'test')
-rw-r--r--test/test_precedence_of.ml57
-rw-r--r--test/test_rite_of.ml110
2 files changed, 109 insertions, 58 deletions
diff --git a/test/test_precedence_of.ml b/test/test_precedence_of.ml
index e1af486..23c26c2 100644
--- a/test/test_precedence_of.ml
+++ b/test/test_precedence_of.ml
@@ -405,7 +405,13 @@ let test_annunciation_in_easter_octave () =
let test_solemnity_on_a_lenten_sunday () =
(* Normae n. 5: a solemnity occurring on a Sunday of Advent, Lent or Easter
goes to the FOLLOWING MONDAY, not to a searched free day. 19 March 2028
- (St Joseph) is the Third Sunday of Lent. *)
+ (St Joseph) is the Third Sunday of Lent, not Palm Sunday -- Rule 0
+ (n. 56(f)) does not fire here even with the REAL slug (see the test
+ right below this one for that), because its own guard is [is_palm_sunday
+ origin], not merely "this candidate is St Joseph". A synthetic slug is
+ used deliberately, to keep this test about rule 2's own generic
+ "solemnity on a privileged Sunday" behaviour, not about St Joseph
+ specifically. *)
let joseph =
mk ~slug:"of-joseph" ~rank:Vocab_of.Sollemnitas ~origin:Precedence.Sanctoral () in
let target =
@@ -413,10 +419,48 @@ let test_solemnity_on_a_lenten_sunday () =
Alcotest.(check string) "St Joseph 2028 goes to the following Monday"
"2028-03-20" (Date.to_iso8601 target)
+(* Normae n. 56(f), Rule 0 -- the one EARLIER-target shape
+ {!Colitur_kernel.Rite.t.transfer_target}'s relaxed obligation now permits
+ (rite.mli's own comment carries the argument; W1,
+ [of-normae-56f], 2026-08-26). Unit-level counterpart to
+ test_rite_of.ml's own full-Calendar [test_joseph_palm_sunday_2062_
+ anticipated_backward] -- this one calls [transfer_target] directly, the
+ same shape every other test in this file uses, and checks the REAL
+ shipped slug (unlike [test_solemnity_on_a_lenten_sunday] above, which
+ uses a placeholder deliberately). Two independent years, not one: 2062
+ and 1978 both have Gregorian Easter = 26 March (Palm Sunday = 19 March),
+ confirmed directly rather than assumed. *)
+let test_st_joseph_on_palm_sunday_anticipated_backward () =
+ let joseph =
+ mk ~slug:"joseph-husband-of-the-blessed-virgin-mary" ~rank:Vocab_of.Sollemnitas
+ ~origin:Precedence.Sanctoral () in
+ List.iter
+ (fun (palm_sunday, expected_target) ->
+ let origin = iso palm_sunday in
+ Alcotest.(check bool)
+ (Printf.sprintf "%s really is a Sunday (Easter = 26 March that year)" palm_sunday)
+ true (Date.weekday origin = Date.Sun);
+ let target = Rite_of.Precedence_of.transfer_target joseph origin occupant in
+ Alcotest.(check string)
+ (Printf.sprintf "St Joseph on Palm Sunday %s is anticipated BACKWARD to the preceding Saturday"
+ palm_sunday)
+ expected_target (Date.to_iso8601 target);
+ Alcotest.(check bool) (Printf.sprintf "%s -> earlier than origin, not later" palm_sunday) true
+ (Date.compare target origin < 0))
+ [ ("2062-03-19", "2062-03-18"); ("1978-03-19", "1978-03-18") ]
+
let test_target_is_strictly_later () =
- (* Rite.t.transfer_target's own stated obligation: Calendar treats
- target <= origin as a legitimate placement, so a rule that can stand
- still would loop candidates in place rather than fail loudly. *)
+ (* This is the Annunciation's OWN obligation (rules 1-3), not a claim
+ about {!Rite_of.Precedence_of.transfer_target} as a whole any more --
+ {!Colitur_kernel.Rite.t.transfer_target}'s kernel-level obligation was
+ relaxed from "strictly later" to "different from origin" (W1,
+ [of-normae-56f], 2026-08-26; see rite.mli's own comment), precisely so
+ Rule 0 above could return an EARLIER target for St Joseph. The
+ Annunciation's own three rules are untouched by that relaxation --
+ none of them was rewritten, and Calendar's placement pass still treats
+ [target = origin] as a legitimate (bad) placement for whichever
+ candidate returns it, so a rule that can stand still would still loop
+ in place rather than fail loudly. *)
List.iter
(fun s ->
let d = iso s in
@@ -496,7 +540,10 @@ let suite =
test_annunciation_in_easter_octave;
Alcotest.test_case "solemnity on a Lenten Sunday -> following Monday" `Quick
test_solemnity_on_a_lenten_sunday;
- Alcotest.test_case "transfer_target is always strictly later" `Quick
+ Alcotest.test_case
+ "Normae n.56(f), W1: St Joseph on Palm Sunday is anticipated backward to 18 March" `Quick
+ test_st_joseph_on_palm_sunday_anticipated_backward;
+ Alcotest.test_case "the Annunciation's own rules 1-3 are always strictly later" `Quick
test_target_is_strictly_later;
Alcotest.test_case "transfer_target lands free of Tabula entries 1-8" `Quick
test_target_is_free_of_entries_1_to_8;
diff --git a/test/test_rite_of.ml b/test/test_rite_of.ml
index 5e369b8..a7d3237 100644
--- a/test/test_rite_of.ml
+++ b/test/test_rite_of.ml
@@ -242,39 +242,35 @@ let test_transfers_reach_a_fixed_point_2005_2050 () =
let slug_of (c : V.rank Cel.t) = Slug.to_string c.Cel.slug
-(* precedence_of.mli's own documented, KNOWN-UNIMPLEMENTED fourth
- transfer_target rule (final review Important #5, ledger ruling R9):
- Normae n. 56(f) -- St Joseph's solemnity, falling on Palm Sunday, is
- ANTICIPATED BACKWARD to 18 March, not carried forward. colitur's
- [transfer_target] has no backward-search branch (rite.mli's own
- [transfer_target] contract requires the result be strictly LATER than
- the origin -- an EF-shaped kernel obligation, per that value's own
- citation), so it instead sends Joseph forward via the general rule 3,
- the same target date the Annunciation would already be heading to that
- same year (Easter + 8).
+(* RESOLVED ([of-normae-56f]/W1, 2026-08-26; previously the
+ precedence_of.mli-documented KNOWN-UNIMPLEMENTED fourth transfer_target
+ rule, final review Important #5, ledger ruling R9). Normae n. 56(f) --
+ St Joseph's solemnity, falling on Palm Sunday, is ANTICIPATED BACKWARD
+ to 18 March, not carried forward. {!Colitur_kernel.Rite.t.transfer_target}'s
+ own obligation was relaxed from "strictly later than origin" to
+ "different from origin" (rite.mli's own comment carries the argument,
+ traced against {!Colitur_kernel.Calendar}'s actual placement pass, not
+ merely assumed safe), and [Precedence_of.transfer_target] gained a
+ Rule 0 for exactly this collision -- see that value's own .ml/.mli for
+ the full citation.
- This is NOT fixed by this task (explicitly out of scope, per the task
- brief) -- pinned here, on a REAL affected year, so the current
- (known-wrong) behaviour is asserted deliberately rather than passing
- silently as if it were correct. All years with Gregorian Easter = 26
- March put Palm Sunday on 19 March, coinciding with Joseph; 2062 is the
- nearest one after today (2026) still inside the differential-adjacent
- range this project favours for worked examples.
+ Previously pinned here as KNOWN WRONG (colitur sent Joseph forward to
+ Easter+9 via the general rule 3 instead); now pinned as CORRECT. All
+ years with Gregorian Easter = 26 March put Palm Sunday on 19 March,
+ coinciding with Joseph; 2062 is the nearest one after today (2026)
+ still inside the differential-adjacent range this project favours for
+ worked examples.
2062 is a doubly-worked example, not chosen only for Joseph: that same
year, 25 March (the Annunciation) falls on HOLY SATURDAY (Easter - 1),
- so BOTH solemnities are impeded and BOTH nominally target Easter + 8 via
- the general rule -- {!Colitur_kernel.Calendar}'s own placement pass
- settles that collision by landing whichever it processes first (Joseph,
- departing the earlier date, 19 March) on Easter + 8 and pushing the
- other to the next admissible day; empirically, on shipped data, it is
- JOSEPH who is pushed one day further still, to Easter + 9 -- read as
- further confirmation this is genuinely rule 3's general forward search
- (sensitive to whatever else is competing that year), not the fixed,
- collision-proof Easter+8 destination rule 3's cousin (the Annunciation's
- own Holy-Week branch) names outright -- not as a claim about exactly
- which of the two wins the tie, which this test does not depend on. *)
-let test_joseph_palm_sunday_2062_known_wrong_forward_transfer () =
+ so the Annunciation is ALSO impeded and targets Easter + 8 via the
+ general rule -- independently of Joseph now, since Rule 0 is a fixed
+ one-day displacement that never enters the general search or competes
+ with the Annunciation's own target at all (Joseph departs 19 March for
+ 18 March; the Annunciation departs 25 March for Easter+8 -- the two no
+ longer collide the way they did when both were pushed toward the same
+ forward date). *)
+let test_joseph_palm_sunday_2062_anticipated_backward () =
let easter_2062 = Computus.gregorian_easter 2062 in
Alcotest.(check string) "2062 Easter is really 26 March (the Normae n.56(f) trigger)" "2062-03-26"
(Date.to_iso8601 easter_2062);
@@ -285,36 +281,44 @@ let test_joseph_palm_sunday_2062_known_wrong_forward_transfer () =
Alcotest.(check string) "25 March 2062 is really Holy Saturday, coinciding with the Annunciation too"
"2062-03-25" (Date.to_iso8601 holy_saturday);
let days = Cal.year real_of_rite real_of_layer 2061 in
- let joseph_day =
- Array.to_list days |> List.find (fun d -> Date.compare d.LD.date palm_sunday = 0)
- in
+ let day_of d = Array.to_list days |> List.find (fun day -> Date.compare day.LD.date d = 0) in
+ let joseph_day = day_of palm_sunday in
Alcotest.(check string) "Palm Sunday itself is observed on 19 March (Joseph impeded, correctly)"
"of-palm-sunday" (slug_of joseph_day.LD.observed);
let joseph_slug = "joseph-husband-of-the-blessed-virgin-mary" in
let departure =
List.find_opt (fun (c, _) -> slug_of c = joseph_slug) joseph_day.LD.transferred_out
in
- match departure with
+ let eighteen_march = Date.add_days palm_sunday (-1) in
+ (match departure with
| None -> Alcotest.fail "expected Joseph to be transferred_out from 19 March 2062 (impeded by Palm Sunday)"
| Some (_, target) ->
- (* Concrete, not merely qualitative: 2062-04-04 is the actual pinned
- value on shipped data today (Easter + 9, one day past the
- Annunciation's own Easter + 8 -- see this test's own header). A
- future data or Calendar change that moves it is meant to be
- caught here; the point being pinned is the SHAPE (forward, past
- Easter, not 18 March), asserted again below in a form that does
- not depend on the exact day. *)
Alcotest.(check string)
- "KNOWN WRONG (Normae n.56(f) unimplemented, precedence_of.mli): Joseph lands forward, past \
- Easter (currently Easter+9, one day beyond the Annunciation's own Easter+8), not the \
- rubrically-correct 18 March (backward)"
- "2062-04-04" (Date.to_iso8601 target);
- Alcotest.(check bool) "not 18 March -- confirming this really is the anticipation defect, not a \
- coincidence" false
- (Date.compare target (Date.add_days palm_sunday (-1)) = 0);
- Alcotest.(check bool) "and it lands on or after Easter itself (rule 3's forward search, not a \
- backward anticipation)" true
- (Date.compare target easter_2062 >= 0)
+ "Normae n.56(f): Joseph is anticipated BACKWARD to 18 March 2062, not carried forward"
+ "2062-03-18" (Date.to_iso8601 target);
+ Alcotest.(check bool) "which really is the day before Palm Sunday" true
+ (Date.compare target eighteen_march = 0));
+ let march18_day = day_of eighteen_march in
+ Alcotest.(check string) "and 18 March 2062 itself observes St Joseph (not Cyril of Jerusalem's \
+ ad-libitum memorial, not the ordinary Lenten Saturday feria)"
+ joseph_slug (slug_of march18_day.LD.observed);
+ let joseph_arrived =
+ match march18_day.LD.transferred_in with
+ | Some c -> slug_of c = joseph_slug
+ | None -> false
+ in
+ Alcotest.(check bool) "and 18 March 2062 records the arrival (transferred_in), not a silent placement"
+ true joseph_arrived;
+ (* {!Colitur_kernel.Rite.t.transfer_target}'s own second obligation
+ (terminate) reduces, for this liturgical year, to the whole-year
+ placement pass reaching a fixed point at all -- {!Val.run}'s own
+ "unconverged" check is exactly {!Colitur_kernel.Calendar}'s round
+ guard surfaced as a named failure (calendar.ml's own
+ [unconverged_reason]). *)
+ let failures = run 2061 in
+ Alcotest.(check bool) "the whole 2061 liturgical year still reaches a fixed point (no unconverged \
+ transfer placement anywhere in it)" false
+ (List.exists (fun f -> f.Val.check = "unconverged") failures)
(* Second NEWLY-FOUND defect, pinned the same way (see
[is_known_holy_family_fallback_gap]'s own citation just above for the
@@ -402,9 +406,9 @@ let suite =
Alcotest.test_case "2005-2050: transfers reach a fixed point, nothing lost or duplicated" `Slow
test_transfers_reach_a_fixed_point_2005_2050;
Alcotest.test_case
- "KNOWN OPEN DEFECT pinned, not fixed: St Joseph on Palm Sunday 2062 transfers forward, not \
- backward per Normae n.56(f)" `Quick
- test_joseph_palm_sunday_2062_known_wrong_forward_transfer;
+ "RESOLVED (W1): St Joseph on Palm Sunday 2062 is anticipated backward to 18 March per Normae \
+ n.56(f), converges, and is recorded as an arrival" `Quick
+ test_joseph_palm_sunday_2062_anticipated_backward;
Alcotest.test_case
"NEWLY-FOUND DEFECT pinned, not fixed: Holy Family's Normae n.35(a) 30-December fallback is \
unreached when Christmas Day is a Sunday (1583)" `Quick