diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-26 12:29:17 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-26 12:29:17 +0200 |
| commit | e6cb5937be7f009eab11f3cf85205d1fde25eb48 (patch) | |
| tree | fb10d57e8a5b124e022eb1f748a5dde31a93736b /test/test_golden_of.ml | |
| parent | e199ad6f599a263cad1f9886b512da8b3a46f465 (diff) | |
| download | colitur-e6cb5937be7f009eab11f3cf85205d1fde25eb48.tar.gz colitur-e6cb5937be7f009eab11f3cf85205d1fde25eb48.zip | |
test(of): validation layers -- Layer 2 properties, litcal grade/identity, golden pins
Task 6 (2026-08-25-colitur-of-phases-3-5), the final task of OF Phases 3-5.
Layer 2 (test/test_validate_of.ml, new): Colitur_kernel.Validate.run
against the real, assembled Rite_of.context -- landmark years, the 9999
domain edge, a 200-year QCheck sample, and the full 8417-year exhaustive
sweep under COLITUR_EXHAUSTIVE_SWEEP=1, all filtered through the same
three pinned-not-fixed gaps test_rite_of.ml already names (Normae n.35(a)
Holy Family fallback, the 25-December lectionary gap, St Joseph/Palm
Sunday), duplicated verbatim rather than shared so this file's own
filtering cannot drift from Task 5's silently. Two dedicated 200-sample
properties confirm Ordinary Time weeks are 1..34 with the final week
always 34, and Calendar.year covers its own liturgical year exactly once
with no gaps, both through the resolved pipeline (a genuinely different
code path from Phase 1's own already-exhaustive Temporal_of-level
property). An early version folded a second Calendar.year pass into the
exhaustive loop for these two properties too and measured ~365s for that
alone (a scratch timing harness, not committed); dropped in favour of one
Val.run-only exhaustive loop (~200s), matching the EF harness's shape
(one property, one sweep) rather than tripling it.
test/test_litcal_of.ml extended beyond season/week to GRADE (litcal's own
grade_lcl bucketed against the real Tabula band Precedence_of.band
assigns the day's observed office) and IDENTITY (observed slug against a
hand-verified event_key -> slug table for the closed 46-entity FIXED/
NAMED set -- deliberately not the numbered Sunday/weekday series, a
stated, counted scope limit). Both follow the counted-and-allow-listed
discipline: 1800/4346 rows compared for grade (1763 match, 37 allow-
listed across 5 new entries), 508/4346 for identity (505 match, 3 allow-
listed across 3 new entries), zero unexplained either way. Real
divergences found and adjudicated, not suppressed: litcal's own grade
vocabulary conflates Tabula I.2/I.3 for Trinity/Corpus Christi (verdict
colitur); litcal's Mary Magdalene data predates the 2016 CDW decree
(verdict litcal); a genuine, unadjudicated Sacred-Heart/Nativity-of-John-
the-Baptist tie-break at equal band, found live at the one Easter date in
this fixture's window that creates it (verdict open, no citation found
either way); a second instance of precedence_of.mli's own documented
forward-only transfer_target limitation, on a date shape the mli's own
worked example does not cover (verdict colitur, known limitation).
test/test_golden_of.ml (new, 7 pins): a year Ordinary Time skips a week
(2026) plus Mary Mother of the Church same date; Advent IV on Christmas
Eve in two independent years (2028, 2034); the Annunciation transferred
out of Holy Week to Easter+8 (2027, both ends of the transfer pinned);
All Souls; the Sacred Heart; 17 December's date-keyed reading in two
civil years/weekday-cycle letters.
Every new layer mutation-proved: reverting the Advent-IV fix reddens two
golden pins; corrupting Precedence_of.band's Tabula I.3 value reddens the
litcal grade comparator across every Solemnity in the fixture; renaming
one temporal slug reddens the litcal identity comparator; dropping
Ordinary Time's second season_runs entry reddens both the landmark-years
test and the sampled property. Each mutation applied to lib/rites/rite_of/,
confirmed red, then reverted -- git diff --stat lib/ is empty.
make check (COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force): exit 0, 890
tests, 363.5s. EF byte-identity holds: git diff --stat v1.0.0..HEAD --
lib/rites/rite_ef/ lib/kernel/ data/ef/ is empty.
Diffstat (limited to 'test/test_golden_of.ml')
| -rw-r--r-- | test/test_golden_of.ml | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/test/test_golden_of.ml b/test/test_golden_of.ml new file mode 100644 index 0000000..f40b62a --- /dev/null +++ b/test/test_golden_of.ml @@ -0,0 +1,332 @@ +(* Task 6 (2026-08-26-colitur-of-phases-3-5): golden pins, the OF + counterpart of test_golden.ml -- validation layer 5, one rite down. + + *** THE ONE RULE THIS FILE IS BUILT AROUND (test_golden.ml's own, + restated for this file) *** + + A golden test blesses whatever it is given. Every other layer this + branch built derives its expectation from something independent of + colitur's own output -- a property (test_validate_of.ml), a second + implementation (test_litcal_of.ml). This layer's expectation comes from + ME, so every literal string below was checked BY HAND, against the + primary-source citations already carried by temporal_of.ml/ + precedence_of.ml/the amendment files (each pin's own comment names + which), BEFORE it was typed in here -- never copied from a `colitur + day`/`readings` run and then rationalised. Every date and weekday + claim below was independently cross-checked against + {!Colitur_kernel.Computus.gregorian_easter} arithmetic done BY HAND + (Easter date + a day-offset, or a `date -d` cross-check for a fixed + date), the same discipline test_golden.ml's own header states for its + own pins. + + Six landmark/tricky dates, per this task's own brief, at minimum: + 1. A year Ordinary Time skips a week (2026 -- already independently + hand-verified, twice over, by test_temporal_of.ml's own + [test_ordinary_time_resumption]; reused here, not re-derived, since + re-deriving the SAME arithmetic a second time would not add + confidence, only a second literal to keep in sync). + 2. A year 24 December falls on a Sunday (2028; the Advent IV case -- + commit a2fba71d, "fix(of): Nativity Vigil no longer suppresses a + Sunday of Advent"). + 3. A year the Annunciation falls in Holy Week (2027; Normae n. 60's + fixed Easter+8 transfer). + 4. Mary, Mother of the Church (Easter+50). + 5. All Souls (2 November). + 6. The Sacred Heart (Easter+68). + Plus 17 December's own date-keyed reading (Gen 49:2,8-10 / Mt 1:1-17, + fixed earlier on this branch, commit b6d0eba0). *) + +module Cal = Colitur_kernel.Calendar +module Layer = Colitur_kernel.Layer +module Overlay = Colitur_kernel.Overlay +module LD = Colitur_kernel.Liturgical_day +module Slug = Colitur_kernel.Slug +module Date = Colitur_kernel.Date +module Cel = Colitur_kernel.Celebration +module Colour = Colitur_kernel.Colour +module Citation = Colitur_kernel.Citation +module Temporal = Colitur_kernel.Temporal +module Subject = Colitur_kernel.Subject +module V = Rite_of.Vocab_of + +let calendar_path = "../data/of/calendar-2002.sexp" +let amendments_dir = "../data/of/amendments/" +let of_lectionary_path = "../data/of/lectionary.sexp" + +let amendment_files = + [ "001-padre-pio.sexp"; "002-juan-diego-cuauhtlatoatzin.sexp"; "003-our-lady-of-guadalupe.sexp"; + "004-john-xxiii-john-paul-ii.sexp"; "005-mary-magdalene-rank.sexp"; "006-mary-mother-of-the-church.sexp"; + "007-paul-vi.sexp"; "008-our-lady-of-loreto.sexp"; "009-faustina-kowalska.sexp"; + "010-narek-avila-hildegard.sexp"; "011-martha-mary-lazarus.sexp"; "012-teresa-of-calcutta.sexp"; + "013-john-henry-newman.sexp" ] + +(* Duplicated verbatim from test_rite_of.ml/test_validate_of.ml -- see + either file's own header for why (no .mli any of them could share it + through). *) +let real_of_layer = + let base = + match Layer.load V.rank_of_sexp calendar_path with + | Ok l -> l + | Error e -> failwith (Printf.sprintf "%s: failed to load: %s" calendar_path e) + in + let overlays = + List.map + (fun name -> + let path = amendments_dir ^ name in + match Overlay.load V.rank_of_sexp path with + | Ok o -> o + | Error e -> failwith (Printf.sprintf "%s: failed to load: %s" path e)) + amendment_files + in + let layer, diagnostics = Overlay.merge base overlays in + if diagnostics <> [] then + failwith + (Printf.sprintf "unexpected amendment diagnostics: %s" + (String.concat "; " (List.map Overlay.diagnostic_to_string diagnostics))); + layer + +let real_of_lectionary = + match Colitur_kernel.Lectionary.load of_lectionary_path with + | Ok l -> l + | Error e -> failwith (Printf.sprintf "%s: failed to load: %s" of_lectionary_path e) + +let real_of_rite = Rite_of.context ~lectionary:real_of_lectionary + +let mk y m d = match Date.make ~year:y ~month:m ~day:d with Ok t -> t | Error e -> failwith e +let fetch y m d = Cal.day real_of_rite real_of_layer (mk y m d) + +let slug_s (c : V.rank Cel.t) = Slug.to_string c.Cel.slug +let rank_s (c : V.rank Cel.t) = V.rank_to_string c.Cel.rank +let colour_s (c : V.rank Cel.t) = Colour.to_string c.Cel.colour +let subject_s (c : V.rank Cel.t) = Subject.to_string c.Cel.subject + +(* [describe]'s own five components mirror {!Colitur_kernel.Liturgical_day + .t}'s own promise never to lose a field, the same discipline + test_golden.ml's header argues for at length -- one string per day, not + five separate checks, so a one-line diff on failure still names the day + and shows exactly which field changed. + + [comms] is included even though it is STRUCTURALLY ALWAYS "[]" for this + rite ({!Rite_of.Precedence_of.admit} never returns anything else, + Precedence_of.mli's own citation) -- kept in the format rather than + dropped, so a future rite-level change that somehow populated it would + still show up in every existing pin's own diff, not silently pass. *) +let describe (day : (V.season, V.rank) LD.t) = + let t = day.LD.temporal in + let week = match t.Temporal.week with Some n -> string_of_int n | None -> "-" in + let comms = + List.map (fun (c, _) -> slug_s c) day.LD.commemorations |> List.sort compare |> String.concat "," + in + let tin = match day.LD.transferred_in with None -> "-" | Some c -> slug_s c in + let tout = + List.map (fun (c, d) -> Printf.sprintf "%s->%s" (slug_s c) (Date.to_iso8601 d)) day.LD.transferred_out + |> List.sort compare |> String.concat "," + in + Printf.sprintf "%s %s season=%s week=%s slug=%s rank=%s colour=%s subject=%s comms=[%s] in=%s out=[%s]" + (Date.to_iso8601 day.LD.date) + (Date.weekday_to_string t.Temporal.weekday) + (V.season_to_string t.Temporal.season) + week (slug_s day.LD.observed) (rank_s day.LD.observed) (colour_s day.LD.observed) (subject_s day.LD.observed) + comms tin tout + +let check ~msg y m d expected = Alcotest.(check string) msg expected (describe (fetch y m d)) + +let describe_readings (day : (V.season, V.rank) LD.t) = + let part_ref p = + match List.find_opt (fun (c : Citation.t) -> c.Citation.part = p) day.LD.citations with + | Some c -> c.Citation.reference + | None -> "-" + in + Printf.sprintf "%s slug=%s first=%s gospel=%s" + (Date.to_iso8601 day.LD.date) + (slug_s day.LD.observed) (part_ref Citation.First) (part_ref Citation.Gospel) + +let check_readings ~msg y m d expected = Alcotest.(check string) msg expected (describe_readings (fetch y m d)) + +(* ------------------------------------------------------------------ *) +(* 1. Ordinary Time skips a week: 2026. Pentecost is 24 May 2026 (Easter *) +(* 5 April + 49); block A tops out at week 6 (Shrove Tuesday, 17 Feb, *) +(* independently hand-verified against Ash Wednesday 18 Feb); Normae *) +(* n. 43-44's own two-block arithmetic resumes the Monday after Pentecost *) +(* (25 May) at week 8, not 7 -- week 7 never occurs anywhere in Ordinary *) +(* Time that civil year (test_temporal_of.ml's own *) +(* [test_ordinary_time_resumption], which checks this exhaustively over *) +(* the WHOLE civil year, not merely this one pinned Monday). Pinned here *) +(* through the ASSEMBLED pipeline (Rite_of.context/Calendar.day), not *) +(* Temporal_of directly -- the same "genuinely different code path" *) +(* argument test_validate_of.ml's own [ot_and_coverage_of_year] makes. *) +(* ------------------------------------------------------------------ *) + +let test_ordinary_time_skips_a_week_2026 () = + (* subject=temporal, not =lord: {!Rite_of.Temporal_of.named}'s own tuple + shape (season, slug, colour, rank) carries no subject field at all, + so every day it builds (Nativity, Epiphany, Ascension, Pentecost, Ash + Wednesday, Palm Sunday, Easter, the Easter Octave, Trinity, Corpus + Christi, Christ the King) defaults to {!Colitur_kernel.Subject + .Temporal} -- {!Rite_of.Precedence_of.band}'s own entry-2 branch + routes these by SLUG membership ([entry_2_named]), never by subject, + so this is not a gap, just a different (and correct) tagging + convention from the DATA-origin solemnities pinned elsewhere in this + file (Sacred Heart, the Annunciation, All Souls), which carry a real + [subject] field in their own calendar-2002.sexp/amendment record. *) + check ~msg:"2026-05-24 Pentecost: Easter+49, red, block A's own last Sunday" 2026 5 24 + "2026-05-24 sunday season=easter week=8 slug=of-pentecost rank=sollemnitas colour=red subject=temporal \ + comms=[] in=- out=[]"; + check ~msg:"2026-05-25 (Mon after Pentecost) resumes Ordinary Time at week 8, SKIPPING week 7" 2026 5 25 + "2026-05-25 monday season=ordinary-time week=8 slug=mary-mother-of-the-church rank=memoria-obligatoria \ + colour=white subject=bvm comms=[] in=- out=[]" + +(* ------------------------------------------------------------------ *) +(* 2. 24 December falls on a Sunday: 2028 (and, symmetrically, 2034 -- *) +(* both inside commit a2fba71d's own worked examples). Before that fix, *) +(* the Vigil Mass of the Nativity silently displaced the Fourth Sunday of *) +(* Advent (Tabula I.2/Normae n. 5 -- a privileged Sunday outranks *) +(* everything but a HIGHER Tabula entry, and the Vigil is Tabula I.3). *) +(* After it, 24 December on a Sunday resolves as the Fourth Sunday of *) +(* Advent itself, violet (Advent's own colour, not Gaudete's rose -- *) +(* that is week 3 only, IGMR 346(c)/[V.season]'s own [is_rose_sunday]). *) +(* ------------------------------------------------------------------ *) + +let test_advent_iv_on_christmas_eve_2028 () = + check ~msg:"2028-12-24 (a Sunday): the Fourth Sunday of Advent, not the Vigil (Tabula I.2 over I.3)" 2028 12 24 + "2028-12-24 sunday season=advent week=4 slug=of-advent-sunday-4 rank=sollemnitas colour=violet \ + subject=temporal comms=[] in=- out=[]"; + (* The day immediately before it: still the Third Week of Advent, an + ordinary (non-rose) Saturday feria -- confirms the season/week + boundary itself sits in the right place, not merely that 24 December + alone reads correctly. *) + check ~msg:"2028-12-23: still Advent week 3, an ordinary violet feria" 2028 12 23 + "2028-12-23 saturday season=advent week=3 slug=of-advent-3-saturday rank=feria colour=violet subject=temporal \ + comms=[] in=- out=[]"; + (* And the day after: Christmas Day itself, entirely unaffected by which + office 24 December carried. *) + check ~msg:"2028-12-25: the Nativity, unaffected" 2028 12 25 + "2028-12-25 monday season=christmas week=- slug=of-nativity rank=sollemnitas colour=white subject=temporal \ + comms=[] in=- out=[]" + +(* The symmetric 2034 witness -- a second, independent year, not a + re-check of the same arithmetic (a fix that happened to work for one + specific weekday alignment but not the general rule would show up as a + difference here). *) +let test_advent_iv_on_christmas_eve_2034 () = + check ~msg:"2034-12-24 (a Sunday): the Fourth Sunday of Advent" 2034 12 24 + "2034-12-24 sunday season=advent week=4 slug=of-advent-sunday-4 rank=sollemnitas colour=violet \ + subject=temporal comms=[] in=- out=[]" + +(* ------------------------------------------------------------------ *) +(* 3. The Annunciation in Holy Week: 2027 (Easter 28 March 2027, so 25 *) +(* March is Easter-3, the Thursday of Holy Week). Normae n. 60's own *) +(* FIRST, fixed-destination rule (not the general "nearest later free *) +(* day" search rule 3): "Sollemnitas... Annuntiationis Domini, *) +(* quotiescumque occurrit aliquo die Hebdomadae sanctae, semper ad feriam *) +(* II post dominicam II Paschae erit transferenda" -- ALWAYS to the *) +(* Monday after the Second Sunday of Easter, Easter+8, here 5 April 2027. *) +(* Pinned as THREE dates, not one: 25 March itself (Holy Thursday's own *) +(* office wins outright, IGMR 346(a)'s "celebrationibus Domini, quae non *) +(* sint de eius Passione" -- white, not Passiontide violet/red), the *) +(* departure recorded in [out], and the arrival on 5 April recorded in *) +(* [in] -- Calendar's own transfer bookkeeping, both ends. *) +(* ------------------------------------------------------------------ *) + +let test_annunciation_holy_week_2027 () = + check ~msg:"2027-03-25 (Holy Thursday, Easter-3): the day's own office wins, white, and records the \ + Annunciation's own departure" + 2027 3 25 + "2027-03-25 thursday season=lent week=6 slug=of-lent-6-thursday rank=feria colour=white subject=temporal \ + comms=[] in=- out=[annunciation-of-the-lord->2027-04-05]"; + check ~msg:"2027-04-05 (Easter+8, the Monday after the Second Sunday of Easter): the Annunciation arrives" 2027 4 + 5 + "2027-04-05 monday season=easter week=2 slug=annunciation-of-the-lord rank=sollemnitas colour=white \ + subject=lord comms=[] in=annunciation-of-the-lord out=[]" + +(* ------------------------------------------------------------------ *) +(* 4. Mary, Mother of the Church: Easter+50, the Monday after Pentecost *) +(* (data/of/amendments/006-mary-mother-of-the-church.sexp's own citation, *) +(* CDW decree 11 February 2018, Prot. N. 1037/2017, AAS 110 (2018) *) +(* 437-438). ALREADY the day this file's own [test_ordinary_time_skips_a_ *) +(* week_2026] pins (2026-05-25) -- not re-pinned a second time under a *) +(* different date; that pin's own [msg] is worded to name both facts it *) +(* carries (the week-8 resumption AND Mother of the Church herself), the *) +(* SAME "one string, several independently-true facts" economy *) +(* test_golden.ml's own header argues for, rather than a second literal *) +(* asserting the identical field values under a different label. *) +(* ------------------------------------------------------------------ *) + +(* ------------------------------------------------------------------ *) +(* 5. All Souls: 2 November, every year, unconditionally (a fixed date, *) +(* not movable) -- Tabula I.3 ("Commemoratio omnium fidelium *) +(* defunctorum"), tagged Sollemnitas on the Tabula's own authority *) +(* despite the calendarium page itself printing no grade word *) +(* (precedence_of.ml's own [band] entry-3 citation has the full *) +(* argument). Violet (not white, not black) -- shipped *) +(* data/of/calendar-2002.sexp's own colour, already independently *) +(* exercised by test_precedence_of.ml's own *) +(* [test_all_souls_beats_an_ordinary_sunday] against a DIFFERENT year *) +(* (2025-11-02, itself a Sunday); 2026 is chosen here instead so this *) +(* file's own pin is not a byte-identical duplicate of that one. ------- *) + +let test_all_souls_2026 () = + check ~msg:"2026-11-02: All Souls, violet, every year unconditionally" 2026 11 2 + "2026-11-02 monday season=ordinary-time week=31 slug=all-souls rank=sollemnitas colour=violet subject=saint \ + comms=[] in=- out=[]" + +(* ------------------------------------------------------------------ *) +(* 6. The Sacred Heart: Easter+68 (calendar-2002.sexp's own *) +(* [Easter_offset 68] entry) -- Easter 2026 is 5 April, so Easter+68 is *) +(* 12 June 2026 (25 days left in April + 31 in May + 12 = 68, hand- *) +(* verified against {!Colitur_kernel.Computus.gregorian_easter} directly, *) +(* not merely trusted from the offset). White, Sollemnitas, subject Lord *) +(* -- shipped data, matching the Missal's own title ("SACRATISSIMI *) +(* CORDIS IESU"). ------------------------------------------------------ *) + +let test_sacred_heart_2026 () = + Alcotest.(check string) "Easter 2026 is really 5 April (the Sacred Heart's own Easter+68 anchor)" "2026-04-05" + (Date.to_iso8601 (Colitur_kernel.Computus.gregorian_easter 2026)); + check ~msg:"2026-06-12 (Easter+68): the Sacred Heart of Jesus" 2026 6 12 + "2026-06-12 friday season=ordinary-time week=10 slug=sacred-heart-of-jesus rank=sollemnitas colour=white \ + subject=lord comms=[] in=- out=[]" + +(* ------------------------------------------------------------------ *) +(* 7. 17 December's own date-keyed reading (OLM n. 69.3, commit b6d0eba0, *) +(* "fix(of-lectionary): stop serving drifting readings for O-Antiphon and *) +(* Christmas-season dates"): Genesis 49:2,8-10 / Matthew 1:1-17, fixed by *) +(* CIVIL DATE, not by the day's own weekday-keyed ferial slug -- this pin *) +(* is what would have caught the original bug (a citation that silently *) +(* drifted year to year on this date before the fix), pinned through the *) +(* full resolved pipeline rather than test_lectionary_of.ml's own more *) +(* narrowly-scoped unit test for the same fact. Raw reference strings *) +(* (data/of/lectionary.sexp's own literal text, "Genesis"/"Matthew" in *) +(* full), not a rendered siglum -- the same discipline test_golden.ml's *) +(* own [describe_readings] follows for EF. *) +(* ------------------------------------------------------------------ *) + +let test_december_17_reading () = + check_readings ~msg:"2026-12-17: the O-Antiphon date-keyed reading, Genesis 49:2,8-10 / Matthew 1:1-17" 2026 12 + 17 "2026-12-17 slug=of-advent-3-thursday first=Genesis 49:2,8-10 gospel=Matthew 1:1-17"; + (* A second, independent civil year -- OLM n. 69.4's weekday-cycle + letter flips on the liturgical year's own parity (Lectionary_of + .weekday_cycle), so a date-keyed reading that accidentally still + routed through the weekday-keyed path would likely only fail in + roughly half of all years -- this second year is chosen from the + OTHER cycle letter than 2026's own (test_lectionary_of.ml's own + [test_date_keyed_no_drift] already proves this pair directly at the + Lectionary_of level; repeated here through the full pipeline for the + same reason [test_advent_iv_on_christmas_eve_2034] repeats 2028's own + shape in a second year). *) + check_readings ~msg:"2027-12-17: the same date-keyed reading, a second civil year, the other weekday-cycle \ + letter" + 2027 12 17 "2027-12-17 slug=of-advent-3-friday first=Genesis 49:2,8-10 gospel=Matthew 1:1-17" + +let suite = + ( "golden-of", + [ Alcotest.test_case "Ordinary Time skips a week (2026); Mary, Mother of the Church" `Quick + test_ordinary_time_skips_a_week_2026; + Alcotest.test_case "Advent IV on Christmas Eve (2028)" `Quick test_advent_iv_on_christmas_eve_2028; + Alcotest.test_case "Advent IV on Christmas Eve, a second year (2034)" `Quick + test_advent_iv_on_christmas_eve_2034; + Alcotest.test_case "the Annunciation in Holy Week, transferred to Easter+8 (2027)" `Quick + test_annunciation_holy_week_2027; + Alcotest.test_case "All Souls (2026)" `Quick test_all_souls_2026; + Alcotest.test_case "the Sacred Heart, Easter+68 (2026)" `Quick test_sacred_heart_2026; + Alcotest.test_case "17 December's own date-keyed reading, two civil years" `Quick test_december_17_reading + ] ) |
