From 12b97761019cfa02ca0da8a5fb50ef815d07685c Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 21 Aug 2026 14:42:30 +0200 Subject: feat(ef): implement RG 33's third omission trigger RG 33 omits a II/III-class vigil in three cases: it falls on a Sunday, it falls on a I-class feast, "vel si festum cui praemittitur in alium diem transferri aut ad commemorationem reduci contingat". Only the first two were built; the third was recorded in precedence_ef.ml as unimplemented on the grounds that no witness existed in the shipped data. That reasoning was wrong, and the rule fires on 1 744 days across 1583-9999. Both halves of the clause reduce to one observable question -- is the feast the OBSERVED office on the following day (RG 34 puts it there) -- so the kernel asks it once per candidate, after place_transfers has settled the year. No fixed point is needed: a vigil is a candidate only on its own day, never on its feast's, so suppressing it cannot change what the next day observes. Precedence.rules gains vigil_feast, which returns the slug of the feast a vigil precedes; the kernel cannot infer that itself, because only two of the five vigil/feast pairs share a slug stem. Blast radius, measured pre-change binary vs HEAD over the whole domain and classified: 1 744 days, three shapes, zero unexplained. 1 199 are the feast reduced to a commemoration (10 August on a Sunday, St Lawrence); 478 and 67 are the feast transferred under RG 96 after the Sacred Heart or Corpus Christi takes its day. The Assumption's and the Ascension's vigils never qualify -- their I-class feasts always keep their own day. Independently witnessed, which is unusual here. The published Ordo -- the only witness outside the Divinum Officium -> missalemeum -> lectio lineage -- omits St Lawrence's vigil on 2025-08-09, agreeing with colitur against both engines. That date had been read earlier as an Ordo gap; the Ordo was right, and correcting the misreading is what surfaced this clause. On 2027-08-09 the feast does keep its day and the Ordo omits a vigil colitur correctly keeps, which is a genuine Ordo gap. Allow-lists: C39 (lectio, 10 rows) and a 2038 oracle class citing the register, the 2026-2027 window having no instance. The golden pin asserting St Lawrence's vigil is violet moved 2025 -> 2027; its own comment had reasoned about the vigil's weekday and missed that RG 33 also looks at the feast's. Two new pins cover both shapes of the clause. The vigil/feast table is built with Slug.of_string_exn: mutation testing showed that of_string plus Result.to_option turns a typo into None, which this hook's contract reads as "not a vigil", switching the rule off in silence. Two tests assert the table against the shipped data in both directions. --- test/test_calendar.ml | 2 +- test/test_differential.ml | 23 +++++++++++++ test/test_golden.ml | 80 +++++++++++++++++++++++++++++++++++++++++----- test/test_oracle.ml | 18 +++++++++++ test/test_precedence.ml | 2 +- test/test_precedence_ef.ml | 10 ++++-- test/test_sanctoral_ef.ml | 72 ++++++++++++++++++++++++++++++++++++++++- test/test_validate.ml | 12 +++---- 8 files changed, 199 insertions(+), 20 deletions(-) (limited to 'test') 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..b882596 100644 --- a/test/test_golden.ml +++ b/test/test_golden.ml @@ -1327,14 +1327,70 @@ 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). + + Independently witnessed. The published Ordo -- the project's only witness + outside the Divinum Officium -> missalemeum -> lectio lineage -- omits the + vigil on this exact date, and lectio (and so missalemeum) keeps it; both + allow-lists gained cited entries for that disagreement. *) +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 +1398,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 -- cgit v1.3 From afd82f97ff440add51ea3e87c7ab53938443d5a0 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 21 Aug 2026 15:00:03 +0200 Subject: docs(C39): withdraw the Ordo corroboration claim C39 and the 2025 golden pin both claimed the published Ordo independently confirms RG 33's third trigger, because it omits St Lawrence's vigil on 2025-08-09 where lectio and missalemeum keep it. It does not. The Ordo omits that vigil on 2027-08-09 as well, where the clause says to keep it (10 August is a Tuesday, the feast holds its day), and 2026-08-09 is a Sunday where the rule's first trigger omits it anyway. The 2024 date has no row. So the Ordo never shows St Lawrence's vigil on any date it covers, and the 2025 agreement is a blanket gap coinciding with the right answer, not evidence. The rule itself is unaffected: it rests on RG 33's text, which admits no latitude, and on lectio not evaluating the clause at all -- "RG 33" appears nowhere in its production code, and the third trigger cannot emerge from per-day rank comparison because it turns on what is observed the following day. The divergence is architectural, not a disagreement about the rubric. Recorded rather than deleted: the withdrawn claim is why this entry was first written up as unusually well corroborated. --- data/ef/expected-divergences.sexp | 4 +++- test/test_golden.ml | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'test') diff --git a/data/ef/expected-divergences.sexp b/data/ef/expected-divergences.sexp index bba21c1..8078611 100644 --- a/data/ef/expected-divergences.sexp +++ b/data/ef/expected-divergences.sexp @@ -96,7 +96,9 @@ Two shapes, both in the rubric's own words. SIX rows are 'ad commemorationem red Domain-wide this clause fires on 1 744 of the 3 074 246 days in 1583-9999 -- 1 199 St Lawrence, 347 St John the Baptist, 198 Sts Peter and Paul -- measured by diffing a pre-change binary against HEAD over the whole domain and classifying every changed day: three shapes, zero unexplained, and every one independently re-checked to confirm the feast really is absent from its own day. -INDEPENDENTLY WITNESSED, which is rare in this file and is the reason this entry is verdict colitur rather than open. The published Ordo -- the project's only witness outside the Divinum Officium -> missalemeum -> lectio lineage (see CLAUDE.md's own account of why layers 3 and 4 are one lineage, not two) -- omits the vigil on 2025-08-09, agreeing with colitur against both engines. It had been examined earlier in the same session that implemented this rule and misread as an Ordo GAP, on the reasoning that the Missal's calendarium lists the vigil; the calendarium lists it as a fixed entry, which is not a claim about whether it survives a given year's occurrence. The Ordo was right. Two dates were compared and they part company for the correct reason: on 2027-08-09 (10 August a Tuesday, the feast keeps its day) the Ordo omits a vigil colitur correctly keeps, and that one IS an Ordo gap. +CORRECTED 2026-08-21, BEFORE THIS ENTRY WAS EVER REVIEWED: an earlier version of this note claimed the published Ordo -- the project's one witness outside the Divinum Officium -> missalemeum -> lectio lineage -- INDEPENDENTLY CONFIRMS colitur here, because it omits St Lawrence's vigil on 2025-08-09 where both software engines keep it. That claim does not survive its own check and is withdrawn. The Ordo covers 2024-2027 and omits that vigil on BOTH 2025-08-09 (where this clause says omit) and 2027-08-09 (where it says keep, 10 August being a Tuesday); 2026-08-09 is a Sunday, omitted by the rule's FIRST trigger anyway, and the fixture has no row for 2024-08-09. So the Ordo never shows St Lawrence's vigil on any date it covers, and its agreement on 2025-08-09 is a coincidence of a blanket gap, not evidence of the third trigger. The Ordo has nothing to say here in either direction. Recorded rather than quietly deleted because the withdrawn claim was the reason this entry was first written up as unusually well corroborated, and a reader who saw that framing should be able to see it retracted. + +WHAT THIS ENTRY ACTUALLY RESTS ON, then. Two things, neither of them a second witness. FIRST, the primary text, which admits no interpretive latitude: RG 33 joins three triggers with 'vel'/'aut' and the third is a plain conditional on the feast's own fate. SECOND -- and this is the substantive point -- lectio does not DISAGREE with the clause, it never evaluates it. 'RG 33' appears nowhere in lectio's production code, only in one test file's comments; its Sunday behaviour is emergent from ordinary rank comparison (a II-class Sunday outranks a III-class vigil), not from a transcribed rule. The third trigger cannot emerge that way at all, because it turns on what is observed the day AFTER, and a per-day precedence contest has no access to it. That is exactly why implementing it in colitur required a whole-year pass and a new field on Precedence.rules rather than a branch in disposition. So the divergence is architectural, not exegetical: on these 10 days lectio is not asserting that the vigil survives, it is producing the only answer its shape can produce. missalemeum and Divinum Officium inherit the same limit by lineage. Gated on lectio's own slug being one of the three vigils this clause can reach and colitur's not being a vigil -- identity on both sides, not a diff-set shape. The Assumption's and the Ascension's vigils are absent from that list because their I-class feasts always keep their own day: 0 occurrences across the whole domain, measured rather than assumed.") (expected_rows 10)) diff --git a/test/test_golden.ml b/test/test_golden.ml index b882596..14fbec4 100644 --- a/test/test_golden.ml +++ b/test/test_golden.ml @@ -1363,10 +1363,14 @@ let test_vigil_of_st_lawrence_is_violet_2027 () = freed IV-class Saturday, which is why this pin reads white and subject=bvm. St Romanus keeps his commemoration either way (RG 111). - Independently witnessed. The published Ordo -- the project's only witness - outside the Divinum Officium -> missalemeum -> lectio lineage -- omits the - vigil on this exact date, and lectio (and so missalemeum) keeps it; both - allow-lists gained cited entries for that disagreement. *) + 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 \ -- cgit v1.3