aboutsummaryrefslogtreecommitdiff
path: root/lib/citation/book.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 13:30:02 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 13:30:02 +0200
commit9cc763c169a6ce4ebbdbed506c5eb2dad1f9f3c1 (patch)
treeb38d6e418c7de49766478a47f5654086157e66e0 /lib/citation/book.ml
parent1a3046e2a3793fe52ec518818b19a6f15aadbb75 (diff)
downloadcolitur-9cc763c169a6ce4ebbdbed506c5eb2dad1f9f3c1.tar.gz
colitur-9cc763c169a6ce4ebbdbed506c5eb2dad1f9f3c1.zip
fix(citation): recognise English-canonical OF books, verse sub-letters
35% of OF citation fields (259/730 on colitur readings --rite of 2026) printed unconverted -- 1 John renders on 2 January but not 3 January. Two independent causes, both in the citation/siglum path, neither in the OF data itself: 1. Book.table only ever surveyed the three EF citation-bearing files, so 345 references citing a book no EF file happens to use (Job, Ruth, Judges, 1/2 Samuel, 1/2 Chronicles, 1/2 Maccabees, Baruch, Ecclesiastes, Habakkuk, Haggai, Nahum, Zechariah, Zephaniah, Deuteronomy, Amos, Micah, Lamentations, Ezra, Joshua, 2/3 John, Jude, Philemon, plus "Isaiah"/"Jeremiah"/"Ezekiel"/"Malachi"/"Mat"/ "The Acts"/"Tobit"/"Song of Solomon" spelling variants of books EF already knows) failed as "unknown book". Added as a new, separate of_lectionary_table rather than folded into the EF-surveyed table: none of these 25 new books is attested in the EF's own scans, and lang/la.ini's own header refuses to fabricate an uncited Latin title, so they are resolvable (parse + render, falling back to their own English spelling) but deliberately excluded from Book.all -- test_lang_coverage.ml's la.ini-completeness promise is preserved exactly for the ids it already covered, not silently weakened. 2. Parse's grammar could not read a verse number carrying a lectionary sub-verse letter ("11a", "1bcde") at all -- the dominant remaining failure shape once (1) was fixed. verse_range now carries a verse_num { n; suffix } on each boundary, PRESERVED through rendering rather than dropped (dropping would silently lose real precision the source text carries). Chapter numbers are untouched (nothing in the data ever attaches a letter to one). A third, subtler bug surfaced by (1): registering "jude"/"philemon"/ "2 John"/"3 John" exposed Parse's existing "leading comma-number is a chapter" heuristic misreading a single-chapter book's bare verse list ("Jude 17,20b-25") as chapter 17 -- a wrong PARSE, worse than the previous safe "unknown book" failure. Book.is_single_chapter now tells Parse to skip that heuristic for the four one-chapter books and default to chapter 1. Residual, honestly enumerated rather than forced to zero: 41 distinct references (of 1540) are hyphenated ranges crossing a chapter boundary ("2:29-3:6") -- a Parse.t shape verse_range/part do not represent, a type restructuring deliberately not attempted this task. Pinned exactly by the new test_citation_coverage_of.ml, both directions (a new failure or one of these 41 starting to convert both go red), and disclosed in data/of/lectionary.sexp's own regenerated provenance header (tools/ bootstrap_lectionary_of.ml now runs the same parser at generation time and names the count and the set). Verified EF-unaffected: git diff v1.0.0..HEAD -- lib/kernel/ lib/rites/rite_ef/ data/ef/ is empty, and `colitur day`/`readings` output for 2027 is byte-identical against the pre-fix binary.
Diffstat (limited to 'lib/citation/book.ml')
-rw-r--r--lib/citation/book.ml107
1 files changed, 97 insertions, 10 deletions
diff --git a/lib/citation/book.ml b/lib/citation/book.ml
index 986a898..b43cdd8 100644
--- a/lib/citation/book.ml
+++ b/lib/citation/book.ml
@@ -19,26 +19,26 @@ let table =
("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", [ "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_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" ]);
- ("jeremiah", [ "Jer"; "Ier"; "Ieremias Propheta"; "Jeremias" ]);
- ("ezekiel", [ "Ezech"; "Ezek"; "Ezechiel Propheta"; "Ezechiel" ]);
+ ("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" ]);
- ("matthew", [ "Matt"; "Matt."; "Matth"; "Evangelium secundum Matthaeum"; "Matthew" ]);
+ ("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" ]);
+ ("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" ]);
@@ -64,6 +64,73 @@ let table =
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:
@@ -89,15 +156,20 @@ let tradition_target_table =
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)
+ (table @ tradition_target_table @ of_lectionary_table)
let default_spelling id =
- match List.assoc_opt id (table @ tradition_target_table) with
+ match List.assoc_opt id (table @ tradition_target_table @ of_lectionary_table) with
| Some (first :: _) -> first
| Some [] | None -> id
@@ -120,3 +192,18 @@ let unknown_fields fields =
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