aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 13:08:55 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 13:08:55 +0200
commit1a3046e2a3793fe52ec518818b19a6f15aadbb75 (patch)
tree83bb4bcc6d646b74093d18324076c32e2fede74b
parent37a3d4ee06717c24ad07db2bc365cad587ad6d5f (diff)
downloadcolitur-1a3046e2a3793fe52ec518818b19a6f15aadbb75.tar.gz
colitur-1a3046e2a3793fe52ec518818b19a6f15aadbb75.zip
fix(of): print the OF's own transcribed Latin/English names, not EF's
day_line_of/readings_line_of resolved every displayed name through lang/la.ini -- data documented as "transcribed from the 1962 Missale Romanum", i.e. EF vocabulary, keyed on slug. 18 of 222 shipped OF sanctoral slugs collide with an EF slug there and all 18 printed the WRONG (1962) title (e.g. "S. Marthae Virg.", a rank word the 1969 reform abolished, silently overriding amendment 011's own "Ss. Marthae, Mariae et Lazari"); the other 204 had no entry and degraded to a bare slug, so none of the 208 titles calendar-2002.sexp itself transcribes (cited against the 2002 Missal) was ever reachable. Add observed_name_of: prefer the observed celebration's own Celebration.names (looked up by the requested language code), fall back to the lang/*.ini table only on a miss. Under --raw this is a no-op (Lang.raw's own code, "raw", is not a valid 2-letter ISO code, so the names lookup always misses and falls through to the identity table exactly as before). EF's day_line/readings_line are untouched -- EF's Celebration.names is almost always empty by design, lang/la.ini is its real, independently-cited name table. Regression net, two layers: test_lang_coverage.ml gains a test walking the real assembled OF layer's entries, asserting that every genuine lang/la.ini collision resolves to the OF's own name, never the EF table's; test/cli.t pins one real collision (Martha, 29 July) through the actual CLI in both la and en.
-rw-r--r--bin/main.ml41
-rw-r--r--test/cli.t12
-rw-r--r--test/test_lang_coverage.ml111
3 files changed, 160 insertions, 4 deletions
diff --git a/bin/main.ml b/bin/main.ml
index 6827fbc..eab41d8 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -664,10 +664,45 @@ let readings_report ~lang ~sigla ~overlays y =
resolved_year_report ~line:(readings_line ~lang ~sigla) ~overlays y
let rubrics_report ~overlays y = resolved_year_report ~line:rubrics_line ~overlays y
+(* Fix wave I1 (final-review.md, 2026-08-25-colitur-of-phases-3-5): unlike
+ EF's [Celebration.names] (almost always empty -- lang/la.ini is EF's own
+ independently-cited Latin name table, keyed on slug, and that is
+ deliberate: see day_line's own header), the OF's data/of/calendar-2002
+ .sexp carries a verified [names] entry (both [la] and [en]) for every one
+ of its 208 sanctoral slugs, transcribed and cited against the 2002
+ Missal. [day_line]/[readings_line]'s original OF twins nonetheless
+ resolved names through the SAME [lang/*.ini] table EF uses -- 18 of 222
+ OF slugs collide with an EF slug there and printed the WRONG (1962)
+ title (e.g. "S. Marthae Virg." instead of amendment 011's own "Ss.
+ Marthae, Mariae et Lazari"); the other 204 had no entry and degraded to
+ a bare slug, so calendar-2002.sexp's own transcription was reachable
+ from nothing.
+
+ [observed_name_of] fixes this at the source: prefer the OBSERVED
+ CELEBRATION'S OWN [names] (keyed by the requested language code), and
+ fall back to the lang/*.ini table only on a miss -- which still covers
+ every slug the calendar data has no name for (the temporal-origin ones,
+ of-pentecost/of-advent-sunday-4/etc, correctly absent from both tables).
+ Under [--raw], [Colitur_naming.Lang.raw]'s own [code] is ["raw"], not a
+ valid 2-letter ISO-639-1 code, so {!Colitur_kernel.Lang.of_string}
+ rejects it, [from_data] is always [None], and this always falls through
+ to the lang-table lookup -- [Lang.raw]'s own identity table -- so [--raw]
+ output is byte-identical to before this fix. *)
+let observed_name_of ~lang (cel : Rite_of.Vocab_of.rank Colitur_kernel.Celebration.t) slug_s =
+ let from_data =
+ match Colitur_kernel.Lang.of_string (Colitur_naming.Lang.code lang) with
+ | Error _ -> None
+ | Ok l -> Colitur_kernel.Names.find cel.Colitur_kernel.Celebration.names l
+ in
+ match from_data with Some n -> n | None -> Colitur_naming.Lang.celebration lang slug_s
+
(* Task 5 (2026-08-25-colitur-of-phases-3-5): [day_line]/[readings_line]'s
OF twins -- same two row shapes, same [~lang]/[~sigla] append-only rules
(see those functions' own citations just above for the full reasoning,
- not repeated here), [Rite_of.Vocab_of] in place of [Rite_ef.Vocab_ef]. *)
+ not repeated here), [Rite_of.Vocab_of] in place of [Rite_ef.Vocab_ef].
+ Names resolve through [observed_name_of] above, not directly through
+ [Colitur_naming.Lang.celebration] as EF's twins do -- see that
+ function's own header for why the two rites differ here. *)
let day_line_of ~lang (d : (Rite_of.Vocab_of.season, Rite_of.Vocab_of.rank) Colitur_kernel.Liturgical_day.t) =
let t = d.Colitur_kernel.Liturgical_day.temporal in
let cel = d.Colitur_kernel.Liturgical_day.observed in
@@ -679,7 +714,7 @@ let day_line_of ~lang (d : (Rite_of.Vocab_of.season, Rite_of.Vocab_of.rank) Coli
String.concat "" (List.map commemoration_suffix d.Colitur_kernel.Liturgical_day.commemorations)
in
let slug_s = Colitur_kernel.Slug.to_string cel.Colitur_kernel.Celebration.slug in
- let name = Colitur_naming.Lang.celebration lang slug_s in
+ let name = observed_name_of ~lang cel slug_s in
let name_suffix = if name = slug_s then "" else " " ^ name in
Printf.printf "%s %s %s %s %s %s %s%s%s\n" (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date)
(D.weekday_to_string t.Colitur_kernel.Temporal.weekday)
@@ -702,7 +737,7 @@ let readings_line_of ~lang ~sigla
| None -> "-"
in
let slug_s = Colitur_kernel.Slug.to_string cel.Colitur_kernel.Celebration.slug in
- let name = Colitur_naming.Lang.celebration lang slug_s in
+ let name = observed_name_of ~lang cel slug_s in
let name_suffix = if name = slug_s then "" else " | " ^ name in
Printf.printf "%s %s | %s | %s%s\n"
(D.to_iso8601 d.Colitur_kernel.Liturgical_day.date)
diff --git a/test/cli.t b/test/cli.t
index 8671694..86291a6 100644
--- a/test/cli.t
+++ b/test/cli.t
@@ -232,6 +232,18 @@ The OF rite resolves too, same two commands, its own vocabulary:
$ colitur readings --rite of 2026 | head -1
2026-01-01 of-mary-mother-of-god | Num 6:22-27 | Luc 2:16-21
+Fix wave I1 (final-review.md): a sanctoral-origin OF day names through its
+OWN transcribed title (calendar-2002.sexp, amended by 011's own rename),
+never through lang/la.ini's EF (1962) table -- 29 July, Martha, is one of
+the 18 real slug collisions between the two vocabularies the review found;
+la.ini's own entry for "martha" is the pre-2021 EF title "S. Marthae Virg.",
+and printing it here would be exactly the regression this guards.
+
+ $ colitur day --rite of 2026 | grep '^2026-07-29 '
+ 2026-07-29 wednesday ordinary-time 17 martha memoria-obligatoria white Ss. Marthae, Mariae et Lazari
+ $ colitur day --rite of 2026 --lang en | grep '^2026-07-29 '
+ 2026-07-29 wednesday ordinary-time 17 martha memoria-obligatoria white Saints Martha, Mary and Lazarus
+
An unrecognised --rite value is a usage error, not a silent fallback to ef
(exit 2):
diff --git a/test/test_lang_coverage.ml b/test/test_lang_coverage.ml
index d41f7e7..01aa9b7 100644
--- a/test/test_lang_coverage.ml
+++ b/test/test_lang_coverage.ml
@@ -295,6 +295,113 @@ let test_en_falls_back_to_latin () =
Alcotest.(check bool) "empty table falls back to la.ini for a real slug" true
(L.celebration chained "hilary" <> "hilary")
+(* ------------------------------------------------------------------ *)
+(* Fix wave I1 (final-review.md, 2026-08-25-colitur-of-phases-3-5): *)
+(* --rite of used to resolve every displayed name through lang/la.ini, *)
+(* EF's own independently-cited 1962-vocabulary table, keyed on slug. *)
+(* 18 of 222 shipped OF sanctoral slugs happen to collide with an EF *)
+(* slug there and all 18 printed the WRONG (1962) title -- e.g. *)
+(* "S. Marthae Virg." (a title the 1969 reform deleted, and which also *)
+(* silently overrode amendment 011's own rename) instead of *)
+(* calendar-2002.sexp's own "Ss. Marthae, Mariae et Lazari". The other *)
+(* 204 had no entry there at all and degraded to a bare slug, so none *)
+(* of the 208 hand-verified titles Task 1 transcribed (cited against *)
+(* the 2002 Missal) was ever reachable. *)
+(* *)
+(* bin/main.ml's [observed_name_of] now prefers the observed *)
+(* celebration's OWN {!Colitur_kernel.Celebration.names}, falling back *)
+(* to the lang table only on a miss. That priority order is duplicated *)
+(* here -- bin/ and test/ are separate dune stanzas that do not share *)
+(* code, the same reasoning test_support.ml's own header gives for why *)
+(* it duplicates bin/main.ml's EF loaders rather than requiring them -- *)
+(* and checked against every entry the REAL, assembled OF layer *)
+(* actually carries (calendar-2002.sexp + all 13 amendments), not a *)
+(* synthetic fixture. Walking {!Colitur_kernel.Layer.t.entries} directly *)
+(* rather than a resolved calendar year: several of the 18 real *)
+(* collisions (e.g. Margaret of Scotland, 16 November) never win the *)
+(* alphabetical tie-break against a same-day rival in ANY year and so *)
+(* are never the OBSERVED day at all -- the collision in the DATA is *)
+(* real and load-bearing regardless (a future tie-break or Calendar *)
+(* change could make her win one), so this test does not depend on *)
+(* which slugs happen to be observed in any particular year range. *)
+let of_base_path = "../data/of/calendar-2002.sexp"
+let of_amendments_dir = "../data/of/amendments/"
+let of_amendment_files =
+ [ "001-padre-pio.sexp"; "002-juan-diego-cuauhtlatoatzin.sexp"; "003-our-lady-of-guadalupe.sexp";
+ "004-john-xxiii-john-paul-ii.sexp"; "005-mary-magdalene-rank.sexp"; "006-mary-mother-of-the-church.sexp";
+ "007-paul-vi.sexp"; "008-our-lady-of-loreto.sexp"; "009-faustina-kowalska.sexp";
+ "010-narek-avila-hildegard.sexp"; "011-martha-mary-lazarus.sexp"; "012-teresa-of-calcutta.sexp";
+ "013-john-henry-newman.sexp" ]
+
+let of_layer () =
+ match Colitur_kernel.Layer.load Rite_of.Vocab_of.rank_of_sexp of_base_path with
+ | Error e -> Alcotest.failf "%s: %s" of_base_path e
+ | Ok base ->
+ let overlays =
+ List.map
+ (fun name ->
+ let path = of_amendments_dir ^ name in
+ match Colitur_kernel.Overlay.load Rite_of.Vocab_of.rank_of_sexp path with
+ | Ok o -> o
+ | Error e -> Alcotest.failf "%s: %s" path e)
+ of_amendment_files
+ in
+ let layer, diagnostics = Colitur_kernel.Overlay.merge base overlays in
+ if diagnostics <> [] then Alcotest.failf "unexpected OF amendment diagnostics on the shipped data";
+ layer
+
+let la_lang = Colitur_kernel.Lang.of_string_exn "la"
+
+(* [observed_name_of]'s own resolution order (bin/main.ml), duplicated
+ rather than called -- see this section's own header. *)
+let resolve_like_cli t (cel : Rite_of.Vocab_of.rank Colitur_kernel.Celebration.t) slug_s =
+ match Colitur_kernel.Names.find cel.Colitur_kernel.Celebration.names la_lang with
+ | Some n -> n
+ | None -> L.celebration t slug_s
+
+let test_of_names_win_over_ef_lang_table () =
+ let t = la () in
+ let layer = of_layer () in
+ let collisions = ref [] in
+ List.iter
+ (fun (e : _ Colitur_kernel.Layer.entry) ->
+ let cel = e.Colitur_kernel.Layer.cel in
+ let slug_s = Colitur_kernel.Slug.to_string cel.Colitur_kernel.Celebration.slug in
+ (* A miss returns the key itself (lang.mli's own TOTAL-lookup
+ contract): [L.celebration t slug_s <> slug_s] means la.ini really
+ does carry an entry for this OF slug -- a genuine collision, not
+ a hypothetical one. *)
+ if L.celebration t slug_s <> slug_s then collisions := slug_s :: !collisions)
+ layer.Colitur_kernel.Layer.entries;
+ (* Not asserting the collision count -- two independently-authored
+ vocabularies sharing a slug is expected (the whole reason this fix
+ exists) and not itself a defect; what matters, and what regresses
+ silently if [observed_name_of]'s priority order is ever reverted, is
+ what happens on EVERY one of them: *)
+ if !collisions = [] then
+ Alcotest.fail
+ "expected at least one real OF/EF slug collision (e.g. \"martha\") -- if this is now empty, \
+ either lang/la.ini or calendar-2002.sexp changed and this test's own premise needs re-checking, \
+ not silently passing on zero cases";
+ List.iter
+ (fun slug_s ->
+ match Colitur_kernel.Layer.find layer (Colitur_kernel.Slug.of_string_exn slug_s) with
+ | None -> Alcotest.failf "%s: found in entries but not via Layer.find" slug_s
+ | Some (e : _ Colitur_kernel.Layer.entry) -> (
+ let cel = e.Colitur_kernel.Layer.cel in
+ match Colitur_kernel.Names.find cel.Colitur_kernel.Celebration.names la_lang with
+ | None ->
+ Alcotest.failf
+ "%s: collides with lang/la.ini and calendar-2002.sexp carries NO own `la` name for it \
+ -- would silently print EF's 1962 title (%s)"
+ slug_s (L.celebration t slug_s)
+ | Some own_name ->
+ Alcotest.(check string)
+ (Printf.sprintf "%s: resolves to the OF's own transcribed name, not lang/la.ini's EF one"
+ slug_s)
+ own_name (resolve_like_cli t cel slug_s)))
+ !collisions
+
let suite =
( "Lang/coverage",
[ Alcotest.test_case "every slug has a Latin name" `Slow test_every_slug_has_a_latin_name;
@@ -307,4 +414,6 @@ let suite =
test_no_two_books_share_a_name;
Alcotest.test_case "shipped styles round-trip" `Quick
test_shipped_styles_round_trip;
- Alcotest.test_case "en.ini falls back to Latin" `Quick test_en_falls_back_to_latin ] )
+ Alcotest.test_case "en.ini falls back to Latin" `Quick test_en_falls_back_to_latin;
+ Alcotest.test_case "OF names win over lang/la.ini on every real slug collision" `Quick
+ test_of_names_win_over_ef_lang_table ] )