From 0994587b3694bb87d9a8ee32d60babf25c784acf Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 25 Aug 2026 23:51:36 +0200 Subject: fix(of): correct subject over-match, provenance truth, and stale hazard comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four fixes from fix round 2 review: classify_subject's "maria"/"b.m.v" substring test over-matched 12 saints who merely carry "Maria" in their own name (Maximilian Mary Kolbe, John Mary Vianney, Mary Magdalene among them) as Bvm. Replaced with an explicit BVM_DATES table of the 13 genuinely Marian entries. Bvm 25 -> 13. The provenance header claimed lectio is "never a source", which omitted that slugs and English names both come from it. Corrected to state exactly what does (slugs, en names) and does not (every substantive field) come from lectio. A missing lectio file used to degrade silently to a different SHA-256 with no en names and no warning; it now fails loudly. lectio's own SHA-256 is now pinned alongside the Missal PDF's. Four more entries carried a second pdftotext artifact distinct from the letter-spacing already handled -- a combining diacritic rendered as a stray spacing character (Kim Taegŏn/Chŏng Ha-sang, Đũng Lạc, Makhlūf, Kęty). Hand-repaired the same way Peter Damian was, each corroborated by the identical corruption recurring in the Missal's own index. precedence_of.ml's PHASE 3 HAZARD comment about All Souls (2 November) was asserting a gap that no longer exists now that OF sanctoral data ships. Added test_all_souls_beats_an_ordinary_sunday, which resolves the shipped All Souls entry against a real per-annum Sunday (2025-11-02) through the actual Precedence.resolve pipeline, and updated the comment to record the resolution instead of demanding it. --- test/test_calendar_of_data.ml | 38 ++++++++++++++++++++++++--------- test/test_precedence_of.ml | 49 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 11 deletions(-) (limited to 'test') diff --git a/test/test_calendar_of_data.ml b/test/test_calendar_of_data.ml index d4f8a8b..9261ebc 100644 --- a/test/test_calendar_of_data.ml +++ b/test/test_calendar_of_data.ml @@ -69,14 +69,16 @@ let sha256_of_file path = don't transcribe it from output" pinning discipline. *) let test_sha256 () = Alcotest.(check string) "data/of/calendar-2002.sexp SHA-256" - "1b4f2117133dea1c70989708d94e6f292f3ce23fce6221391191d6fcf6042d19" (sha256_of_file path) + "91479d8b5d92d8e35c2ae34f239d5623a2a59cca2f02d51a8cbbad52edd46b3a" (sha256_of_file path) (* Counts independently re-derived from the shipped file - (`grep -c '(slug'`; `grep -oP '(?<=\(rank )[A-Za-z_]+' | sort | uniq -c`). - 206 = 209 raw table rows minus the 3 dates (1 Jan, 6 Jan, 25 Dec) already - computed by Temporal_of.named -- see test_excludes_temporal_of_dates - below, which closes the loop on that exclusion rather than merely - asserting the count moved. *) + (`grep -c '((slug'`; `grep -oP '(?<=\(rank )[A-Za-z_]+' | sort | uniq -c`). + 208 = 209 raw FIXED-date table rows minus the 3 dates (1 Jan, 6 Jan, 25 + Dec) already computed by Temporal_of.named (see + test_excludes_temporal_of_dates below, which closes the loop on that + exclusion) PLUS the 2 movable entries fix round 1 added (Sacred Heart + of Jesus, Immaculate Heart of Mary -- see test_movable_entries): 209 - + 3 + 2 = 208. *) let test_load_and_counts () = let l = load () in Alcotest.(check int) "208 entries" 208 (List.length l.L.entries); @@ -92,7 +94,19 @@ let test_load_and_counts () = Alcotest.(check int) "208 Feast (OF admits no Commemoration_only status)" 208 (count (fun e -> e.L.cel.Cel.status = Cel.Feast)); Alcotest.(check int) "every entry tagged Precedence_of.universal_layer" 208 - (count (fun e -> e.L.cel.Cel.layer = PO.universal_layer)) + (count (fun e -> e.L.cel.Cel.layer = PO.universal_layer)); + (* Fix round 2: classify_subject's "maria"/"b.m.v" SUBSTRING heuristic + over-matched 12 saints who merely carry "Maria" in their own name + (Maximilian Mary Kolbe, John Mary Vianney, Mary Magdalene, ...) as + Bvm -- replaced with an explicit BVM_DATES table of the 13 genuinely + Marian entries (12 fixed + the movable Immaculate Heart of Mary). + Pinned here so a regression to the old substring rule -- or a new one + with the same failure shape -- reddens immediately rather than only + showing up if/when a future Subject.Bvm-keyed rule is added. *) + Alcotest.(check int) "13 Bvm (12 fixed + Immaculate Heart of Mary)" 13 + (count (fun e -> e.L.cel.Cel.subject = Sub.Bvm)); + Alcotest.(check int) "9 Lord" 9 (count (fun e -> e.L.cel.Cel.subject = Sub.Lord)); + Alcotest.(check int) "186 Saint" 186 (count (fun e -> e.L.cel.Cel.subject = Sub.Saint)) let test_slugs_unique () = let l = load () in @@ -109,9 +123,13 @@ let test_slugs_revalidate () = | Error msg -> Alcotest.failf "slug %s failed re-validation: %s" (S.to_string e.L.cel.Cel.slug) msg) l.L.entries -(* Every date is a Fixed month/day; DS.fixed already rejects an out-of-range - day against the leap-year maximum, so this also proves no entry claims a - nonexistent day (e.g. 30 February). *) +(* Every FIXED-date entry's month/day pair already went through DS.fixed, + which rejects an out-of-range day against the leap-year maximum, so + resolving every entry (fixed AND the 2 movable Easter_offset ones, via + DS.resolve's own Easter_offset branch) in a real leap year also proves + no fixed entry claims a nonexistent day (e.g. 30 February) and that both + movable entries resolve at all. Not "every date is a Fixed month/day" + (stale since fix round 1 -- 2 of 208 are Easter_offset). *) let test_dates_resolve_in_a_leap_year () = let l = load () in List.iter diff --git a/test/test_precedence_of.ml b/test/test_precedence_of.ml index 610cbfb..e1af486 100644 --- a/test/test_precedence_of.ml +++ b/test/test_precedence_of.ml @@ -436,6 +436,50 @@ let test_target_is_free_of_entries_1_to_8 () = true (b > 80)) [ "2027-03-25"; "2035-03-25"; "2026-03-25"; "2029-03-25" ] +(* Fix round 2 (Task 1, 2026-08-25): the shipped All Souls entry (2 + November, data/of/calendar-2002.sexp) actually beats an ordinary + per-annum Sunday when they coincide -- the exact hazard the [band] + comment above named ("PHASE 3 HAZARD ... before shipping OF sanctoral + data"). Loads the REAL shipped entry (not a synthetic reconstruction) + and resolves it through {!Colitur_kernel.Precedence.resolve} with + [Rite_of.Precedence_of.rules] -- not [band] in isolation -- against a + real per-annum Sunday context, 2 November 2025 (independently a real + Sunday: date -d confirms it, and it is the same date CLAUDE.md's own + EF golden pin "All Souls falling on a Sunday" already uses). *) +let test_all_souls_beats_an_ordinary_sunday () = + let path = "../data/of/calendar-2002.sexp" in + let l = + match Colitur_kernel.Layer.load Vocab_of.rank_of_sexp path with + | Ok l -> l + | Error e -> Alcotest.failf "%s: failed to load: %s" path e + in + let all_souls = + match Colitur_kernel.Layer.find l (Slug.of_string_exn "all-souls") with + | Some e -> e.Colitur_kernel.Layer.cel + | None -> Alcotest.fail "\"all-souls\" not found in the shipped calendar" + in + Alcotest.(check bool) "shipped All Souls is tagged Sollemnitas" true + (all_souls.Celebration.rank = Vocab_of.Sollemnitas); + let all_souls_candidate = { Precedence.cel = all_souls; origin = Precedence.Sanctoral } in + let d = Date.of_iso8601 "2025-11-02" |> Result.get_ok in + Alcotest.(check bool) "2025-11-02 is really a Sunday" true (Date.weekday d = Date.Sun); + let ctx = { Precedence.date = d; season = Vocab_of.Ordinary_time; weekday = Date.Sun } in + (* An ordinary per-annum Sunday's own candidate, shaped exactly as + Temporal_of.temporal builds one (rank Festum, Tabula II.6) -- + independent of temporal_candidate/ctx_of_date above, which read the + office off Temporal_of.temporal directly; this test instead builds it + by hand so it does not depend on 2025-11-02 having no competing named + temporal office of its own. *) + let sunday_candidate = + mk ~slug:"of-ordinary-time-sunday" ~rank:Vocab_of.Festum ~origin:Precedence.Temporal () + in + let resolution = + Precedence.resolve Rite_of.Precedence_of.rules ctx ~temporal:sunday_candidate + ~sanctoral:[ all_souls_candidate ] + in + Alcotest.(check bool) "All Souls, not the Sunday, is observed" true + (resolution.Precedence.observed = all_souls_candidate) + let suite = ( "precedence-of", [ Alcotest.test_case "Tabula part I (entries 1-4)" `Quick test_part1; @@ -455,5 +499,8 @@ let suite = Alcotest.test_case "transfer_target is 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 ] + test_target_is_free_of_entries_1_to_8; + Alcotest.test_case + "fix round 2: shipped All Souls beats an ordinary per-annum Sunday (2025-11-02)" `Quick + test_all_souls_beats_an_ordinary_sunday ] @ List.map QCheck_alcotest.to_alcotest [ prop_band_total ] ) -- cgit v1.3