From 263d0d889073d214229aef9655d3db29540c0d12 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 20 Aug 2026 16:05:34 +0200 Subject: fix(citation): complete the modern tradition, and test the shipped file Apocalypse -> Revelation was missing from [modern] while `revelation` was already a declared tradition target, so the target existed with nothing mapping onto it. Same Vulgate-to-modern shape as Ecclesiasticus -> Sirach. The injectivity test used to hand-copy the mapping table, which asserted things about the copy and nothing about the artifact: a mapping added to traditions.ini and forgotten in the test would have passed. It now reads lang/traditions.ini and additionally asserts that [vulgate] is empty -- an entry there would silently renumber the DEFAULT, the one thing this design promises never happens -- and that no declared target is left unreachable. Mutation-proved three ways: declaring an unused target, deleting a mapping, and adding an entry to [vulgate] each redden it, the first two naming the offending book. --- lang/traditions.ini | 1 + test/test_citation.ml | 44 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lang/traditions.ini b/lang/traditions.ini index e2d53ab..46f6ee7 100644 --- a/lang/traditions.ini +++ b/lang/traditions.ini @@ -19,3 +19,4 @@ esdras_2 = nehemiah ecclesiasticus = sirach osee = hosea jonas = jonah +apocalypse = revelation diff --git a/test/test_citation.ml b/test/test_citation.ml index d9be86e..1efaa10 100644 --- a/test/test_citation.ml +++ b/test/test_citation.ml @@ -180,13 +180,49 @@ let test_every_data_file_token_resolves () = always a typo -- it would silently merge two books -- so this asserts the mapping is injective, not merely that every id is known. *) let test_modern_tradition_is_injective () = - let fields = - [ ("kings_3", "kings_1"); ("kings_4", "kings_2"); - ("esdras_2", "nehemiah"); ("ecclesiasticus", "sirach"); - ("osee", "hosea"); ("jonas", "jonah") ] + (* READ the shipped file. An earlier version of this test hand-copied the + table, which asserted things about the copy and nothing at all about the + artifact -- a mapping added to traditions.ini and forgotten here would + have passed. *) + let text = + let ic = open_in_bin "../lang/traditions.ini" in + let s = really_input_string ic (in_channel_length ic) in + close_in ic; s in + let sections = + match Colitur_kernel.Overlay_ini.parse_sections text with + | Ok ss -> ss + | Error e -> Alcotest.failf "lang/traditions.ini: %s" e + in + let section name = + List.filter_map + (fun (sc : Colitur_kernel.Overlay_ini.section) -> + if sc.Colitur_kernel.Overlay_ini.name = name then + Some sc.Colitur_kernel.Overlay_ini.fields + else None) + sections + |> List.concat + in + (* [vulgate] must be the identity: an entry there would silently renumber + the DEFAULT, which is the one thing this design promises never happens. *) + Alcotest.(check (list (pair string string))) "vulgate is identity" [] + (section "vulgate"); + let fields = section "modern" in + Alcotest.(check bool) "modern is not empty" true (fields <> []); Alcotest.(check (list string)) "all ids known" [] (B.unknown_fields fields); + (* every declared tradition target is actually reachable -- an unused target + is either a forgotten mapping or dead weight *) let targets = List.map snd fields in + (* [B.tokens] is (spelling, id) pairs, so membership of an ID is a lookup + over the VALUES, not [mem_assoc] over the keys. Getting that backwards + flags every ordinary book as an unused target. *) + let cited = List.map (fun (_, i) -> B.to_string i) B.tokens in + List.iter + (fun id -> + let n = B.to_string id in + if (not (List.mem n cited)) && not (List.mem n targets) then + Alcotest.failf "%s is a declared target no tradition maps onto" n) + B.all; let uniq = List.sort_uniq compare targets in Alcotest.(check int) "no two ids share a target" (List.length targets) (List.length uniq) -- cgit v1.3