aboutsummaryrefslogtreecommitdiff
path: root/test/test_validate.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 14:28:50 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 14:28:50 +0200
commite5cbc662227de4f3ab0089fe73f639f4f84601a4 (patch)
tree264b29424d2d3371f6f31f2b87ac111c74622808 /test/test_validate.ml
parente5066c0d082a09eca3d4c8bbf8da1cc04dde9104 (diff)
downloadcolitur-e5cbc662227de4f3ab0089fe73f639f4f84601a4.tar.gz
colitur-e5cbc662227de4f3ab0089fe73f639f4f84601a4.zip
fix(citation): make the well-formed reading-part set rite-supplied
I7 (final-review.md, 2026-08-25-colitur-of-phases-3-5): OF Sundays and solemnities shipped without the Second reading, and the lectionary data header called the First reading an "Epistle" -- EF vocabulary. The sole blocker was Validate's own kernel-hardcoded assertion that a well-formed day's sorted citation parts are exactly [First; Gospel]; Citation.part already carried Second. Rite.t gains citation_shapes : Citation.part list list, the fourth EF-shaped kernel misfit (after Preface.t, Mass_formulary.source, transfer_target's strictly-later contract) and the first fixed rather than only documented. EF supplies its one shape unchanged; OF supplies two, [First; Gospel] on a feria/feast/memorial and [First; Second; Gospel] on a Sunday or solemnity (OLM 1981 Praenotanda n. 66.1 vs n. 69.1, n. 84(b)/(c), page-image verified against olm-1981.pdf). Validate's "citations" check now matches against the rite's own declared shapes rather than a single literal pattern, preserving all three invariants it always enforced: zero or one complete shape, never partial; no part outside what the rite declares; no part repeated. bootstrap_lectionary_of.ml's cite now extracts a Second reading from Sunday-cycle ini sections only, correctly excluding the one stray `second` field lectio's snapshot carries on a weekday-cycle section (a content-verified scraping artifact). The hand-authored Christmas Day entry gains its own Second reading from the same OLM passage already cited for its First/Gospel. Regenerated data/of/lectionary.sexp: 771 entries unchanged, citation fields 1542 -> 1725 (+183, all Second readings), SHA-256 re-pinned, header wording fixed and a new coverage bullet added. Responsorial Psalm is deliberately still not emitted -- OLM 1981 Praenotanda n. 71 itself lists it apart from the readings proper as a chant with its own refrain, and the finding driving this change names only the missing Second reading. EF output verified byte-identical to the branch start (empty lib/rites/rite_ef + data/ef diff since v1.0.0, plus a direct binary comparison of `day`/`readings` for 2026, 1583 and 9999 against a worktree build of the pre-change commit). Tests: three new fixtures in test_validate.ml prove the generalisation holds both directions (a multi-shape rite accepts either of its declared shapes; a rite that never declared a shape still rejects it). test_citation_coverage_of.ml's pinned residual grows 41 -> 49, all new entries the same already-understood chapter-crossing shape.
Diffstat (limited to 'test/test_validate.ml')
-rw-r--r--test/test_validate.ml73
1 files changed, 67 insertions, 6 deletions
diff --git a/test/test_validate.ml b/test/test_validate.ml
index 3da8fbb..4e21829 100644
--- a/test/test_validate.ml
+++ b/test/test_validate.ml
@@ -318,6 +318,23 @@ module Synthetic = struct
[ { Citation.part = Citation.First; reference = "Synth 1:1" };
{ Citation.part = Citation.Gospel; reference = "Synth 2:2" } ]
+ (* A second, OF-shaped well-formed set -- three parts, not two -- so the
+ "more than one declared shape" fixtures below (Task: rite-supplied
+ citation shapes) have real, distinct content to offer, the same
+ "assert SHAPE, never content" discipline [well_formed_citations]
+ above already follows. *)
+ let well_formed_citations_with_second =
+ [ { Citation.part = Citation.First; reference = "Synth 1:1" };
+ { Citation.part = Citation.Second; reference = "Synth 3:3" };
+ { Citation.part = Citation.Gospel; reference = "Synth 2:2" } ]
+
+ (* The default: the one shape every fixture above this point already
+ assumed implicitly, back when [Validate] hardcoded [[First; Gospel]]
+ itself. Overridable ([?citation_shapes] below) for the fixtures that
+ need a rite declaring MORE than one shape, mirroring the real OF's own
+ {!Rite_of.Rite_of.context}. *)
+ let citation_shapes = [ [ Citation.First; Citation.Gospel ] ]
+
(* The formulary equivalent of [well_formed_citations] above -- shape only,
never rubrically meaningful content. *)
let well_formed_formulary =
@@ -335,8 +352,9 @@ module Synthetic = struct
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) ?(creed = creed)
- ?(gloria = gloria) ?(preface = preface) temporal : (season, rank) Rite.t =
+ ?(transfer_target = fun _ origin _ -> origin) ?(readings = readings)
+ ?(citation_shapes = citation_shapes) ?(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
@@ -346,7 +364,7 @@ module Synthetic = struct
(* 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 }
+ transfer_target; readings; citation_shapes; 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
@@ -355,10 +373,10 @@ module Synthetic = struct
unchallenged). Task 12's own resolution fixtures pass their own. *)
let empty_layer = Layer.empty ~id:"synthetic-empty" ~name:"empty"
- let run ?vocab ?anchors ?season_runs ?rules ?transfer_target ?readings ?(layer = empty_layer)
- temporal =
+ let run ?vocab ?anchors ?season_runs ?rules ?transfer_target ?readings ?citation_shapes
+ ?(layer = empty_layer) temporal =
Val.run
- (rite ?vocab ?anchors ?season_runs ?rules ?transfer_target ?readings temporal)
+ (rite ?vocab ?anchors ?season_runs ?rules ?transfer_target ?readings ?citation_shapes temporal)
layer ~year:2026
let has_check check (fs : Val.failure list) = List.exists (fun f -> f.Val.check = check) fs
@@ -805,6 +823,43 @@ let test_citations_fires_on_an_out_of_scope_part () =
Alcotest.(check bool) "citations check fires when a part outside First/Gospel appears" true
(has_check "citations" (run ~readings good))
+(* ---- rite-supplied citation shapes (Second-reading kernel change) ----
+
+ [Rite.t.citation_shapes] replaced a single kernel-hardcoded
+ [[First; Gospel]] with a rite-supplied LIST of well-formed shapes -- the
+ OF declares two, EF still only the one. These three fixtures prove the
+ generalisation actually does work, both directions: a multi-shape rite
+ accepts EITHER of its own declared shapes, and a rite that never declared
+ a shape still rejects it even though that exact shape is well-formed for
+ a DIFFERENT (multi-shape) rite -- the check reads the offered rite's own
+ declaration, not a kernel-wide allowance the OF's addition would
+ otherwise silently open up for every rite, EF included. *)
+let multi_shape =
+ [ [ Citation.First; Citation.Gospel ]; [ Citation.First; Citation.Second; Citation.Gospel ] ]
+
+let test_citations_clean_with_a_second_declared_shape () =
+ let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ =
+ (None, well_formed_citations_with_second)
+ in
+ let fs = run ~readings ~citation_shapes:multi_shape good in
+ Alcotest.(check bool) "a rite that declares [First; Second; Gospel] accepts a day carrying it" false
+ (has_check "citations" fs || has_check "citations-unresolved" fs)
+
+let test_citations_clean_when_a_multi_shape_rite_uses_its_other_shape_too () =
+ let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = (None, well_formed_citations) in
+ let fs = run ~readings ~citation_shapes:multi_shape good in
+ Alcotest.(check bool)
+ "the same multi-shape rite also accepts a day carrying only its [First; Gospel] shape" false
+ (has_check "citations" fs || has_check "citations-unresolved" fs)
+
+let test_citations_fires_on_a_shape_the_rite_itself_never_declared () =
+ let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ =
+ (None, well_formed_citations_with_second)
+ in
+ Alcotest.(check bool)
+ "a rite that never declared [First; Second; Gospel] still flags a day carrying it" true
+ (has_check "citations" (run ~readings good))
+
(* The coverage half, kept a SEPARATE check name from the three above: a rite
that resolves readings on most days but falls through on one. On real EF
data this has no witness at all, so this fixture is the only thing that
@@ -923,6 +978,12 @@ 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 "citations clean with a second declared shape" `Quick
+ test_citations_clean_with_a_second_declared_shape;
+ Alcotest.test_case "citations clean when a multi-shape rite uses its other shape too" `Quick
+ test_citations_clean_when_a_multi_shape_rite_uses_its_other_shape_too;
+ Alcotest.test_case "citations fires on a shape the rite itself never declared" `Quick
+ test_citations_fires_on_a_shape_the_rite_itself_never_declared;
Alcotest.test_case "formulary silent without a lectionary" `Quick
test_formulary_silent_without_a_lectionary;
Alcotest.test_case "formulary clean when well formed" `Quick