diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-26 14:28:50 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-26 14:28:50 +0200 |
| commit | e5cbc662227de4f3ab0089fe73f639f4f84601a4 (patch) | |
| tree | 264b29424d2d3371f6f31f2b87ac111c74622808 /test | |
| parent | e5066c0d082a09eca3d4c8bbf8da1cc04dde9104 (diff) | |
| download | colitur-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')
| -rw-r--r-- | test/test_calendar.ml | 11 | ||||
| -rw-r--r-- | test/test_citation_coverage_of.ml | 31 | ||||
| -rw-r--r-- | test/test_lectionary_of.ml | 12 | ||||
| -rw-r--r-- | test/test_validate.ml | 73 |
4 files changed, 109 insertions, 18 deletions
diff --git a/test/test_calendar.ml b/test/test_calendar.ml index 9059574..0b8d3f7 100644 --- a/test/test_calendar.ml +++ b/test/test_calendar.ml @@ -122,7 +122,16 @@ module Fixture = 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)); - rules; season_runs = [ A; B ]; transfer_target; readings; creed; gloria; preface } + rules; season_runs = [ A; B ]; transfer_target; readings; + (* [readings] above is a harmless constant [(None, [])] -- this + fixture never actually exercises {!Colitur_kernel.Validate}'s + citation checks (it drives {!Colitur_kernel.Calendar.year} + directly), so any non-empty shape list satisfies {!Rite.t}'s + type without being meaningfully tested here. Mirrors EF's own + one-shape [[First; Gospel]] rather than inventing a bespoke + synthetic vocabulary for a field this file never reads. *) + citation_shapes = [ [ Colitur_kernel.Citation.First; Colitur_kernel.Citation.Gospel ] ]; + creed; gloria; preface } let entry ~month ~day ~slug ~rank = { Layer.date = (match Date_spec.fixed ~month ~day with Ok d -> d | Error e -> failwith e); diff --git a/test/test_citation_coverage_of.ml b/test/test_citation_coverage_of.ml index 70e63ad..7badd96 100644 --- a/test/test_citation_coverage_of.ml +++ b/test/test_citation_coverage_of.ml @@ -27,9 +27,20 @@ and {!Colitur_citation.Render}'s own consumption of it), deliberately not built this task. + UPDATED, fix wave I7 (final-review.md, 2026-08-25-colitur-of-phases-3-5, + 2026-08-26): {!Rite.t.citation_shapes} (kernel change) plus + tools/bootstrap_lectionary_of.ml's own [cite] now also extract a Second + reading on every Sunday-cycle (Sunday/solemnity) entry, so the total grew + 1542 -> 1725 (+183, exactly the count data/of/lectionary.sexp's own + header COVERAGE (5) now discloses) and the unparseable residual grew + 41 -> 49: the same chapter-crossing shape, now also hit by 8 of the 183 + new Second-reading references. Not a new failure MODE, just a bigger + population drawing from the same, already-understood one -- confirmed + below by [looks_chapter_crossing], unchanged, still holding for all 49. + This file pins that residual EXACTLY -- not merely "at least these fail" -- so it fails loudly in BOTH directions: a NEW unconverted reference - appearing (a regression) and one of these 41 starting to convert (this + appearing (a regression) and one of these 49 starting to convert (this list going stale, e.g. because a future task builds cross-chapter support) are both caught, the same discipline every allow-list in this project's suite already follows (`expected_rows`, `expected_bands`, the @@ -48,12 +59,14 @@ let lectionary () = can never silently drift apart without one of them visibly failing to regenerate/re-test clean. *) let known_cross_chapter_residual = - [ "1 Corinthians 12:31-13:13"; "1 John 1:5-2:2"; "1 John 2:29-3:6"; "1 John 3:22-4:6"; - "1 John 4:19-5:4"; "2 Corinthians 3:15-4:1,3-6"; "2 Samuel 18:9-10,14b,24-25a,30-19:3"; - "Colossians 1:24-2:3"; "Ecclesiastes 11:9-12:8"; "Ephesians 4:32-5:8"; "Exodus 11:10-12:14"; - "Exodus 14:21-15:1"; "Ezekiel 2:8-3:4"; "Galatians 4:22-24,26-27,31-5:1"; "Genesis 1:1-2:2"; - "Genesis 1:20-2:4a"; "Habakkuk 1:12-2:4"; "Hebrews 7:25-8:6"; "Isaiah 52:13-53:12"; - "Isaiah 8:23b-9:3"; "John 15:26-16:4a"; "John 18:1-19:42"; "Jonah 1:1-2:2,11"; "Luke 7:36-8:3"; + [ "1 Corinthians 10:31-11:1"; "1 Corinthians 12:31-13:13"; "1 John 1:5-2:2"; "1 John 2:29-3:6"; + "1 John 3:22-4:6"; "1 John 4:19-5:4"; "1 Thessalonians 3:12-4:2"; "2 Corinthians 3:15-4:1,3-6"; + "2 Corinthians 4:13-5:1"; "2 Samuel 18:9-10,14b,24-25a,30-19:3"; "2 Thessalonians 1:11-2:2"; + "2 Thessalonians 2:16-3:5"; "2 Timothy 3:14-4:2"; "Colossians 1:24-2:3"; "Ecclesiastes 11:9-12:8"; + "Ephesians 4:30-5:2"; "Ephesians 4:32-5:8"; "Exodus 11:10-12:14"; "Exodus 14:21-15:1"; + "Ezekiel 2:8-3:4"; "Galatians 4:22-24,26-27,31-5:1"; "Genesis 1:1-2:2"; "Genesis 1:20-2:4a"; + "Habakkuk 1:12-2:4"; "Hebrews 7:25-8:6"; "Isaiah 52:13-53:12"; "Isaiah 8:23b-9:3"; + "James 3:16-4:3"; "John 15:26-16:4a"; "John 18:1-19:42"; "Jonah 1:1-2:2,11"; "Luke 7:36-8:3"; "Malachi 1:14b-2:2b,8-10"; "Mark 2:23-3:6"; "Mark 8:34-9:1"; "Matthew 10:34-11:1"; "Matthew 18:21-19:1"; "Matthew 9:35-10:1,5a,6-8"; "Matthew 9:36-10:8"; "Numbers 13:1-2,25-14:1,26a-29a,34-35"; "Philippians 3:17-4:1"; "Revelation 20:1-4,11-21:2"; @@ -91,9 +104,9 @@ let test_of_citations_convert_or_are_the_known_cross_chapter_residual () = cits) (Colitur_kernel.Lectionary.entries (lectionary ())); let bad = List.sort compare !bad in - Alcotest.(check int) "1542 citation fields in data/of/lectionary.sexp today -- if this changes, the \ + Alcotest.(check int) "1725 citation fields in data/of/lectionary.sexp today -- if this changes, the \ pinned residual below may need updating too, not just this count" - 1542 !total; + 1725 !total; Alcotest.(check (list string)) "the unconverted set is EXACTLY the known cross-chapter residual, no \ more and no fewer" (List.sort compare known_cross_chapter_residual) bad; diff --git a/test/test_lectionary_of.ml b/test/test_lectionary_of.ml index 03aaea5..706ee15 100644 --- a/test/test_lectionary_of.ml +++ b/test/test_lectionary_of.ml @@ -183,9 +183,17 @@ let test_sha256_pinned () = the Christmas Day Mass hand-authored from OLM 1981; see test_coverage_2026's own citation), not header-only as the I2 change just above was. Re-derived with `sha256sum data/of/lectionary.sexp`, - same discipline. *) + same discipline. + + Fix wave I7 (final-review.md, 2026-08-25-colitur-of-phases-3-5, + 2026-08-26): changed again -- {!Colitur_kernel.Rite.t.citation_shapes} + (kernel change) plus tools/bootstrap_lectionary_of.ml's own [cite] now + also extract a Second reading on every Sunday-cycle entry (183 new + citation fields; see data/of/lectionary.sexp's own header COVERAGE (5) + and test_citation_coverage_of.ml), and the header's "Epistle" wording + is fixed to "First". Re-derived the same way, same discipline. *) Alcotest.(check string) "data/of/lectionary.sexp SHA-256" - "e08207614472955974b504bfe6984ff44e882c4d8fadc778c4fa1ddce1adecf8" (sha256_of_file lectionary_path) + "0d82069e879d8312943dee543aabc23208265534fcf7edd85c3d9137c2f4581d" (sha256_of_file lectionary_path) (* lectio's OWN of-lectionary.ini SHA-256, pinned inside data/of/ lectionary.sexp's own provenance header (tools/bootstrap_lectionary_of 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 |
