summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 23:53:49 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 23:53:58 +0200
commit90584d87e763789808329847c46dbaa8e22dca47 (patch)
tree6b5e7c29adc0d1cf5fdfb71cfb8f097867bf04b5 /test
parent9ebb06983b7a26db5564302253f2551dfbcf834e (diff)
downloadcolitur-90584d87e763789808329847c46dbaa8e22dca47.tar.gz
colitur-90584d87e763789808329847c46dbaa8e22dca47.zip
fix(citation): represent chapter-crossing verse ranges (W4)
Parse.verse_range's [last] endpoint gains an optional chapter (Parse.verse_end: { chapter : int option; verse : verse_num }), so a hyphen range whose two endpoints lie in different chapters ("1 John 1:5-2:2") can be represented at all. Rejected: a bare [int option] living alongside [last] as a second field on verse_range -- that would let 'a chapter with no verse' exist as a constructible value. parse_part now splits a part's leading "chapter:" at the FIRST colon only (not every colon), so the verses side can itself carry a second colon from a crossing range. parse_range detects a crossing by checking whether the range's right-hand side contains ':', and skips the same-chapter descending-range guard for that case (a later chapter is always "ahead", whatever its own verse numbers are). Handles the compound shape too -- a crossing range followed by further, same-chapter verse references in the same comma list ("Matthew 9:35-10:1,5a,6-8") -- since those trailing pieces parse as ordinary bare verses/ranges, unaffected by the preceding crossing. Render's one_range renders a crossing [last] through the same chapter_verse template one_part already uses for the part's own leading "chapter:verses", so a style that reconfigures the chapter/verse separator renders a crossing endpoint in that same convention rather than a hardcoded ':'. This closes the W4 known-wrong: 41 (now 49, after an intervening Second-reading extraction) of the OF lectionary's citations printed unconverted, every one this exact shape. test_citation_coverage_of.ml's pinned residual is now empty and asserted exactly, over the full 1725-field data/of/lectionary.sexp population, including the round-trip check (parse -> render -> parse structural equality). test_citation.ml gains direct parse-suite cases for the basic crossing, the compound shape, a mid-list crossing, a crossing with a sub-verse letter, and a malformed-crossing rejection. data/of/lectionary.sexp is regenerated via its own generator (tools/bootstrap_lectionary_of.ml, whose own embedded header text is updated to match); only comment lines change, confirmed by diff -- no lectionary entry differs. test_lectionary_of.ml's whole-file SHA-256 pin is updated to match. EF is unaffected: data/ef/ is untouched since v1.0.0, and a direct byte comparison of `colitur day`/`colitur readings` for 2026, 1583 and 9999 against a git-worktree build of 1c0137d is identical on all six outputs.
Diffstat (limited to 'test')
-rw-r--r--test/test_citation.ml32
-rw-r--r--test/test_citation_coverage_of.ml109
-rw-r--r--test/test_lectionary_of.ml12
3 files changed, 88 insertions, 65 deletions
diff --git a/test/test_citation.ml b/test/test_citation.ml
index 3cbc8c6..ffbefe8 100644
--- a/test/test_citation.ml
+++ b/test/test_citation.ml
@@ -265,13 +265,18 @@ let suite =
module P = Colitur_citation.Parse
(* Render a parse back to a debug string so a test can assert shape
- compactly: "book|chapter:v-v,v-v|chapter:v". *)
+ compactly: "book|chapter:v-v,v-v|chapter:v". A [last] that names its own,
+ later chapter (a chapter-crossing range) shows as "v-C:v", the same
+ "chapter:verse" shape a plain part already uses -- so a crossing is
+ visible in the debug string exactly where the source text put it. *)
let show (t : P.t) =
let num (v : P.verse_num) = string_of_int v.P.n ^ v.P.suffix in
let range (r : P.verse_range) =
match r.P.last with
| None -> num r.P.first
- | Some l -> Printf.sprintf "%s-%s" (num r.P.first) (num l)
+ | Some { P.chapter = None; verse } -> Printf.sprintf "%s-%s" (num r.P.first) (num verse)
+ | Some { P.chapter = Some c; verse } ->
+ Printf.sprintf "%s-%d:%s" (num r.P.first) c (num verse)
in
let part (p : P.part) =
Printf.sprintf "%d:%s" p.P.chapter
@@ -319,6 +324,29 @@ let parse_suite =
("four parts", `Quick, parses "Eccli 24:5; 14:7; 14:9-11; 24:30-31"
"ecclesiasticus|24:5|14:7|14:9-11|24:30-31");
("modern name, vulgate id", `Quick, parses "Rev 12:1" "apocalypse|12:1");
+ (* Chapter-crossing ranges (W4): a hyphen range whose two endpoints lie
+ in different chapters -- real shapes from data/of/lectionary.sexp,
+ previously unparseable in their entirety (parse.mli's own
+ [verse_end]). *)
+ ("cross-chapter range", `Quick, parses "1 John 1:5-2:2" "john_1|1:5-2:2");
+ (* The compound shape: a chapter-crossing range FOLLOWED by further,
+ same-chapter verse references in the same comma list -- "5a" and
+ "6-8" belong to chapter 10, the chapter the preceding range just
+ crossed into, never chapter 9. A naive fix that only special-cased
+ "the whole part is one crossing range" breaks exactly here. *)
+ ("cross-chapter then more verses (compound)", `Quick,
+ parses "Matthew 9:35-10:1,5a,6-8" "matthew|9:35-10:1,5a,6-8");
+ (* The crossing is not the first comma-piece, and a normal same-chapter
+ range ("24-25a") sits between two others -- proves the crossing is
+ detected per-piece, not merely at a fixed position in the list. *)
+ ("cross-chapter mid-list", `Quick,
+ parses "2 Samuel 18:9-10,14b,24-25a,30-19:3" "samuel_2|18:9-10,14b,24-25a,30-19:3");
+ (* Verse sub-letters survive on a chapter-crossing endpoint too. *)
+ ("cross-chapter with sub-verse letter", `Quick,
+ parses "Genesis 1:20-2:4a" "genesis|1:20-2:4a");
+ (* A malformed crossing (no verse named after the second chapter) is
+ rejected, not silently misread. *)
+ ("rejects malformed cross-chapter", `Quick, rejects "Luke 1:5-2:");
(* Ordinals 3 and 4 must survive [split_book]'s leading-digit scan.
Narrowing its '1'..'4' range to '1'..'3' passes every OTHER case in
this suite silently, while seven real citations depend on it. A
diff --git a/test/test_citation_coverage_of.ml b/test/test_citation_coverage_of.ml
index 7badd96..af8d161 100644
--- a/test/test_citation_coverage_of.ml
+++ b/test/test_citation_coverage_of.ml
@@ -3,48 +3,46 @@
(* Fix wave I2 (final-review.md, 2026-08-25-colitur-of-phases-3-5): the OF
counterpart of test_citation_coverage.ml. That file's own header states
the rule this one follows: "every citation the engine can emit must
- parse". EF reaches zero unconverted citations; OF does not, and this
- file is the honest record of exactly where it falls short, mirroring the
- review's own explicit instruction -- "enumerate them rather than
- lowering the bar to fit".
+ parse". At the time this file was first written, EF reached zero
+ unconverted citations and OF did not; this file was the honest record of
+ exactly where it fell short.
- MEASURED, not estimated: before this fix wave, 259 of 730 citation
- fields in `colitur readings --rite of 2026` (35%) printed unconverted --
- 345 distinct book spellings unregistered ({!Colitur_citation.Book}),
- plus verse sub-letter markers ("11a") and a handful of single-chapter-
- book misreads ({!Colitur_citation.Book.is_single_chapter}) the parser
- could not read at all. All of that is now fixed at the source (book.ml's
- own [of_lectionary_table] and parse.ml's own [verse_num]/[single_chapter]
- additions), closing every genuinely fixable shape. What remains, walking
- the FULL data/of/lectionary.sexp (not merely what one civil year happens
- to observe -- a stricter check than EF's own year-walk, and the same one
- tools/bootstrap_lectionary_of.ml's own generated header now runs and
- discloses): 41 distinct references, out of 1542 total, EVERY ONE a
- hyphenated range crossing a chapter boundary ("2:29-3:6", "1 John") --
- a {!Colitur_citation.Parse.t} shape [part]/[verse_range] do not
- represent (a [part] is one chapter and a list of within-chapter ranges;
- representing a cross-chapter range would need restructuring that type
- and {!Colitur_citation.Render}'s own consumption of it), deliberately
- not built this task.
+ MEASURED, not estimated: before fix wave I2, 259 of 730 citation fields
+ in `colitur readings --rite of 2026` (35%) printed unconverted -- 345
+ distinct book spellings unregistered ({!Colitur_citation.Book}), plus
+ verse sub-letter markers ("11a") and a handful of single-chapter-book
+ misreads ({!Colitur_citation.Book.is_single_chapter}) the parser could
+ not read at all. All of that was fixed at the source (book.ml's own
+ [of_lectionary_table] and parse.ml's own [verse_num]/[single_chapter]
+ additions). What remained, walking the FULL data/of/lectionary.sexp (not
+ merely what one civil year happens to observe -- a stricter check than
+ EF's own year-walk, and the same one tools/bootstrap_lectionary_of.ml's
+ own generated header runs and discloses): a hyphenated verse range whose
+ two endpoints lie in different chapters ("2:29-3:6", "1 John") -- a
+ {!Colitur_citation.Parse.t} shape [part]/[verse_range] did not
+ represent. Fix wave I7 (2026-08-26) grew the population from 1542 to
+ 1725 citation fields (a new Second-reading extraction) and the residual
+ from 41 to 49 references, same shape, no new failure mode.
- 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.
+ CLOSED (W4, this task): {!Colitur_citation.Parse.verse_end} now lets a
+ range's [last] endpoint name its own, later chapter, and
+ {!Colitur_citation.Render}'s [one_range] renders it back through the
+ part's own [chapter_verse] template. Every one of the 49 pinned
+ references now parses and round-trips -- including the COMPOUND shape
+ ("Matthew 9:35-10:1,5a,6-8": a crossing range followed by further,
+ same-chapter verse references in the same comma list) and a crossing
+ that is not the list's first or only piece ("2 Samuel
+ 18:9-10,14b,24-25a,30-19:3"). The residual below is therefore now EMPTY,
+ over the full 1725-field population -- measured, not merely no longer
+ observed on a sample.
- 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 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
- litcal/missalemeum comparators). *)
+ The pinning discipline is kept rather than deleted: this file still
+ fails LOUDLY, in both directions, if the unconverted set ever stops
+ being empty -- a real regression (a genuinely new unparseable shape) and
+ a silently corrupted lectionary file are both caught by the same
+ mechanism every allow-list in this project's suite already follows
+ (`expected_rows`, `expected_bands`, the litcal/missalemeum
+ comparators). *)
let lectionary_path = "../data/of/lectionary.sexp"
@@ -53,25 +51,12 @@ let lectionary () =
| Ok l -> l
| Error e -> Alcotest.failf "%s: %s" lectionary_path e
-(* The exact, pinned residual -- copied from data/of/lectionary.sexp's own
- generated "Citations that do not parse" listing, itself produced by the
- SAME {!Colitur_citation.Parse.parse} call this test makes, so the two
- can never silently drift apart without one of them visibly failing to
- regenerate/re-test clean. *)
-let known_cross_chapter_residual =
- [ "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";
- "Sirach 27:30-28:7"; "The Acts 12:24-13:5a"; "The Acts 17:15,22-18:1"; "The Acts 7:51-8:1a";
- "Wisdom 11:22-12:2"; "Wisdom 2:23-3:9"; "Wisdom 7:22b-8:1" ]
+(* The exact, pinned residual -- EMPTY since W4 closed the cross-chapter gap
+ (see this file's own header). Kept as a named binding, not inlined as
+ [[]] at each use site, so a future partial regression has one place to
+ list what it could not fix, exactly as the pre-W4 version of this file
+ did for the 49 references it once named here. *)
+let known_cross_chapter_residual = []
(* Every failure this walk finds really is a chapter-crossing hyphen range
(a "-" whose right side itself contains a ":") -- checked mechanically,
@@ -107,8 +92,8 @@ let test_of_citations_convert_or_are_the_known_cross_chapter_residual () =
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"
1725 !total;
- Alcotest.(check (list string)) "the unconverted set is EXACTLY the known cross-chapter residual, no \
- more and no fewer"
+ Alcotest.(check (list string)) "the unconverted set is EXACTLY the known cross-chapter residual (empty, \
+ since W4) -- no more and no fewer"
(List.sort compare known_cross_chapter_residual) bad;
List.iter
(fun r ->
@@ -119,7 +104,9 @@ let test_of_citations_convert_or_are_the_known_cross_chapter_residual () =
(* Everything OUTSIDE the pinned residual round-trips through Sigla, the
same structural (not textual) comparison test_citation_coverage.ml's own
- EF version makes, and for the identical reason its own header gives. *)
+ EF version makes, and for the identical reason its own header gives.
+ Since the residual is now empty, this exercises the FULL 1725-field
+ population, cross-chapter shapes included. *)
let test_round_trip_outside_the_residual () =
let sg =
Colitur_citation.Sigla.make ~style:Colitur_citation.Render.default_style
diff --git a/test/test_lectionary_of.ml b/test/test_lectionary_of.ml
index 706ee15..dc02176 100644
--- a/test/test_lectionary_of.ml
+++ b/test/test_lectionary_of.ml
@@ -178,7 +178,15 @@ let sha256_of_file path =
don't transcribe it from generator stdout" discipline
test_calendar_of_data.ml's own [test_sha256] already follows. *)
let test_sha256_pinned () =
- (* Fix wave I8 (final-review.md, 2026-08-25-colitur-of-phases-3-5): the
+ (* W4 (this task): header-only change, same discipline as I2 below --
+ {!Colitur_citation.Parse.verse_end} closed the chapter-crossing gap,
+ so the residual the header's own COVERAGE (4) and the trailing
+ "Citations that do not parse" listing disclose went from 49 to 0. No
+ lectionary ENTRY changed (`git diff --stat` on this file shows only
+ comment lines). Re-derived with `sha256sum data/of/lectionary.sexp`,
+ same discipline.
+
+ Fix wave I8 (final-review.md, 2026-08-25-colitur-of-phases-3-5): the
hash changed again -- ONE genuine new entry this time ("of-nativity",
the Christmas Day Mass hand-authored from OLM 1981; see
test_coverage_2026's own citation), not header-only as the I2 change
@@ -193,7 +201,7 @@ let test_sha256_pinned () =
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"
- "0d82069e879d8312943dee543aabc23208265534fcf7edd85c3d9137c2f4581d" (sha256_of_file lectionary_path)
+ "788ddc6d2d783286b6e64882a5372ae213cd5462c887e68aa2bea22388cdb4f7" (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