aboutsummaryrefslogtreecommitdiff
path: root/test/test_citation_coverage_of.ml
blob: 344470673670f401431d1e3f33a8701a5becf588 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
(* 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". 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".

   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 1540 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.

   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
   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). *)

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 -- 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 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";
    "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" ]

(* 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) "1540 citation fields in data/of/lectionary.sexp today -- if this changes, the \
                        pinned residual below may need updating too, not just this count"
    1540 !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;
  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. *)
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 ] )