diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 01:19:37 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 01:19:52 +0200 |
| commit | 9ce4527550fed24036f6f116474c1f67714f5194 (patch) | |
| tree | fa941f55a64c930b3c8c12606d1c7a3eadcf3aaa | |
| parent | 4774ef07edc3c197cbd0134e2839eeee2701db2e (diff) | |
| download | colitur-9ce4527550fed24036f6f116474c1f67714f5194.tar.gz colitur-9ce4527550fed24036f6f116474c1f67714f5194.zip | |
rite(ef): the Annunciation's RG96 exception is conditional, not unconditional
The register was transcribed faithfully but was itself wrong: RG 96's
Attamen (a), primary-source-verified from the scans and now corrected
in the register, reads 'festum Annuntiationis B. Mariae Virg., quando
est transferendum post Pascha, transfertur ... in feriam II post
dominicam in albis' -- the Monday-after-Low-Sunday seat applies ONLY
'quando est transferendum post Pascha', when the feast is to be
transferred PAST EASTER. The unconditional transcription made the
exception fire on every impeded Annunciation regardless of cause.
transfer_target now computes the general RG 96 target first, for every
candidate, and overrides to the Monday after Low Sunday only when that
general target itself falls after Easter Sunday -- testing the rubric's
own condition directly rather than re-deriving a date-proximity rule
from first principles.
Confirmed against three real years the review named: 2007, 2012 and
2057 all previously sent the Annunciation to Easter + 8 (16 April,
16 April, 30 April respectively) when the correct, now-produced target
is the next free day before Easter (26 March in each case -- Passion
Sunday in 2007/2012, Lent III Sunday in 2057). Verified with actual CLI
output for all three, before and after.
Also cites RG 96 Attamen (b), the same primary-source passage, as the
direct authority for All Souls' own move to the following Monday when
impeded by a Sunday -- previously inferred from RG 91 entry 8's
parenthetical plus the general walk, which happened to produce the
right date; now stated directly.
Rewrote the two existing Annunciation unit tests, whose synthetic
occupants no longer trigger the (now correctly conditional) exception,
and added a 2057 regression test using the real Temporal_ef.temporal as
occupant plus a real-data cram pin -- both mutation-verified against
the unconditional reading.
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.ml | 88 | ||||
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.mli | 32 | ||||
| -rw-r--r-- | test/cli.t | 13 | ||||
| -rw-r--r-- | test/test_precedence_ef.ml | 74 |
4 files changed, 152 insertions, 55 deletions
diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index b9b1731..0feb3e0 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -405,9 +405,16 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate) branch that completes Task 7's All Souls fix (register line 334, RG 91 entry 8): All Souls is I class, not a vigil, and not a Sunday slug, so once it loses to an occurring Sunday it still reaches here - and transfers -- to 3 November per the register, but WHERE it lands - is Rite.transfer_target's job (RG 96), not this function's; - disposition only says THAT it moves. *) + and transfers -- to 3 November, now DIRECTLY authorised by RG 96 + Attamen (b) (primary-source-verified 2026-08-12): "Commemoratio + omnium Fidelium defunctorum, quando occurrit cum dominica, + transfertur, tamquam in sedem propriam, in feriam II sequentem" -- + when it coincides with a Sunday, transferred, as to its own proper + seat, to the following Monday. Previously this rested only on entry + 8's own parenthetical plus the general RG 96 walk, which happened to + produce the right date; WHERE it lands either way is + Rite.transfer_target's job, not this function's -- disposition only + says THAT it moves. *) Precedence.Transfer else (* RG 95's other branch: "aut commemorantur aut penitus omittuntur" -- @@ -545,14 +552,23 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) already draws for RG 111 ({!dignity}, not [band]). *) let is_blocking (rank : Vocab_ef.rank) = rank = Vocab_ef.Class1 || rank = Vocab_ef.Class2 -(* RG 96's own named exception, register-transcribed: "(Annunciation - exception): -> Monday after Low Sunday." Identified by slug -- the same - convention this file already uses to pick out one specific celebration - from a rank/status shape shared by many others ({!nativity_octave_prefix}, - [is_ember_18]'s date anchors) -- not an RG citation itself: RG 96 does not - encode how a computer recognises "the Annunciation", only what happens to - it once recognised. data/ef/sanctoral.sexp's own bootstrapped slug (Task - 10), reused verbatim rather than guessed. *) +(* RG 96's own named exception (docs/research/rules-register.md §4, + "Transfer/translation", RG 96 Attamen (a) -- primary-source-verified + 2026-08-12, corrected from an earlier unconditional transcription; see + the register's own correction note). Verbatim: "festum Annuntiationis + B. Mariae Virg., quando est transferendum post Pascha, transfertur, + tamquam in sedem propriam, in feriam II post dominicam in albis" -- when + [the feast] is to be transferred PAST EASTER, [it] is transferred, as to + its own proper seat, to the Monday after Low Sunday. The exception is + CONDITIONAL on that "past Easter" clause -- {!transfer_target} tests it + by comparing the GENERAL RG 96 target against Easter itself, not by + testing the date here. Identified by slug -- the same convention this + file already uses to pick out one specific celebration from a rank/ + status shape shared by many others ({!nativity_octave_prefix}, + [is_ember_18]'s date anchors) -- not an RG citation itself: RG 96 does + not encode how a computer recognises "the Annunciation", only what + happens to it once recognised. data/ef/sanctoral.sexp's own bootstrapped + slug (Task 10), reused verbatim rather than guessed. *) let annunciation_slug = "annunciation-of-the-blessed-virgin-mary" (* Not an RG citation -- a defensive engineering ceiling, the same role @@ -620,27 +636,39 @@ let rec search_from (occupant : Date.t -> Vocab_ef.rank Celebration.t) (steps : (* [transfer_target]'s contract (rite.mli): total, terminating, and its result is always strictly after [origin]. Terminating: [search_from]'s - own structural bound, above. Strictly after [origin]: the ordinary branch - starts the search at [Date.add_days origin 1] and [search_from] only ever - advances forward from its own starting point, so the result is always >= - origin + 1. The Annunciation branch starts instead at the Monday after - Low Sunday for [origin]'s own civil year -- NOT provably later than - [origin] by the code alone, but true of every representable year: the - Annunciation's [origin] is always 25 March (Date_spec.Fixed in + own structural bound, above. Strictly after [origin]: the general branch + is exactly [search_from]'s own result starting at [Date.add_days origin + 1], which only ever advances forward from there, so it is always >= + origin + 1. The Annunciation branch, when it fires, instead searches from + the Monday after Low Sunday for [origin]'s own civil year -- NOT provably + later than [origin] by the code alone, but true of every representable + year: the Annunciation's [origin] is always 25 March (Date_spec.Fixed in data/ef/sanctoral.sexp), Easter always falls within that SAME civil year in [22 March, 25 April] (Computus's own documented range, register §0), so Low Sunday (Easter + 7) falls in [29 March, 2 May] and the Monday - after it in [30 March, 3 May] -- always after 25 March. *) + after it in [30 March, 3 May] -- always after 25 March. + + RG 96 Attamen (a) (see {!annunciation_slug}'s own comment) makes the + Annunciation exception CONDITIONAL on the general walk carrying the + feast past Easter -- so the general target is always computed FIRST, + for every candidate, and only overridden for the Annunciation when that + target itself falls after Easter Sunday. A version of this function that + tested the DATE of [origin] instead (e.g. "is 25 March within some fixed + window of Easter") would be re-deriving the register's own "quando est + transferendum post Pascha" condition from first principles, exactly the + kind of guess this project's "a wrong citation is worse than a missing + one" rule warns against; comparing the general target against Easter + directly tests the rubric's own words. *) let transfer_target (c : Vocab_ef.rank Precedence.candidate) (origin : Date.t) (occupant : Date.t -> Vocab_ef.rank Celebration.t) : Date.t = - let start = - if Slug.to_string c.Precedence.cel.Celebration.slug = annunciation_slug then - (* Low Sunday = Easter + 7 (register §0, temporal_ef.ml's [off 7]); the - Monday after it = Easter + 8. Searched onward from there exactly - like the general case searches from [origin + 1] -- "only if that - day is itself blocked" (rite.mli) is [search_from]'s ordinary - behaviour, not a second mechanism. *) - Date.add_days (Computus.gregorian_easter (Date.year origin)) 8 - else Date.add_days origin 1 - in - search_from occupant 0 start + let general_target = search_from occupant 0 (Date.add_days origin 1) in + let is_annunciation = Slug.to_string c.Precedence.cel.Celebration.slug = annunciation_slug in + let easter = Computus.gregorian_easter (Date.year origin) in + if is_annunciation && Date.compare general_target easter > 0 then + (* Low Sunday = Easter + 7 (register §0, temporal_ef.ml's [off 7]); the + Monday after it = Easter + 8. Searched onward from there exactly + like the general case searches from [origin + 1] -- "only if that + day is itself blocked" (rite.mli) is [search_from]'s ordinary + behaviour, not a second mechanism. *) + search_from occupant 0 (Date.add_days easter 8) + else general_target diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli index 7318ccd..8234bf3 100644 --- a/lib/rites/rite_ef/precedence_ef.mli +++ b/lib/rites/rite_ef/precedence_ef.mli @@ -187,11 +187,23 @@ val annunciation_slug : string RG 96's own rule: the next following day whose currently-resolved occupant is not I or II class (read off [Vocab_ef.rank], RG 8's dignity -- not {!band}'s finer occurrence-table entry, the same distinction - {!admit} draws for RG 111). RG 96's own named exception: the - Annunciation ({!annunciation_slug}) does not search from [origin + 1] at - all -- it starts at the Monday after Low Sunday for [origin]'s own civil - year, searching onward from there only if that day is itself occupied by - a blocking class. + {!admit} draws for RG 111). This general target is computed for EVERY + candidate, always, first. + + RG 96's own named exception (Attamen (a), primary-source-verified -- + see {!annunciation_slug}'s comment for the Latin and the register's own + correction note): for the Annunciation specifically, IF that general + target would fall after Easter Sunday itself ("quando est transferendum + post Pascha" -- when it is to be transferred past Easter), the + Annunciation is placed instead at the Monday after Low Sunday (its + [sedes propria]), searching onward from there only if that day is + itself occupied by a blocking class. The exception is CONDITIONAL, not + unconditional: an Annunciation impeded for a reason that resolves + BEFORE Easter (e.g. an ordinary Lent Sunday with a free feria the next + day) takes the general target like any other I-class feast. Operationally + the condition holds exactly when 25 March falls close enough to Easter + that the general walk crosses it -- concretely, when 25 March itself + falls within Holy Week or Easter Week. Total, terminating, and its result is always strictly later than [origin] -- {!Colitur_kernel.Rite.t}.transfer_target's own obligations, @@ -205,10 +217,10 @@ val annunciation_slug : string [domain_max_date]), not by an argument about the real 1962 calendar's own structure, so a rite/data shape this function has not anticipated fails FINITELY rather than hanging or crashing the caller. Strictly - later than [origin]: the ordinary search - starts at [origin + 1] and only ever advances forward from there; the - Annunciation's own starting point is provably later than 25 March for - every representable year (Easter's documented range, register §0) -- - see the .ml for the full argument. *) + later than [origin]: the general search starts at [origin + 1] and only + ever advances forward from there; the Annunciation's own alternate + starting point is provably later than 25 March for every representable + year (Easter's documented range, register §0) -- see the .ml for the + full argument. *) val transfer_target : Vocab_ef.rank Precedence.candidate -> Date.t -> (Date.t -> Vocab_ef.rank Celebration.t) -> Date.t @@ -101,6 +101,19 @@ Sunday: 2 Nov 2026 is a Monday (1 Jan 2026 is a Thursday, same day-of-year (week 23: Pentecost 2026 is 24 May (colitur easter 2026); 24 May - 2 Nov is 162 days, floor_div(162, 7) = 23 -- same formula, same independent check.) +RG 96's Annunciation exception (25 March) is CONDITIONAL, not unconditional +-- fix round 1, coordinator review, register corrected 2026-08-12. In 2057, +25 March is Lent III Sunday (I class, impedes it); 26 March is an ordinary +Lent feria (III class, well before Easter, 22 April 2057), so the GENERAL +RG 96 target -- not the Monday after Low Sunday -- is what governs, since +the general walk never crosses Easter. Before this fix the unconditional +reading sent it to 30 April (Easter + 8) instead: + + $ colitur day 2057 | grep '^2057-03-26 ' + 2057-03-26 monday lent 3 annunciation-of-the-blessed-virgin-mary class-1 white +ef-lent-3-monday + $ colitur day 2057 | grep -c 'annunciation-of-the-blessed-virgin-mary' + 1 + A year outside the supported domain is rejected (exit 2): $ colitur day 1000 diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index 0973a93..aa98686 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -844,36 +844,76 @@ let test_transfer_target_general_multi_step_search () = Alcotest.(check string) "lands on the first day past the blocked run" "2026-01-13" (D.to_iso8601 target) -(* RG 96's Annunciation exception: starts the search at the Monday after Low - Sunday, NOT [origin + 1] -- occupant is unconditionally free, so a - general-path implementation would return [origin + 1] (26 March), a date - this test explicitly rules out as well as pinning the real expected one, - so the assertion genuinely discriminates the two starting points rather - than merely checking "some date after origin". *) +(* Coordinator review (fix round 1): RG 96 Attamen (a) (register-transcribed, + primary-source-verified) makes the Annunciation exception CONDITIONAL on + the general RG 96 walk carrying the feast past Easter Sunday -- NOT + unconditional as the first transcription had it. The occupant here blocks + every day from [origin + 1] through the day after Easter (26 March - 6 + April 2026 inclusive), so the GENERAL target itself would land at 7 + April -- after Easter (5 April) -- which is exactly the trigger + condition, not merely "the Annunciation is impeded at all". *) let test_transfer_target_annunciation_starts_at_monday_after_low_sunday () = let origin = mk 2026 3 25 in - let occupant = occupant_blocking_on [] in + let easter_2026 = Comp.gregorian_easter 2026 in + let blocked_through_day_after_easter = + let rec range a b = if D.compare a b > 0 then [] else a :: range (D.add_days a 1) b in + range (D.add_days origin 1) (D.add_days easter_2026 1) + in + let occupant = occupant_blocking_on blocked_through_day_after_easter in let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer PE.annunciation_slug in let target = PE.transfer_target c origin occupant in - let monday_after_low_sunday = D.add_days (Comp.gregorian_easter 2026) 8 in - Alcotest.(check string) "lands on the Monday after Low Sunday (Easter + 8)" + let monday_after_low_sunday = D.add_days easter_2026 8 in + Alcotest.(check string) "lands on the Monday after Low Sunday (Easter + 8), the general \ + walk having crossed Easter itself" (D.to_iso8601 monday_after_low_sunday) (D.to_iso8601 target); - Alcotest.(check bool) "NOT the general path's origin + 1 (discriminates the branch)" true - (D.compare target (D.add_days origin 1) <> 0) + Alcotest.(check bool) "NOT the general target (2 days after Easter, discriminates the branch)" + true + (D.compare target (D.add_days easter_2026 2) <> 0) (* RG 96's own qualifier on the exception -- "searching onward from there only if that day is itself blocked" (rite.mli) -- is [search_from]'s - ORDINARY behaviour, not a second mechanism: block the Monday after Low - Sunday itself and confirm the search continues exactly one more day. *) + ORDINARY behaviour, not a second mechanism: same blocked run as above + (forcing the general target past Easter, so the exception fires), PLUS + the Monday after Low Sunday itself blocked, confirming the search + continues exactly one more day from there. *) let test_transfer_target_annunciation_searches_onward_if_blocked () = let origin = mk 2026 3 25 in - let monday_after_low_sunday = D.add_days (Comp.gregorian_easter 2026) 8 in - let occupant = occupant_blocking_on [ monday_after_low_sunday ] in + let easter_2026 = Comp.gregorian_easter 2026 in + let monday_after_low_sunday = D.add_days easter_2026 8 in + let blocked = + let rec range a b = if D.compare a b > 0 then [] else a :: range (D.add_days a 1) b in + range (D.add_days origin 1) (D.add_days easter_2026 1) @ [ monday_after_low_sunday ] + in + let occupant = occupant_blocking_on blocked in let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer PE.annunciation_slug in let target = PE.transfer_target c origin occupant in Alcotest.(check string) "searches onward one more day when that Monday is itself blocked" (D.to_iso8601 (D.add_days monday_after_low_sunday 1)) (D.to_iso8601 target) +(* THE REGRESSION PIN (coordinator review): the bug an unconditional + exception produced. 25 March 2057 is Lent III Sunday (I class, RG 91 + entry 6), impeding the Annunciation; 26 March 2057 is an ordinary Lent + feria (III class, well before Easter, 22 April 2057) -- the general RG + 96 target. The general target does NOT fall after Easter, so the + exception must NOT fire: the Annunciation lands on 26 March, not 13 + April (Easter + 8), which is what the unconditional reading produced + (verified by reverting the fix and re-running this exact test -- see the + task report's mutation record). Uses the REAL [Temporal_ef.temporal] as + [occupant] (not a synthetic stand-in), the same coupling-safety + convention [of_temporal]'s callers use elsewhere in this file, so this + is also effectively an end-to-end check of the real 2057 calendar + shape, not just the search's own logic in isolation. *) +let test_transfer_target_annunciation_not_overridden_when_general_target_precedes_easter () = + let origin = mk 2057 3 25 in + Alcotest.(check string) "25 March 2057 is a Sunday (Lent III)" "sunday" + (D.weekday_to_string (D.weekday origin)); + let occupant d = (T.temporal d).Colitur_kernel.Temporal.office in + let c = cand ~origin:P.Sanctoral ~layer:PE.universal_layer PE.annunciation_slug in + let target = PE.transfer_target c origin occupant in + Alcotest.(check string) "lands on 26 March 2057 (the general RG96 target), NOT the \ + Annunciation exception's Monday after Low Sunday" + "2057-03-26" (D.to_iso8601 target) + (* rite.mli's own obligations on [transfer_target] (Task 11 brief): the call must TERMINATE and its result must be STRICTLY AFTER [origin], even for a rite/data shape this function cannot have anticipated -- an occupant that @@ -974,6 +1014,10 @@ let suite = Alcotest.test_case "transfer_target: Annunciation exception searches onward if that Monday is blocked" `Quick test_transfer_target_annunciation_searches_onward_if_blocked; + Alcotest.test_case + "transfer_target: Annunciation NOT overridden when the general target precedes Easter \ + (2057 regression)" + `Quick test_transfer_target_annunciation_not_overridden_when_general_target_precedes_easter; Alcotest.test_case "transfer_target: terminates and stays forward under a pathological occupant" `Quick test_transfer_target_terminates_under_pathological_occupant; Alcotest.test_case "transfer_target: does not raise probing past the domain ceiling" `Quick |
