summaryrefslogtreecommitdiff
path: root/test/test_citation_coverage_of.ml
blob: af8d1614da540bb877fbef707e80895018d44e85 (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". 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 ] )