diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-21 15:07:44 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-21 15:07:44 +0200 |
| commit | 35c11c7ccf95acfb35a7ec34edfa86d92da60e4a (patch) | |
| tree | 63e2e0c8f9c6317f68a52961c7770f72c3ba79c0 /test/test_sanctoral_ef.ml | |
| parent | 0eae32f67145065b0e4960650ae06c293fd9f272 (diff) | |
| parent | f3ac69d0a679200a48716be065a0fa2e70a2c6da (diff) | |
| download | colitur-35c11c7ccf95acfb35a7ec34edfa86d92da60e4a.tar.gz colitur-35c11c7ccf95acfb35a7ec34edfa86d92da60e4a.zip | |
merge: RG 33's third omission trigger
The last unbuilt clause in the EF module. A II/III-class vigil is omitted
entirely when the feast it precedes fails to keep its own day -- 1 744 days
across 1583-9999, three verified shapes, zero unexplained.
Rule and vigil inventory independently confirmed by O'Connell 1964, outside
the Divinum Officium -> missalemeum -> lectio lineage. The 10 affected days
in 2005-2050 are allow-listed as C39; no external day-level witness exists,
which the note states rather than glosses.
Diffstat (limited to 'test/test_sanctoral_ef.ml')
| -rw-r--r-- | test/test_sanctoral_ef.ml | 72 |
1 files changed, 71 insertions, 1 deletions
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 ] ) |
