(* SPDX-License-Identifier: AGPL-3.0-or-later *) (* 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". 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 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. 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. 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" let lectionary () = match Colitur_kernel.Lectionary.load lectionary_path with | Ok l -> l | Error e -> Alcotest.failf "%s: %s" lectionary_path e (* 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, not merely asserted, so a future genuinely-different failure shape cannot hide inside this test by coincidentally also being in the pinned list above. No [Str]/regex (frozen deps): plain character scanning. *) let looks_chapter_crossing reference = let n = String.length reference in let rec scan i saw_dash = if i >= n then false else if reference.[i] = '-' then scan (i + 1) true else if reference.[i] = ':' && saw_dash then true else scan (i + 1) saw_dash in scan 0 false let test_of_citations_convert_or_are_the_known_cross_chapter_residual () = let total = ref 0 in let bad = ref [] in List.iter (fun (_slug, cits) -> List.iter (fun (c : Colitur_kernel.Citation.t) -> incr total; match Colitur_citation.Parse.parse c.Colitur_kernel.Citation.reference with | Ok _ -> () | Error _ -> if not (List.mem c.Colitur_kernel.Citation.reference !bad) then bad := c.Colitur_kernel.Citation.reference :: !bad) cits) (Colitur_kernel.Lectionary.entries (lectionary ())); let bad = List.sort compare !bad in 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 (empty, \ since W4) -- no more and no fewer" (List.sort compare known_cross_chapter_residual) bad; List.iter (fun r -> Alcotest.(check bool) (Printf.sprintf "%s: really is a chapter-crossing range, not some other \ unrelated failure shape" r) true (looks_chapter_crossing r)) bad (* 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. 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 ~tradition:Colitur_citation.Book.vulgate ~names:(fun id _form -> Colitur_citation.Book.default_spelling id) in let bad = ref [] in List.iter (fun (_slug, cits) -> List.iter (fun (c : Colitur_kernel.Citation.t) -> let r = c.Colitur_kernel.Citation.reference in if not (List.mem r known_cross_chapter_residual) then match Colitur_citation.Parse.parse r with | Error e -> bad := Printf.sprintf "%s: unexpectedly does not parse (%s)" r e :: !bad | Ok first -> ( let rendered = Colitur_citation.Sigla.format sg r in match Colitur_citation.Parse.parse rendered with | Error e -> bad := Printf.sprintf "%s: re-parse failed: %s" r e :: !bad | Ok again -> if again <> first then bad := Printf.sprintf "%s: structure changed: %s" r rendered :: !bad)) cits) (Colitur_kernel.Lectionary.entries (lectionary ())); Alcotest.(check (list string)) "every OF citation outside the pinned residual round-trips through Sigla" [] (List.sort compare !bad) let suite = ( "citation-coverage-of", [ Alcotest.test_case "every OF citation converts, or is the pinned cross-chapter residual" `Slow test_of_citations_convert_or_are_the_known_cross_chapter_residual; Alcotest.test_case "every OF citation outside the residual round-trips" `Slow test_round_trip_outside_the_residual ] )