diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/test_calendar.ml | 2 | ||||
| -rw-r--r-- | test/test_differential.ml | 23 | ||||
| -rw-r--r-- | test/test_golden.ml | 84 | ||||
| -rw-r--r-- | test/test_oracle.ml | 18 | ||||
| -rw-r--r-- | test/test_precedence.ml | 2 | ||||
| -rw-r--r-- | test/test_precedence_ef.ml | 10 | ||||
| -rw-r--r-- | test/test_sanctoral_ef.ml | 72 | ||||
| -rw-r--r-- | test/test_validate.ml | 12 |
8 files changed, 203 insertions, 20 deletions
diff --git a/test/test_calendar.ml b/test/test_calendar.ml index 59624aa..132a466 100644 --- a/test/test_calendar.ml +++ b/test/test_calendar.ml @@ -82,7 +82,7 @@ module Fixture = struct test below a genuine Precedence-native omission (distinct from a deferred one) to exercise. *) let rules : (season, rank) P.rules = - { P.band; disposition; + { P.vigil_feast = (fun _ -> None); band; disposition; admit = (fun ~observed:_ ~temporal:_ cs -> List.filteri (fun i _ -> i < 1) cs |> List.map (fun (c, p, (_ : int)) -> (c, p))) } diff --git a/test/test_differential.ml b/test/test_differential.ml index 8423888..8c1dd8d 100644 --- a/test/test_differential.ml +++ b/test/test_differential.ml @@ -1089,6 +1089,13 @@ let rg69_one_hop_slugs = [ "ef-christmas-1-saturday" ] (* [layer_c_reason l c diffs] returns the [data/ef/expected-divergences.sexp] [id] this row-pair's remaining (post Layer A/B) diff set belongs to, or [None] if nothing here explains it (a genuine, uncovered failure). *) +(* The three vigils RG 33's third trigger can actually reach on the shipped + universal calendar -- see C39 in [layer_c_reason]. Named here rather than + written inline so a future slug rename fails in one visible place. *) +let rg33_vigil_slugs = + [ "vigil-of-st-lawrence"; "vigil-of-the-nativity-of-st-john-the-baptist"; + "vigil-of-sts-peter-paul" ] + let layer_c_reason (l : row) (c : row) diffs = let m = month_of_date l.date and d = day_of_date l.date in if diffs = [] then None @@ -1135,6 +1142,22 @@ let layer_c_reason (l : row) (c : row) diffs = colour-only Saturday still falls to C17 and cannot be absorbed here. *) else if is_bvm_saturday_row c diffs then Some "C17" else if diffs = [ Colour_f ] && List.mem c.slug audit_colour_corrected_slugs then Some "C18" + (* C39 -- RG 33's THIRD omission trigger, implemented 2026-08-21. lectio + omits a II/III-class vigil that itself falls on a Sunday (the rule's + SECOND trigger) but not one whose FEAST fails to keep its own day, so on + these days lectio still shows the vigil and colitur no longer does. + + Gated on LECTIO's own slug being one of the three vigils this clause can + reach, and on colitur's NOT being a vigil -- identity on both sides, not + merely a diff-set shape, the same discipline C6 and C14 were given. The + three are exhaustive here: the Assumption's and the Ascension's vigils + can never qualify, because their I-class feasts always keep their own + day (measured over the whole 1583-9999 domain, 0 occurrences). *) + else if + List.mem l.slug rg33_vigil_slugs + && (not (List.mem c.slug rg33_vigil_slugs)) + && subset diffs [ Slug_f; Rank; Colour_f; First_f; Gospel_f ] + then Some "C39" (* C25 -- CLOSED, BVM Saturday Mass (2026-08-17), predicate removed: 0 rows. Its days are unoccupied IV-class Saturdays, so they carry RG 78's office and now RG 309(a)'s Mass for it, which answers before step 3's walkback diff --git a/test/test_golden.ml b/test/test_golden.ml index 33ca362..14fbec4 100644 --- a/test/test_golden.ml +++ b/test/test_golden.ml @@ -1327,14 +1327,74 @@ let test_rogation_wednesday_yields_to_a_feast_2026 () = white" -- which colitur already emits correctly (ef-ascension-vigil, white, pinned by the two Rogation Wednesday cases just above). - Pinned in years where each vigil is actually OBSERVED rather than impeded: - 9 August 2025 is a Saturday and 14 August 2025 a Thursday, both checked - with `date -d`. In 2026, 9 August is a Sunday and the vigil never appears. *) -let test_vigil_of_st_lawrence_is_violet_2025 () = - check ~msg:"RG 128: III-class vigil outside Paschaltide is violet, not red" 2025 8 9 - "2025-08-09 saturday season=time-after-pentecost week=8 slug=vigil-of-st-lawrence \ + Pinned in years where each vigil is actually OBSERVED rather than impeded. + 14 August 2025 is a Thursday and the Assumption always keeps 15 August (I + class, nothing in the universal calendar outranks it), so that pin is + stable in any year the vigil's own day is free. + + ST LAWRENCE'S PIN MOVED 2025 -> 2027 when RG 33's third omission trigger + was implemented. Its original comment reasoned only about the vigil's OWN + weekday -- "9 August 2025 is a Saturday", true, so RG 33's Sunday trigger + does not fire -- and missed that the rule also looks at the FEAST: 10 + August 2025 is a Sunday, the II-class Sunday reduces St Lawrence to a + commemoration, and "vel si festum cui praemittitur... ad commemorationem + reduci contingat" omits the vigil entirely. The day now keeps the BVM + Saturday Office instead, pinned separately just below. 9 August 2027 is a + Monday and 10 August 2027 a Tuesday (both `date -d`-checked), so St + Lawrence keeps his own day there and the vigil genuinely is observed -- + which is what this pin has always been trying to assert about RG 128's + colour rule. *) +let test_vigil_of_st_lawrence_is_violet_2027 () = + check ~msg:"RG 128: III-class vigil outside Paschaltide is violet, not red" 2027 8 9 + "2027-08-09 monday season=time-after-pentecost week=12 slug=vigil-of-st-lawrence \ rank=class-3 colour=violet subject=saint name_la=- comms=[romanus:ordinary] in=- out=[]" +(* RG 33's THIRD omission trigger -- "vel si festum cui praemittitur in alium + diem transferri aut ad commemorationem reduci contingat" -- on the shipped + universal calendar, no overlay involved. 10 August 2025 is a Sunday; the + II-class Sunday outranks St Lawrence's II-class feast (RG 15), so the feast + is "ad commemorationem reducta" on its own day and the vigil of 9 August is + "penitus omissa". + + This is the clause's most common live shape by a wide margin: 1 199 of the + 1 744 days it fires on across 1583-9999 are St Lawrence's vigil dropped + because 10 August fell on a Sunday. The day does not become a bare feria -- + 9 August 2025 is a Saturday, so RG 78's votive Office of the BVM takes the + freed IV-class Saturday, which is why this pin reads white and subject=bvm. + St Romanus keeps his commemoration either way (RG 111). + + NOT independently witnessed, and an earlier version of this comment wrongly + said it was. The published Ordo does omit the vigil on this date -- but it + also omits it on 2027-08-09, where this clause says to KEEP it, so it never + shows St Lawrence's vigil at all in the years it covers and cannot + corroborate anything here. This pin rests on RG 33's own text and on the + fact that lectio never evaluates the clause (it appears nowhere in its + production code); see C39's own note in + data/ef/expected-divergences.sexp. *) +let test_rg33_vigil_omitted_when_its_feast_is_commemorated_2025 () = + check + ~msg:"RG 33: 10 Aug 2025 is a Sunday, so St Lawrence is only commemorated and his vigil is \ + omitted entirely -- the BVM Saturday Office (RG 78) takes the freed day" + 2025 8 9 + "2025-08-09 saturday season=time-after-pentecost week=8 \ + slug=ef-time-after-pentecost-8-saturday rank=class-4 colour=white subject=bvm \ + name_la=Officium sanctae Mariae in sabbato comms=[romanus:ordinary] in=- out=[]" + +(* The same clause's OTHER half, "in alium diem transferri". 24 June 2038 is + impeded by the Sacred Heart (I class, movable), so the Nativity of St John + the Baptist is TRANSFERRED under RG 96 and its vigil of 23 June is omitted. + 478 days across the domain take this shape (a further 67 via Corpus + Christi). Pinned in 2038 because that year already carries an oracle + fixture, so the divergence this creates is visible to layer 4 as well. *) +let test_rg33_vigil_omitted_when_its_feast_transfers_2038 () = + check + ~msg:"RG 33: the Sacred Heart takes 24 June 2038, St John the Baptist transfers, \ + and his vigil of 23 June is omitted entirely" + 2038 6 23 + "2038-06-23 wednesday season=time-after-pentecost week=1 \ + slug=ef-time-after-pentecost-1-wednesday rank=class-4 colour=green subject=temporal \ + name_la=- comms=[] in=- out=[]" + let test_vigil_of_the_assumption_is_violet_2025 () = check ~msg:"RG 128: II-class vigil outside Paschaltide is violet, not white" 2025 8 14 "2025-08-14 thursday season=time-after-pentecost week=9 slug=vigil-of-the-assumption \ @@ -1342,8 +1402,16 @@ let test_vigil_of_the_assumption_is_violet_2025 () = let suite = ( "golden pins (known-tricky years)", - [ Alcotest.test_case "RG128: St Lawrence's vigil is violet (2025)" `Quick - test_vigil_of_st_lawrence_is_violet_2025; + [ Alcotest.test_case "RG128: St Lawrence's vigil is violet (2027)" `Quick + test_vigil_of_st_lawrence_is_violet_2027; + Alcotest.test_case + "RG33 third trigger: the vigil is omitted when its feast is reduced to a commemoration \ + (2025-08-09, St Lawrence)" + `Quick test_rg33_vigil_omitted_when_its_feast_is_commemorated_2025; + Alcotest.test_case + "RG33 third trigger: the vigil is omitted when its feast is transferred away \ + (2038-06-23, St John the Baptist)" + `Quick test_rg33_vigil_omitted_when_its_feast_transfers_2038; Alcotest.test_case "RG128: the Assumption's vigil is violet (2025)" `Quick test_vigil_of_the_assumption_is_violet_2025; Alcotest.test_case "Easter extreme: 1598 earliest (22 Mar, Gauss-verified)" `Quick diff --git a/test/test_oracle.ml b/test/test_oracle.ml index 7ee3848..f1d1a89 100644 --- a/test/test_oracle.ml +++ b/test/test_oracle.ml @@ -1878,6 +1878,21 @@ let classify_2038 (c : colitur_row) (o : oracle_row) diffs = else if contains title "Mass of the B. V. M." then Some "BVM-SATURDAY-MASS (M26 shape 1)" else if first_is "Col 1:12-20" then Some "CHRIST-THE-KING-WEEK (M26 shape 2b)" else if diffs = [ Colour_f ] && List.mem c.c_observed_slug m21_2038_slugs then Some "COLOUR (M21)" + (* RG 33's third omission trigger (register 6.31, implemented + 2026-08-21). The Sacred Heart takes 24 June 2038, so the Nativity of St + John the Baptist is transferred under RG 96 and the vigil of 23 June is + "penitus omissa". missalemeum keeps it -- as does lectio, whose own C39 + covers this same date on the differential side, which is exactly what + one expects of two points on one lineage. Gated on the ORACLE's own + title rather than on the date, so a different divergence arriving on 23 + June could not be silently absorbed here. Cited to the register rather + than to an allow-list id because data/ef/expected-divergences-missalemeum.sexp + is scoped to the 2026-2027 window, which contains no instance of this + clause -- the same convention PERPETUA-COMMON and MISSAL-TWO-PART-CITATION + already follow. The tracked, full account is C39's own note in + data/ef/expected-divergences.sexp. *) + else if contains title "Vigil of the Nativity of St. John the Baptist" then + Some "RG33-THIRD-TRIGGER (colitur; register 6.31, see C39)" else if first_is "Judith" then Some "JUDITH (M28, register 6.6)" else if c.c_observed_slug = "sts-felicitas-perpetua" then Some "PERPETUA-COMMON (register 6.8)" else if c.c_observed_slug = "ef-nativity" then Some "CHRISTMAS-MULTI-MASS (M27)" @@ -2108,6 +2123,9 @@ let test_2038_class_counts () = ("COMM-ONLY-TIE-BREAK (RG-under-determined convention -- register 6.16)", 1); ("JOSEPH-ANNUNCIATION-TRANSFER-ORDER (C14's own collision)", 2); ("JUDITH (M28, register 6.6)", 2); + (* RG 33's third trigger: 23 June only -- the Sacred Heart takes 24 + June 2038 and St John the Baptist transfers. See M31. *) + ("RG33-THIRD-TRIGGER (colitur; register 6.31, see C39)", 1); ("MISSAL-TWO-PART-CITATION (register 6.9)", 1); ("COMM-IDENTITY-UNRESOLVED (temporal-origin, no English name -- M15's limit)", 2); ("PERPETUA-COMMON (register 6.8)", 1); diff --git a/test/test_precedence.ml b/test/test_precedence.ml index ce75ab6..acc89fc 100644 --- a/test/test_precedence.ml +++ b/test/test_precedence.ml @@ -17,7 +17,7 @@ let ctx = (* Band: Hi beats Lo. Temporal breaks a tie in its own favour. *) let rules = - { P.band = (fun _ c -> (match c.P.cel.Cel.rank with Hi -> 10 | Lo -> 20) + { P.vigil_feast = (fun _ -> None); band = (fun _ c -> (match c.P.cel.Cel.rank with Hi -> 10 | Lo -> 20) - (match c.P.origin with P.Temporal -> 1 | P.Sanctoral -> 0)); disposition = (fun ~winner:_ ~loser -> diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index 3f3e070..6bd7bfd 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -370,7 +370,7 @@ let test_all_souls_yields_to_sunday () = in let all_souls = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls" in let rules = - { P.band = (fun c cd -> PE.band c cd); + { P.vigil_feast = (fun _ -> None); band = (fun c cd -> PE.band c cd); disposition = (fun ~winner:_ ~loser:_ -> P.Omit); admit = (fun ~observed:_ ~temporal:_ cs -> List.map (fun (c, p, (_ : int)) -> (c, p)) cs) } in @@ -1443,7 +1443,11 @@ let test_admit_uses_precedence_not_slug_when_they_disagree () = [rules] deliberately reused, not rebuilt per test, since it is always the same three real functions. *) -let real_rules = { P.band = PE.band; disposition = PE.disposition; admit = PE.admit } +(* The real EF ruleset, including RG 33's third-trigger hook -- a stub here + would make any test built on [real_rules] blind to it. *) +let real_rules = + { P.vigil_feast = PE.vigil_feast; band = PE.band; disposition = PE.disposition; + admit = PE.admit } (* I-class day, zero admitted: the strongest form of "does not vanish" -- EVERY commemoration due is dropped (RG 111: "none save one privileged", @@ -1929,7 +1933,7 @@ let test_all_souls_transfers_end_to_end () = in let all_souls = cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls" in let rules = - { P.band = PE.band; disposition = PE.disposition; + { P.vigil_feast = (fun _ -> None); band = PE.band; disposition = PE.disposition; admit = (fun ~observed:_ ~temporal:_ cs -> List.map (fun (c, p, (_ : int)) -> (c, p)) cs) } in let resolution = P.resolve rules day_ctx ~temporal:sunday ~sanctoral:[ all_souls ] in diff --git a/test/test_sanctoral_ef.ml b/test/test_sanctoral_ef.ml index 5c46edf..b237f25 100644 --- a/test/test_sanctoral_ef.ml +++ b/test/test_sanctoral_ef.ml @@ -165,6 +165,72 @@ let test_commemoration_only_has_no_readings () = Alcotest.(check int) "a commemoration contributes an oration, not a reading" 0 (List.length bad) +(* RG 33's third omission trigger reads a hand-written table in + [Precedence_ef.vigil_feast_table] pairing each vigil with the feast it + precedes. That table is the one part of the rule no type can check: a slug + that matches nothing in the data makes the rule silently inert for that + entry, and a vigil ADDED to the data but not to the table is never omitted + at all. Both directions are asserted here against the shipped file. + + The Ascension's vigil is excluded from the data-side checks because it is + TEMPORAL (Temporal_ef builds it off an Easter offset) and correctly absent + from the sanctoral layer -- asserted explicitly below rather than skipped, + so that the exclusion cannot quietly grow to cover a real miss. *) +let ascension_vigil = "ef-ascension-vigil" + +let test_vigil_feast_table_slugs_all_exist () = + let l = load () in + let mem slug = + match Colitur_kernel.Slug.of_string slug with + | Error e -> Alcotest.failf "%s: not a well-formed slug: %s" slug e + | Ok s -> L.mem l s + in + List.iter + (fun (vigil, feast) -> + let feast = Colitur_kernel.Slug.to_string feast in + if String.equal vigil ascension_vigil then begin + (* BOTH halves of this pair are temporal -- Temporal_ef builds the + vigil at Easter+38 and the feast at Easter+39 -- so neither belongs + in the sanctoral layer. Asserted in the negative rather than + skipped, so the exclusion cannot quietly widen to hide a real miss; + the pair's positive witness is test_temporal_ef's own Ascension + cases and the 2026-05-13 golden pin. *) + Alcotest.(check bool) "ef-ascension-vigil is temporal, not sanctoral" false (mem vigil); + Alcotest.(check bool) "ef-ascension is temporal, not sanctoral" false (mem feast) + end + else begin + Alcotest.(check bool) + (Printf.sprintf "%s is present in the shipped sanctoral" vigil) + true (mem vigil); + Alcotest.(check bool) + (Printf.sprintf "%s (the feast %s precedes) is present in the shipped sanctoral" + feast vigil) + true (mem feast) + end) + PE.vigil_feast_table + +let test_vigil_feast_table_covers_every_shipped_vigil () = + let l = load () in + let tabled = List.map fst PE.vigil_feast_table in + let shipped_vigils = + l.L.entries + |> List.filter (fun (e : V.rank L.entry) -> + let slug = Colitur_kernel.Slug.to_string e.L.cel.Colitur_kernel.Celebration.slug in + PE.is_vigil slug && PE.is_omissible_vigil e.L.cel.Colitur_kernel.Celebration.rank) + |> List.map (fun (e : V.rank L.entry) -> + Colitur_kernel.Slug.to_string e.L.cel.Colitur_kernel.Celebration.slug) + |> List.sort compare + in + let missing = List.filter (fun s -> not (List.mem s tabled)) shipped_vigils in + Alcotest.(check (list string)) + "every II/III-class vigil in the shipped sanctoral is in vigil_feast_table -- a new one \ + added to the data without a table row would never be omitted under RG 33" + [] missing; + (* Pinned so that a vigil VANISHING from the data is caught too: an empty + [shipped_vigils] would satisfy the subset check above vacuously. *) + Alcotest.(check int) "four sanctoral vigils are subject to RG 33" 4 + (List.length shipped_vigils) + let suite = ( "Sanctoral_ef (data/ef/sanctoral.sexp)", [ Alcotest.test_case "load succeeds and counts match the source INI" `Quick @@ -176,4 +242,8 @@ let suite = Alcotest.test_case "sanctoral entries carry their proper readings" `Quick test_sanctoral_carries_propers; Alcotest.test_case "Commemoration_only entries carry no readings" `Quick - test_commemoration_only_has_no_readings ] ) + test_commemoration_only_has_no_readings; + Alcotest.test_case "RG33: every vigil_feast_table slug exists in the shipped data" `Quick + test_vigil_feast_table_slugs_all_exist; + Alcotest.test_case "RG33: vigil_feast_table covers every shipped II/III-class vigil" `Quick + test_vigil_feast_table_covers_every_shipped_vigil ] ) diff --git a/test/test_validate.ml b/test/test_validate.ml index 4d5458f..8b3014a 100644 --- a/test/test_validate.ml +++ b/test/test_validate.ml @@ -247,7 +247,7 @@ module Synthetic = struct fixtures further down override [rules] (and supply a non-empty [layer]), each with its own small, deliberately-shaped ruleset. *) let rules : (season, rank) P.rules = - { P.band = (fun _ _ -> 0); + { P.vigil_feast = (fun _ -> None); band = (fun _ _ -> 0); disposition = (fun ~winner:_ ~loser:_ -> P.Omit); admit = (fun ~observed:_ ~temporal:_ _ -> []) } @@ -425,7 +425,7 @@ module Synthetic = struct let dup_layer = Layer.of_entries ~id:"dup" ~name:"dup" [ dup_entry ] let dup_rules : (season, rank) P.rules = - { P.band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10); + { P.vigil_feast = (fun _ -> None); band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10); disposition = (fun ~winner:_ ~loser:_ -> P.Commemorate P.Ordinary); admit = (fun ~observed:_ ~temporal:_ cs -> @@ -464,7 +464,7 @@ module Synthetic = struct let guard_layer = Layer.of_entries ~id:"guard" ~name:"guard" [ guard_winner_entry; guard_loser_entry ] let guard_rules : (season, rank) P.rules = - { P.band = (fun _ c -> match c.P.origin with P.Temporal -> 50 | P.Sanctoral -> 10); + { P.vigil_feast = (fun _ -> None); band = (fun _ c -> match c.P.origin with P.Temporal -> 50 | P.Sanctoral -> 10); disposition = (fun ~winner:_ ~loser -> match loser.P.cel.Cel.rank with R1 -> P.Transfer | R2 -> P.Commemorate P.Ordinary); @@ -493,7 +493,7 @@ module Synthetic = struct | x :: xs -> if n <= 0 then [] else x :: adm_take (n - 1) xs let adm_rules : (season, rank) P.rules = - { P.band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10); + { P.vigil_feast = (fun _ -> None); band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10); disposition = (fun ~winner:_ ~loser:_ -> P.Commemorate P.Ordinary); admit = (fun ~observed:_ ~temporal:_ cs -> @@ -520,7 +520,7 @@ module Synthetic = struct let collide_layer = Layer.of_entries ~id:"collide" ~name:"collide" [ collide_a_entry; collide_b_entry ] let collide_rules : (season, rank) P.rules = - { P.band = + { P.vigil_feast = (fun _ -> None); band = (fun ctx c -> match c.P.origin with | P.Temporal -> if D.compare ctx.P.date collide_d1 = 0 then 5 else 50 @@ -543,7 +543,7 @@ module Synthetic = struct let clean_sanctoral_layer = Layer.of_entries ~id:"clean" ~name:"clean" [ clean_sanctoral_entry ] let clean_sanctoral_rules : (season, rank) P.rules = - { P.band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10); + { P.vigil_feast = (fun _ -> None); band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10); disposition = (fun ~winner:_ ~loser:_ -> P.Commemorate P.Ordinary); admit = (fun ~observed:_ ~temporal:_ cs -> List.map (fun (c, p, (_ : int)) -> (c, p)) cs) } end |
