diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-24 16:29:23 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-24 16:29:23 +0200 |
| commit | 8a5da8756cf6b57aa36c01b71ce893f7967d3a29 (patch) | |
| tree | 10873a692400fa25ce54c67ac9fc34d46af86272 /test/test_validate.ml | |
| parent | 4f87cbde4ee79ac96aa7ebfed105b3a5ec02be43 (diff) | |
| parent | 73b15551804bb63ee0081005e2869d36afb54be2 (diff) | |
| download | colitur-8a5da8756cf6b57aa36c01b71ce893f7967d3a29.tar.gz colitur-8a5da8756cf6b57aa36c01b71ce893f7967d3a29.zip | |
merge: celebrant Mass rubrics, and four rubrical corrections
colitur now prints what a real ordo prints for the Mass: which formulary is
said and how it was reached, the Gloria, the Creed, the preface, and the
commemorations with their Low-Mass/sung distinction. colitur rubrics joins
day and readings.
Four defects were found and fixed on the way, each by an external witness
rather than by inspection: the Creed said at Requiem Masses (RG 476(f)),
the missing bissextile shift of St Matthias and St Gabriel (2 041 leap
years), Rogation Monday and Tuesday coloured violet in Paschaltide (RG
119(b)), and the ferias after the Ascension resuming the wrong Sunday's
Mass rather than the Ascension's.
Validation gained a sixth layer and then some: the preface is checked
against three independent publishers over seven witness-years (FIUV, three
LMS editions, three extraordinaryform.org editions), none of which shares
the Divinum Officium -> missalemeum -> lectio lineage the older layers all
descend from.
Diffstat (limited to 'test/test_validate.ml')
| -rw-r--r-- | test/test_validate.ml | 128 |
1 files changed, 114 insertions, 14 deletions
diff --git a/test/test_validate.ml b/test/test_validate.ml index 8b3014a..3da8fbb 100644 --- a/test/test_validate.ml +++ b/test/test_validate.ml @@ -300,15 +300,16 @@ module Synthetic = struct defaults against the default empty [layer], since nothing ever contests the temporal office there) so the resolution fixtures further down can override them without duplicating every other field. *) - (* Most fixtures here exercise no citations -- [readings] is a harmless - constant [], the same role the other placeholder defaults above play, - and {!Validate}'s own citation checks are gated on a rite producing SOME - citation somewhere, so a constant [] leaves them entirely dormant. Task + (* Most fixtures here exercise no citations and no formulary -- [readings] + is a harmless constant [(None, [])], the same role the other + placeholder defaults above play, and {!Validate}'s own citation and + formulary checks are gated on a rite producing SOME citation/formulary + somewhere, so a constant [(None, [])] leaves them entirely dormant. Task 10 makes it overridable ([?readings] below) so the citation fixtures at the end of this file can drive those checks directly, exactly as every - other check here is driven -- rather than leaving two kernel checks with - no committed proof that they can fire at all. *) - let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = [] + other check here is driven -- rather than leaving kernel checks with no + committed proof that they can fire at all. *) + let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = (None, []) (* The shape {!Validate} accepts: exactly one First and one Gospel. The references are deliberately nonsense -- these fixtures assert SHAPE, @@ -317,16 +318,35 @@ module Synthetic = struct [ { Citation.part = Citation.First; reference = "Synth 1:1" }; { Citation.part = Citation.Gospel; reference = "Synth 2:2" } ] + (* The formulary equivalent of [well_formed_citations] above -- shape only, + never rubrically meaningful content. *) + let well_formed_formulary = + { Colitur_kernel.Mass_formulary.said = Some (Slug.of_string_exn "syn-formulary"); + via = Colitur_kernel.Mass_formulary.Own_slug } + + (* No fixture here exercises the Creed, Gloria or preface rubrics -- a + rite that has not implemented them returns [false]/[None] explicitly, + {!Rite.t.creed}/{!Rite.t.gloria}/{!Rite.t.preface}'s own documented + default. Made overridable ([?creed]/[?gloria]/[?preface] below) on + the same footing as [?readings] just above, for Task 6's own + fixtures. *) + let creed ~temporal:_ ~observed:_ ~date:_ = false + let gloria ~temporal:_ ~observed:_ ~date:_ = false + let preface ~temporal:_ ~observed:_ ~date:_ = None + let rite ?(vocab = vocab) ?(anchors = fun _ -> []) ?(season_runs = [ A; B ]) ?(rules = rules) - ?(transfer_target = fun _ origin _ -> origin) ?(readings = readings) temporal : - (season, rank) Rite.t = + ?(transfer_target = fun _ origin _ -> origin) ?(readings = readings) ?(creed = creed) + ?(gloria = gloria) ?(preface = preface) temporal : (season, rank) Rite.t = { Rite.id = "synthetic"; vocab; year_start; temporal; anchors; rules; season_runs; (* Not a Roman rite, but a Rite.t must supply SOME Easter now that movable Date_spec variants exist. The Gregorian one is as good as any for a fixture; nothing here is Easter-relative, so the value is never actually read. *) easter = Colitur_kernel.Computus.gregorian_easter; - transfer_target; readings } + (* Not a Roman rite either, so no bissextile-doubling convention: + identity, {!Rite.t.fixed_key}'s own documented default. *) + fixed_key = (fun d -> Some (D.month d, D.day d)); + transfer_target; readings; creed; gloria; preface } (* Empty by default: every check built before Task 12 exercises the TEMPORAL-only pass, where an empty layer is exactly the fixture that @@ -759,7 +779,7 @@ let test_citations_silent_without_a_lectionary () = (* The positive: well-formed citations on every day report nothing. *) let test_citations_clean_when_well_formed () = - let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = well_formed_citations in + let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = (None, well_formed_citations) in let fs = run ~readings good in Alcotest.(check bool) "neither citation check fires when every day carries First + Gospel" false (has_check "citations" fs || has_check "citations-unresolved" fs) @@ -769,7 +789,7 @@ let test_citations_clean_when_well_formed () = plausibly produce -- half a lookup succeeding. *) let test_citations_fires_on_a_lone_epistle () = let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = - [ { Citation.part = Citation.First; reference = "Synth 1:1" } ] + (None, [ { Citation.part = Citation.First; reference = "Synth 1:1" } ]) in Alcotest.(check bool) "citations check fires when a day carries an Epistle but no Gospel" true (has_check "citations" (run ~readings good)) @@ -780,7 +800,7 @@ let test_citations_fires_on_a_lone_epistle () = even though the day is otherwise a well-formed pair. *) let test_citations_fires_on_an_out_of_scope_part () = let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = - { Citation.part = Citation.Tract; reference = "Synth 3:3" } :: well_formed_citations + (None, { Citation.part = Citation.Tract; reference = "Synth 3:3" } :: well_formed_citations) in Alcotest.(check bool) "citations check fires when a part outside First/Gospel appears" true (has_check "citations" (run ~readings good)) @@ -792,7 +812,7 @@ let test_citations_fires_on_an_out_of_scope_part () = this file singles out. *) let test_citations_unresolved_fires_on_a_gap () = let readings ~observed:_ ~temporal:_ ~date ~temporal_at:_ = - if D.compare date target = 0 then [] else well_formed_citations + if D.compare date target = 0 then (None, []) else (None, well_formed_citations) in Alcotest.(check bool) "citations-unresolved fires when one day of the year resolves nothing" true (has_check "citations-unresolved" (run ~readings good)); @@ -801,6 +821,79 @@ let test_citations_unresolved_fires_on_a_gap () = Alcotest.(check bool) "the well-formedness check stays silent on a pure coverage gap" false (has_check "citations" (run ~readings good)) +(* ---------------------------------------------------------------------- *) +(* The formulary invariant (Task 3, celebrant-rubrics-phase1): the same *) +(* negative-path discipline the citation checks above already hold *) +(* themselves to, driven through the same synthetic fixture. Model on the *) +(* citations trio above -- "same shape, its own name" is what {!Validate} *) +(* itself now does, so the tests proving it can fire follow the same *) +(* pattern. *) +(* ---------------------------------------------------------------------- *) + +(* The gate: a rite that resolves no formulary at all (the default constant + [(None, [])]) must report nothing -- not "usually", not "on this year". *) +let test_formulary_silent_without_a_lectionary () = + let fs = run good in + Alcotest.(check bool) "no formulary check fires for a rite with no readings at all" false + (has_check "formulary" fs) + +(* The positive: a formulary on every day reports nothing. *) +let test_formulary_clean_when_well_formed () = + let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = + (Some well_formed_formulary, well_formed_citations) + in + Alcotest.(check bool) "formulary check stays silent when every day resolves one" false + (has_check "formulary" (run ~readings good)) + +(* The coverage gap: a rite that resolves a formulary on most days but falls + through on one. On real EF data this has no witness at all (Task 3's own + [test_every_day_has_a_formulary] below confirms it directly), so this + fixture is the only thing that holds it honest. *) +let test_formulary_fires_on_a_gap () = + let readings ~observed:_ ~temporal:_ ~date ~temporal_at:_ = + if D.compare date target = 0 then (None, []) + else (Some well_formed_formulary, well_formed_citations) + in + Alcotest.(check bool) "formulary check fires when one day of the year resolves none" true + (has_check "formulary" (run ~readings good)) + +(* ---------------------------------------------------------------------- *) +(* Direct real-EF-data coverage (Task 3 brief): every day of every year in *) +(* the sample resolves a formulary, the same discipline the "citations" *) +(* checks already hold EF to -- asserted directly against *) +(* [Liturgical_day.t] rather than through [Validate.run]'s failure list, *) +(* so a bug in [Validate]'s own gating could not hide this gap. *) +(* ---------------------------------------------------------------------- *) + +(* 2005-2050: the same 46-year sample test_rite_ef.ml's own [sample_years] + uses, for the same reason -- non-trivial, deterministic, and already the + project's differential-testing window (CLAUDE.md). Defined locally + rather than shared: test executables in this project cross-reference + only [.suite] values (test_colitur.ml), never each other's internal + helpers. *) +let sample_years = + let rec range a b = if a > b then [] else a :: range (a + 1) b in + range 2005 2050 + +let year_of y = Colitur_kernel.Calendar.year real_ef_rite real_ef_layer y + +(* Every day of every year resolves a formulary, for the same reason + [Validate] already asserts exactly one First and one Gospel: a day that + says no Mass at all is a defect, not a gap. Mirrors the "citations" + check. *) +let test_every_day_has_a_formulary () = + let missing = ref [] in + List.iter + (fun y -> + Array.iter + (fun (d : (_, _) Colitur_kernel.Liturgical_day.t) -> + if d.Colitur_kernel.Liturgical_day.formulary = None then + missing := + Colitur_kernel.Date.to_iso8601 d.Colitur_kernel.Liturgical_day.date :: !missing) + (year_of y)) + sample_years; + Alcotest.(check (list string)) "every day resolves a formulary" [] !missing + let suite = ( "Validate", [ Alcotest.test_case "landmark years" `Quick test_landmark_years; @@ -830,6 +923,13 @@ let suite = test_citations_fires_on_an_out_of_scope_part; Alcotest.test_case "citations-unresolved fires on a gap" `Quick test_citations_unresolved_fires_on_a_gap; + Alcotest.test_case "formulary silent without a lectionary" `Quick + test_formulary_silent_without_a_lectionary; + Alcotest.test_case "formulary clean when well formed" `Quick + test_formulary_clean_when_well_formed; + Alcotest.test_case "formulary fires on a gap" `Quick test_formulary_fires_on_a_gap; + Alcotest.test_case "every day (2005-2050) resolves a formulary" `Quick + test_every_day_has_a_formulary; Alcotest.test_case "lost fires on resolution exception" `Quick test_lost_fires_on_resolution_exception; Alcotest.test_case "duplicated fires" `Quick test_duplicated_fires; Alcotest.test_case "unconverged fires" `Quick test_unconverged_fires; |
