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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
|
(* SPDX-License-Identifier: AGPL-3.0-or-later *)
type id = string
let to_string t = t
(* Every book cited across the shipped EF data (lectionary, sanctoral
propers, and commons), with every spelling any of the three files uses.
The dotted/undotted and modern/Vulgate pairs are inherited from lectio --
see book.mli. Surveyed directly against the data, all three files, not
the lectionary alone -- sanctoral.sexp alone carries more citations than
the lectionary and was the source of every token missed in the first
pass. *)
let table =
[ ("genesis", [ "Gen"; "Liber Genesis"; "Genesis" ]);
("exodus", [ "Ex"; "Exod"; "Liber Exodi"; "Exodus" ]);
("leviticus", [ "Lev"; "Levit"; "Liber Levitici"; "Leviticus" ]);
("numbers", [ "Num"; "Liber Numeri"; "Numbers" ]);
("kings_3", [ "3 Kings"; "3 Kgs."; "3 Reg"; "Liber Regum III" ]);
("kings_4", [ "4 Kings"; "4 Reg"; "Liber Regum IV"; "4 Kgs." ]);
("esdras_2", [ "2 Esd."; "2 Esdr"; "Liber Esdrae"; "2 Esdras" ]);
("tobit", [ "Tob"; "Liber Tobiae"; "Tobias"; "Tobit" ]);
("judith", [ "Judith"; "Iudith"; "Liber Iudith"; "Jth" ]);
("esther", [ "Esther"; "Esth"; "Liber Esther" ]);
("proverbs", [ "Prov"; "Proverbs" ]);
("song_of_songs", [ "Song"; "Cant."; "Canticle of Canticles"; "Song of Solomon" ]);
("wisdom", [ "Wis"; "Wis."; "Sap"; "Liber Sapientiae"; "Wisdom" ]);
("ecclesiasticus", [ "Ecclus"; "Sir"; "Eccli"; "Ecclesiasticus" ]);
("isaiah", [ "Isa"; "Isa."; "Isai"; "Isaias Propheta"; "Isaias"; "Isaiah" ]);
("jeremiah", [ "Jer"; "Ier"; "Ieremias Propheta"; "Jeremias"; "Jeremiah" ]);
("ezekiel", [ "Ezech"; "Ezek"; "Ezechiel Propheta"; "Ezechiel"; "Ezekiel" ]);
("daniel", [ "Dan"; "Daniel Propheta"; "Daniel" ]);
("osee", [ "Osee"; "Osee Propheta" ]);
("joel", [ "Joel"; "Ioel"; "Ioel Propheta" ]);
("jonas", [ "Jonas"; "Ionae"; "Ionas Propheta" ]);
("malachi", [ "Mal"; "Malach"; "Malachias Propheta"; "Malachias"; "Malachi" ]);
("matthew", [ "Matt"; "Matt."; "Matth"; "Mat"; "Evangelium secundum Matthaeum"; "Matthew" ]);
("mark", [ "Mark"; "Marc"; "Evangelium secundum Marcum" ]);
("luke", [ "Luke"; "Luc"; "Evangelium secundum Lucam" ]);
("john", [ "John"; "Ioann"; "Evangelium secundum Ioannem" ]);
("acts", [ "Acts"; "Act"; "Actus Apostolorum"; "Acts of the Apostles"; "The Acts" ]);
("romans", [ "Rom"; "Epistola ad Romanos"; "Romans" ]);
("corinthians_1", [ "1 Cor"; "1 Cor."; "Epistola I ad Corinthios"; "1 Corinthians" ]);
("corinthians_2", [ "2 Cor"; "2 Cor."; "Epistola II ad Corinthios"; "2 Corinthians" ]);
("galatians", [ "Gal"; "Epistola ad Galatas"; "Galatians" ]);
("ephesians", [ "Eph"; "Eph."; "Ephes"; "Epistola ad Ephesios"; "Ephesians" ]);
("philippians", [ "Phil"; "Epistola ad Philippenses"; "Philippians" ]);
("colossians", [ "Col"; "Col."; "Epistola ad Colossenses"; "Colossians" ]);
("thessalonians_1", [ "1 Thess"; "1 Thess."; "Epistola I ad Thessalonicenses"; "1 Thessalonians" ]);
("thessalonians_2", [ "2 Thess"; "Epistola II ad Thessalonicenses"; "2 Thessalonians" ]);
("timothy_1", [ "1 Tim."; "1 Tim"; "Epistola I ad Timotheum"; "1 Timothy" ]);
("timothy_2", [ "2 Tim"; "2 Tim."; "Epistola II ad Timotheum"; "2 Timothy" ]);
("titus", [ "Titus"; "Tit"; "Epistola ad Titum" ]);
("hebrews", [ "Heb"; "Hebr"; "Epistola ad Hebraeos"; "Hebrews" ]);
("james", [ "Jas"; "James"; "Iac"; "Epistola beati Iacobi Apostoli" ]);
("peter_1", [ "1 Pet"; "1 Pet."; "1 Petri"; "Epistola I beati Petri Apostoli"; "1 Peter" ]);
("peter_2", [ "2 Pet."; "2 Petri"; "Epistola II beati Petri Apostoli"; "2 Pet"; "2 Peter" ]);
("john_1", [ "1 John"; "1 Ioann"; "Epistola beati Ioannis Apostoli" ]);
(* "Apoc" is the Vulgate spelling and "Rev" its modern equivalent, but
BOTH sit inside Vulgate-tradition data, so both resolve to the same
Vulgate id here -- see book.mli's note by [apocalypse] never being an
[of_token] result under that name. Do not add "revelation" as a
spelling: it exists only as a tradition target (below), and giving it
an [of_token] entry would let one book carry two different ids. *)
("apocalypse", [ "Apoc"; "Rev"; "Liber Apocalypsis"; "Apocalypse" ]) ]
(* Fix wave I2 (final-review.md, 2026-08-25-colitur-of-phases-3-5): books
data/of/lectionary.sexp cites that no EF file ever needed, so the survey
above never registered them -- 345 of 730 OF citation fields printed
unconverted (35%) because [Book.of_token] had never heard of them, not
because [Parse]'s grammar itself could not read the reference. English-
canonical spellings only, verified directly against that one file
(`grep`, not invented) -- the SAME "every spelling any of the files uses"
discipline [table] above states, extended to a fourth file.
Deliberately a SEPARATE table, not folded into [table] above, for one
reason that matters: [table]'s ids are covered by [all] below, and
book.mli's own contract for [all] is "every SHIPPED language file is
asserted complete over" it (test_lang_coverage.ml's [test_every_book_
named] and its siblings). lang/la.ini's own header is equally strict --
"do not invent a Latin title", every `.full`/`.abbr` row cited to an
exact scan line -- and NONE of these 25 books is attested anywhere in
the EF's own scans (the EF lectionary/propers/commons never read from
Ruth, Job, Judges, Philemon, ... at all). Registering them in [table]
would force a choice between fabricating 50 uncited Latin titles (the
one thing this project refuses hardest) and quietly weakening book.mli's
own stated promise for the ids that DO have one. Neither is right, so
these ids are resolvable ([of_token]/[tokens]/[default_spelling] below
all include them) but excluded from [all] -- a citation using one still
PARSES and RENDERS (bin/main.ml's own [names_of] already falls back to
{!default_spelling} on a lang-table miss, book.mli's own documented
reason that function exists), it just renders in its own English
spelling rather than a Latin one no primary source has ever supplied,
the exact "degraded, not broken" contract {!default_spelling} documents.
[samuel_1]/[samuel_2] in particular are NOT [kings_1]/[kings_2] below:
those tradition targets already denote (via traditions.ini's own
[modern] mapping, "kings_3 = kings_1") the SAME physical book as the
Vulgate's own [kings_3]/[kings_4] ("3/4 Kings" = modern "1/2 Kings") --
a different pair of books from Samuel ("Liber Regum I/II" in the
Vulgate's own naming, never cited by any shipped EF data and so never
given an id before now). *)
let of_lectionary_table =
[ ("samuel_1", [ "1 Sam"; "1 Samuel" ]);
("samuel_2", [ "2 Sam"; "2 Samuel" ]);
("joshua", [ "Josh"; "Joshua" ]);
("judges", [ "Judg"; "Judges" ]);
("ruth", [ "Ruth" ]);
("deuteronomy", [ "Deut"; "Deuteronomy" ]);
("chronicles_1", [ "1 Chron"; "1 Chronicles" ]);
("chronicles_2", [ "2 Chron"; "2 Chronicles" ]);
("ezra", [ "Ezra" ]);
("job", [ "Job" ]);
("ecclesiastes", [ "Eccl"; "Ecclesiastes" ]);
("baruch", [ "Bar"; "Baruch" ]);
("lamentations", [ "Lam"; "Lamentations" ]);
("amos", [ "Amos" ]);
("micah", [ "Mic"; "Micah" ]);
("nahum", [ "Nah"; "Nahum" ]);
("habakkuk", [ "Hab"; "Habakkuk" ]);
("zephaniah", [ "Zeph"; "Zephaniah" ]);
("haggai", [ "Hag"; "Haggai" ]);
("zechariah", [ "Zech"; "Zechariah" ]);
("maccabees_1", [ "1 Macc"; "1 Maccabees" ]);
("maccabees_2", [ "2 Macc"; "2 Maccabees" ]);
("philemon", [ "Phlm"; "Philemon" ]);
("jude", [ "Jude" ]);
(* [john_2]/[john_3], siblings of [john_1] above -- three separate
Johannine epistles, three separate ids, the same one-id-per-physical-
book rule every other entry in this file follows. *)
("john_2", [ "2 John" ]);
("john_3", [ "3 John" ]) ]
(* Targets a tradition can map ONTO that the Vulgate data never cites
directly. Present so [tradition_of_fields] can validate both sides.
[sirach] and [revelation] exist ONLY here, never as an [of_token] result:
"Sir" and "Rev" already resolve to the Vulgate ids [ecclesiasticus] and
[apocalypse] above, so a modern-numbering tradition maps ONTO these
targets rather than data ever citing them directly. *)
(* The modern-numbering targets. They have table rows so that colitur's OWN
rendered output re-parses: with --sigla-tradition modern the book prints
as "1 Reg" or "1 Kings", and a user copying that back into an overlay must
get it read correctly. The shipped data never cites them directly, which
is why they are listed apart. *)
let tradition_target_table =
[
("kings_1", [ "1 Reg"; "Liber Regum I"; "1 Kgs"; "1 Kings" ]);
("kings_2", [ "2 Reg"; "Liber Regum II"; "2 Kgs"; "2 Kings" ]);
("nehemiah", [ "Neh"; "Liber Nehemiae"; "Nehemiah" ]);
("sirach", [ "Liber Ecclesiastici"; "Sirach" ]);
("hosea", [ "Os"; "Hos"; "Hosea" ]);
("jonah", [ "Ion"; "Jon"; "Jonah" ]);
("revelation", [ "Apocalypsis"; "Revelation" ]);
]
let tradition_targets = List.map fst tradition_target_table
(* Deliberately NOT [of_lectionary_table] -- see that table's own header for
why: those ids have no la.ini-verified name, so they stay out of the set
{!Colitur_naming.Lang}-completeness checks (test_lang_coverage.ml) run
against, without weakening what those checks still guarantee for every id
here. *)
let all = List.map fst table @ tradition_targets
let tokens =
List.concat_map
(fun (id, sp) -> List.map (fun s -> (s, id)) sp)
(table @ tradition_target_table @ of_lectionary_table)
let default_spelling id =
match List.assoc_opt id (table @ tradition_target_table @ of_lectionary_table) with
| Some (first :: _) -> first
| Some [] | None -> id
let of_token s =
let s = String.trim s in
List.assoc_opt s tokens
type tradition = (string * string) list
let vulgate = []
let known id = List.mem id all
let tradition_of_fields fields =
List.filter (fun (a, b) -> known a && known b) fields
let unknown_fields fields =
List.filter_map
(fun (a, b) -> if known a && known b then None else Some a)
fields
let map tr id = match List.assoc_opt id tr with Some x -> x | None -> id
(* Fix wave I2: books with exactly one chapter are conventionally cited by
VERSE ALONE, no chapter number at all ("Jude 17, 20b-25", "Phlm 7-22") --
found while adding [of_lectionary_table] above: [jude]'s own citation
"Jude 17,20b-25" used to fail as "unknown book" (safe, if unconverted);
registering [jude] made it PARSE, but WRONGLY -- [Parse]'s existing
"a leading comma-piece that looks like a number introduces a chapter"
rule (parse.ml's own [parse_part], written for real multi-chapter data
like "15, 1-46") misread "17" as chapter 17, which does not exist in a
25-verse, one-chapter book. A parse that SUCCEEDS with the wrong
structure is worse than one that fails: nothing downstream would ever
suspect it. [Parse.parse] consults this to skip that rule entirely for
these four ids and default to chapter 1. *)
let single_chapter_ids = [ "jude"; "philemon"; "john_2"; "john_3" ]
let is_single_chapter id = List.mem id single_chapter_ids
|