summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 10:11:47 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 10:11:47 +0200
commitb6d0eba04d1da51515dad70127112898b500c1bc (patch)
treea82263c3d436d94b6208b14c61295b0e550fbe93
parent8fa9db25f55f4ae92ad6a28115484dd981593a3c (diff)
downloadcolitur-b6d0eba04d1da51515dad70127112898b500c1bc.tar.gz
colitur-b6d0eba04d1da51515dad70127112898b500c1bc.zip
fix(of-lectionary): stop serving drifting readings for O-Antiphon and Christmas-season dates
The bootstrap excluded 19 lectio bases as "date-keyed duplicates", but 17 of them were not duplicates at all: the 8 O-Antiphon days (17-24 December) and 8 further Christmas-season dates (29-31 December, 2-5 and 7 January) each carry unique per-date content found nowhere else among the emitted entries, verified against lectio's own ini directly. 17 December is Gen 49:2,8-10 / Matthew 1:1-17, but colitur was falling through to that year's unrelated weekday-keyed "advent-3-mon" content instead. OLM n. 69.3 explains why: those ferias are fixed by civil date, unlike every other Advent/Christmastide/Paschaltide feria, which is merely non-alternating within a weekday slot. Temporal_of's own ferial slugs for these dates are weekday-keyed, so looking one of those up served whichever OTHER date happened to share that year's weekday alignment -- a citation that silently drifted year to year on ~16 real civil dates (6 January stays excluded: Temporal_of fixes Epiphany there unconditionally, so that date can never reach the ferial path at all; two more, easter-6-thu and advent-4-sat, stay excluded for their own, different structural reasons). Fixed with a date-keyed lookup route (Lectionary_of.date_keyed_slug), tried before the weekday-keyed one in readings' own step 3. Temporal_of's slugs are unchanged -- only which lectionary key resolves a day's citations. tools/bootstrap_lectionary_of.ml now maps the 17 bases to synthetic date-keyed slugs instead of excluding them, and its own reachability sweep calls date_keyed_slug directly so the two can never drift apart. Re-emitted data/of/lectionary.sexp (754 -> 770 entries) and corrected its provenance header, which previously described these dates as excluded duplicates. Also states, for the first time, a Minor finding from the same review: 190 of 222 shipped sanctoral slugs have no dedicated lectionary entry and correctly fall through to the ferial per OLM norms -- expected behaviour, not a gap, but never said in the coverage report before. New tests pin 17 December (Gen 49:2,8-10 / Matthew 1:1-17) and 2 January (1 John 2:22-28 / John 1:19-28) in two years with different weekday alignments, proving neither reading drifts, plus direct boundary coverage of date_keyed_slug itself (both windows, the 6 January exclusion, and the Sunday guard).
-rw-r--r--data/of/lectionary.sexp98
-rw-r--r--lib/rites/rite_of/lectionary_of.ml36
-rw-r--r--lib/rites/rite_of/lectionary_of.mli50
-rw-r--r--test/test_lectionary_of.ml119
-rw-r--r--tools/bootstrap_lectionary_of.ml228
5 files changed, 456 insertions, 75 deletions
diff --git a/data/of/lectionary.sexp b/data/of/lectionary.sexp
index 4f43f12..167a7c7 100644
--- a/data/of/lectionary.sexp
+++ b/data/of/lectionary.sexp
@@ -25,28 +25,56 @@
; a MISSING source file is fatal, checked before any read is attempted --
; see this generator's own [parse_ini]) and commit the diff instead.
; Every emitted key is asserted, at generation time, to be a slug
-; Rite_of.Temporal_of actually computes OR a slug the real merged
+; Rite_of.Temporal_of actually computes, OR a slug the real merged
; sanctoral layer (data/of/calendar-2002.sexp + all 13 decree overlays)
-; actually carries -- see [assert_reachable].
+; actually carries, OR a date Rite_of.Lectionary_of.date_keyed_slug
+; actually reaches -- see [assert_reachable].
+;
+; CORRECTED 2026-08-26 (review): this file used to exclude 16 lectio
+; bases -- the 8 O-Antiphon days (17-24 December) and 8 further
+; Christmas-season dates (29-31 December, 2-5 and 7 January) -- as
+; "date-keyed duplicates" of the weekday-keyed ferial families mapped
+; above. That was false: each carries content found nowhere else among
+; the emitted entries (17 December: Gen 49:2,8-10/Matt 1:1-17), which
+; OLM n. 69.3 explains -- these ferias are fixed by CIVIL DATE, not
+; merely non-alternating within a weekday slot like every other Advent/
+; Christmastide feria. Every day in 2005-2050 previously served a
+; DRIFTING citation there (whichever weekday-keyed family that year's
+; own alignment happened to land on) instead of the Missal's fixed one.
+; Fixed via a new date-keyed lookup route, tried BEFORE the weekday-
+; keyed one (Rite_of.Lectionary_of.date_keyed_slug, readings' own step
+; 3) -- Temporal_of's slugs are UNCHANGED, only which lectionary key
+; resolves the day's citations. 3 lectio bases remain excluded, for
+; three genuinely different structural-unreachability reasons, none of
+; them "duplicate" -- see [excluded_bases]'s own comment.
;
; Source: ../lectio/internal/caldata/of-lectionary.ini
; SHA-256: f0b527aef58b93e6ccdfbeafe52b340ed2a9d7d72805bf5083e3da04b080123c
-; 988 ini sections (988 expected) -> 438 resolved (base, cycle-shape) pairs -> 419 colitur slugs mapped, 19 ini bases deliberately excluded (date-
-; keyed duplicates/late-Advent, no colitur date-keyed lookup exists --
-; see [excluded_bases]), 0 ini bases genuinely unmapped (no pattern, no
-; override, no matching sanctoral slug) -> 754 emitted lectionary entries.
+; 988 ini sections (988 expected) -> 438 resolved (base, cycle-shape) pairs -> 435 colitur slugs mapped, 3 ini bases genuinely excluded (structurally
+; unreachable -- see [excluded_bases]), 0 ini bases genuinely unmapped
+; (no pattern, no override, no matching sanctoral slug) -> 770 emitted
+; lectionary entries.
;
; COVERAGE, BOTH DIRECTIONS (Step 1 of this task's own brief):
; (1) Temporal-day gap: of the 365 days of civil year 2026, 1 produce a
-; Rite_of.Temporal_of slug with NO entry in this file (tried flat,
-; then both the Sunday- and weekday-cycle letter suffixes -- the
-; same order Lectionary_of.readings itself tries). Those are days
-; with no readings via the temporal-slug path (a sanctoral proper
-; on the same civil day, where one exists, is Lectionary_of's own
+; Rite_of.Temporal_of slug with NO entry in this file (tried the
+; date-keyed route first, then flat, then both the Sunday- and
+; weekday-cycle letter suffixes -- the same order
+; Lectionary_of.readings itself tries). Those are days with no
+; readings via the temporal-slug path (a sanctoral proper on the
+; same civil day, where one exists, is Lectionary_of's own
; separate first chance). Named set below.
; (2) Unmapped lectio keys: 0 ini base names (of 988 sections, 438
; distinct bases) map to no colitur slug at all -- data being
; silently dropped if unreported. Named below.
+; (3) Sanctoral coverage (Minor, 2026-08-26 review): of the 222 shipped
+; sanctoral slugs (data/of/calendar-2002.sexp + all 13 decree
+; overlays), 32 have a DEDICATED entry in this file (a lectio base
+; name this generator recognised verbatim as one of them). The
+; other 190 have none and correctly fall through to the day's own
+; ferial (Lectionary_of.readings' own step 2 -> step 3) -- this is
+; OLM norms working as designed for a saint with no proper of
+; their own, not a gap in this data.
; Regenerate with:
; eval $(opam env) && dune exec tools/bootstrap_lectionary_of.exe -- ../lectio/internal/caldata/of-lectionary.ini data/of/lectionary.sexp
; Unmapped lectio bases (0):
@@ -203,6 +231,30 @@
(of-advent-4-wednesday-ii
(((part First) (reference "1 Samuel 1:24-28"))
((part Gospel) (reference "Luke 1:46-56"))))
+ (of-advent-dec-17
+ (((part First) (reference "Genesis 49:2,8-10"))
+ ((part Gospel) (reference "Matthew 1:1-17"))))
+ (of-advent-dec-18
+ (((part First) (reference "Jeremiah 23:5-8"))
+ ((part Gospel) (reference "Matthew 1:18-24"))))
+ (of-advent-dec-19
+ (((part First) (reference "Judges 13:2-7,24-25a"))
+ ((part Gospel) (reference "Luke 1:5-25"))))
+ (of-advent-dec-20
+ (((part First) (reference "Isaiah 7:10-14"))
+ ((part Gospel) (reference "Luke 1:26-38"))))
+ (of-advent-dec-21
+ (((part First) (reference "Song of Solomon 2:8-14"))
+ ((part Gospel) (reference "Luke 1:39-45"))))
+ (of-advent-dec-22
+ (((part First) (reference "1 Samuel 1:24-28"))
+ ((part Gospel) (reference "Luke 1:46-56"))))
+ (of-advent-dec-23
+ (((part First) (reference "Malachi 3:1-4;4:5-6"))
+ ((part Gospel) (reference "Luke 1:57-66"))))
+ (of-advent-dec-24
+ (((part First) (reference "2 Samuel 7:1-5,8b-12,14a,16"))
+ ((part Gospel) (reference "Luke 1:67-79"))))
(of-advent-sunday-1-a
(((part First) (reference "Isaiah 2:1-5"))
((part Gospel) (reference "Matthew 24:37-44"))))
@@ -356,6 +408,30 @@
(of-christmas-2-wednesday
(((part First) (reference "1 John 4:11-18"))
((part Gospel) (reference "Mark 6:45-52"))))
+ (of-christmas-dec-29
+ (((part First) (reference "1 John 2:3-11"))
+ ((part Gospel) (reference "Luke 2:22-35"))))
+ (of-christmas-dec-30
+ (((part First) (reference "1 John 2:12-17"))
+ ((part Gospel) (reference "Luke 2:36-40"))))
+ (of-christmas-dec-31
+ (((part First) (reference "1 John 2:18-21"))
+ ((part Gospel) (reference "John 1:1-18"))))
+ (of-christmas-jan-2
+ (((part First) (reference "1 John 2:22-28"))
+ ((part Gospel) (reference "John 1:19-28"))))
+ (of-christmas-jan-3
+ (((part First) (reference "1 John 2:29-3:6"))
+ ((part Gospel) (reference "John 1:29-34"))))
+ (of-christmas-jan-4
+ (((part First) (reference "1 John 3:7-10"))
+ ((part Gospel) (reference "John 1:35-42"))))
+ (of-christmas-jan-5
+ (((part First) (reference "1 John 3:11-21"))
+ ((part Gospel) (reference "John 1:43-51"))))
+ (of-christmas-jan-7
+ (((part First) (reference "1 John 5:14-21"))
+ ((part Gospel) (reference "John 2:1-11"))))
(of-christmas-sunday-2
(((part First) (reference "Sirach 24:1-4,12-16"))
((part Gospel) (reference "John 1:1-18"))))
diff --git a/lib/rites/rite_of/lectionary_of.ml b/lib/rites/rite_of/lectionary_of.ml
index 1664fad..eb3e4e0 100644
--- a/lib/rites/rite_of/lectionary_of.ml
+++ b/lib/rites/rite_of/lectionary_of.ml
@@ -60,6 +60,24 @@ let lookup_any lectionary ~year_start base date =
| Some cs -> Some cs
| None -> Lectionary.find lectionary (with_suffix (weekday_cycle_letter (weekday_cycle ~year_start date))))
+(* See lectionary_of.mli's own doc comment for the full citation (OLM n.
+ 69.3) and argument. Kept as ordinary [Printf.sprintf] string
+ construction, not a lookup table, so tools/bootstrap_lectionary_of.ml's
+ own reachability sweep (which calls this function directly, not a
+ hand-copied re-implementation) and this module's own runtime lookup can
+ never drift apart -- the single failure mode a hand-duplicated version
+ in each place would invite. *)
+let date_keyed_slug date =
+ if Date.weekday date = Date.Sun then None
+ else
+ let m = Date.month date and dd = Date.day date in
+ if m = 12 && dd >= 17 && dd <= 24 then Some (Slug.of_string_exn (Printf.sprintf "of-advent-dec-%d" dd))
+ else if m = 12 && dd >= 29 && dd <= 31 then
+ Some (Slug.of_string_exn (Printf.sprintf "of-christmas-dec-%d" dd))
+ else if m = 1 && ((dd >= 2 && dd <= 5) || dd = 7) then
+ Some (Slug.of_string_exn (Printf.sprintf "of-christmas-jan-%d" dd))
+ else None
+
let readings ~lectionary ~year_start ~(observed : Vocab_of.rank Celebration.t)
~(temporal : (Vocab_of.season, Vocab_of.rank) Temporal.t) ~date ~temporal_at:_ =
match observed.Celebration.citations with
@@ -99,9 +117,23 @@ let readings ~lectionary ~year_start ~(observed : Vocab_of.rank Celebration.t)
Mass_formulary.source was never built to carry. *)
(Some { Mass_formulary.said = Some observed.Celebration.slug; via = Mass_formulary.Proper }, cs)
| None -> (
- match lookup_any lectionary ~year_start temporal.Temporal.office.Celebration.slug date with
+ (* OLM n. 69.3's own date-fixed windows (see date_keyed_slug's
+ own doc comment): tried BEFORE the ordinary weekday-keyed
+ slug lookup, not instead of it, so every other temporal day
+ is unaffected -- this branch only ever hits inside the two
+ named civil-date windows, and [date_keyed_slug] itself
+ already refuses a Sunday, where a NAMED Sunday office (never
+ one of these weekday-keyed ferial slugs) is what temporal.
+ office actually names. *)
+ match (match date_keyed_slug date with Some s -> Lectionary.find lectionary s | None -> None) with
| Some cs ->
( Some { Mass_formulary.said = Some temporal.Temporal.office.Celebration.slug;
via = Mass_formulary.Own_slug },
cs )
- | None -> (None, [])))
+ | None -> (
+ match lookup_any lectionary ~year_start temporal.Temporal.office.Celebration.slug date with
+ | Some cs ->
+ ( Some { Mass_formulary.said = Some temporal.Temporal.office.Celebration.slug;
+ via = Mass_formulary.Own_slug },
+ cs )
+ | None -> (None, []))))
diff --git a/lib/rites/rite_of/lectionary_of.mli b/lib/rites/rite_of/lectionary_of.mli
index 0c4d1d6..4a447d2 100644
--- a/lib/rites/rite_of/lectionary_of.mli
+++ b/lib/rites/rite_of/lectionary_of.mli
@@ -110,8 +110,13 @@ val weekday_cycle : year_start:(int -> Date.t) -> Date.t -> weekday_cycle
closed kernel type outside this task's own scope to extend; see
lectionary_of.ml's own implementation comment on this branch for the
full argument. [said] the observed slug.
- - {b Step 3} -- the day's own temporal slug, same three-way lookup.
- {!Colitur_kernel.Mass_formulary.Own_slug}, [said] that slug.
+ - {b Step 3} -- the day's own temporal slug. FIRST tries
+ {!date_keyed_slug} (OLM n. 69.3's date-fixed windows -- the
+ O-Antiphons and the pre-Epiphany run of the Christmas season), THEN
+ falls back to the same three-way slug lookup step 2 uses.
+ {!Colitur_kernel.Mass_formulary.Own_slug} either way, [said] the
+ temporal office's own slug -- {!date_keyed_slug} changes which KEY
+ resolves the citations, not which office is said to have said them.
- A day matching neither gets [(None, [])] -- see data/of/lectionary
.sexp's own provenance header for exactly which days that is, both
directions, measured, not guessed.
@@ -134,3 +139,44 @@ val readings :
date:Date.t ->
temporal_at:(Date.t -> (Vocab_of.season, Vocab_of.rank) Temporal.t) ->
Mass_formulary.t option * Citation.t list
+
+(** OLM 1981 Praenotanda n. 69.3 (the same passage {!weekday_cycle}'s own
+ doc comment cites: "Pro feriis Adventus, temporis Nativitatis et
+ paschalis cyclus eodem modo annualis est: lectiones ideo non
+ mutantur" -- Advent/Christmastide/Paschaltide ferial readings do not
+ change year to year). Every OTHER such family is non-alternating
+ WITHIN a weekday slot ({!weekday_cycle}); two short windows go
+ further and are keyed by CIVIL DATE instead: the O-Antiphon days
+ (17-24 December) and the pre-Epiphany run of the Christmas season
+ (29-31 December, then 2-5 and 7 January) -- verified concretely
+ against lectio's own ini, not merely inferred from the citation: 17
+ December is Gen 49:2,8-10 / Ps 72:1-2,3-4ab,7-8,17 / Matthew 1:1-17
+ regardless which weekday it falls on in a given year.
+ {!Rite_of.Temporal_of}'s own ferial slugs for these same civil dates
+ are WEEKDAY-keyed
+ ({!Rite_of.Temporal_of.christmas_feria_slug}/the Advent ferial
+ branch), so looking one of THOSE slugs up would silently serve
+ whichever OTHER date happens to share that year's weekday alignment
+ -- the defect this function exists to close (bug found by review, not
+ by a test: colitur used to serve 17 December the unrelated
+ "advent-3-mon" reading in years where the two happened to align).
+
+ 6 January is deliberately NOT one of the six Christmas-season dates,
+ despite lectio's own "christmas-jan-6" base existing:
+ {!Rite_of.Temporal_of}'s own [named] fixes Epiphany to 6 January
+ unconditionally (temporal_of.ml, "m = 1 && dd = 6"), so that date is
+ never reached as a ferial by {!readings}'s own step 3 in the first
+ place -- a real lectio entry with no reachable colitur day, the same
+ "structurally unreachable" shape as tools/bootstrap_lectionary_of
+ .ml's own "easter-6-thu"/"advent-4-sat" (see its own [excluded_bases]
+ comment).
+
+ [None] on a Sunday: Advent 4, Holy Family, the Second Sunday after
+ Christmas and the Baptism of the Lord can each land inside these two
+ windows, each is a NAMED office with its own distinct slug
+ ({!Rite_of.Temporal_of.sunday_slug}), and each must keep taking
+ priority over a ferial date -- checked here explicitly rather than
+ trusted to the call site, since this function's whole contract is
+ "this civil date, unconditionally, is this citation", which is false
+ on the Sunday the window shares its calendar date with. *)
+val date_keyed_slug : Date.t -> Slug.t option
diff --git a/test/test_lectionary_of.ml b/test/test_lectionary_of.ml
index 6fb48a0..03838be 100644
--- a/test/test_lectionary_of.ml
+++ b/test/test_lectionary_of.ml
@@ -172,13 +172,14 @@ let sha256_of_file path =
| hash :: _ -> hash
| [] -> Alcotest.fail "sha256sum produced no output"
-(* Pinned 2026-08-26 against the shipped file itself, independently
- re-derived (`sha256sum data/of/lectionary.sexp`), the same "derive the
- new value, don't transcribe it from generator stdout" discipline
+(* Pinned 2026-08-26 (FIX 1, O-Antiphon/Christmas-season date-keyed review)
+ against the shipped file itself, independently re-derived
+ (`sha256sum data/of/lectionary.sexp`), the same "derive the new value,
+ don't transcribe it from generator stdout" discipline
test_calendar_of_data.ml's own [test_sha256] already follows. *)
let test_sha256_pinned () =
Alcotest.(check string) "data/of/lectionary.sexp SHA-256"
- "e7fd109bc30baf8a5531a614995e986959214424c80f4d444fa2b772538decde" (sha256_of_file lectionary_path)
+ "2277e3dc302e853d78b72608884f59eb67671bfdcca4f5e4c99358ee12811f89" (sha256_of_file lectionary_path)
(* lectio's OWN of-lectionary.ini SHA-256, pinned inside data/of/
lectionary.sexp's own provenance header (tools/bootstrap_lectionary_of
@@ -244,7 +245,7 @@ let test_missing_source_fails_loudly () =
let test_load () =
let l = real_lectionary () in
- Alcotest.(check int) "754 entries" 754 (List.length (Lectionary.entries l))
+ Alcotest.(check int) "770 entries" 770 (List.length (Lectionary.entries l))
(* ---- coverage: a full sample civil year, both directions, PINNED ------ *)
@@ -357,6 +358,105 @@ let test_step2_sanctoral_wins () =
(cs <> Option.get (Lectionary.find lectionary (Slug.of_string_exn "of-advent-1-monday")))
| _ -> Alcotest.fail "30 November 2026 should resolve readings via step 2"
+(* ---- date-keyed windows (OLM n. 69.3) -- FIX 1, 2026-08-26 review ----- *)
+
+(* {!Lect_of.date_keyed_slug} itself, direct unit coverage of its own
+ boundaries: the O-Antiphon window (17-24 December), the Christmas-season
+ window (29-31 December, then 2-5 and 7 January), 6 January's own
+ deliberate exclusion (Temporal_of's own [named] fixes Epiphany there
+ unconditionally, so that date can never reach this function's caller as
+ a ferial), and the Sunday guard (a Sunday inside either window is a
+ NAMED office -- Advent 4, Holy Family, the Second Sunday after
+ Christmas -- never one of these ferial dates). *)
+let test_date_keyed_slug_boundaries () =
+ let some_str dt = Option.map Slug.to_string (Lect_of.date_keyed_slug dt) in
+ Alcotest.(check (option string)) "16 December: outside the window" None (some_str (d 2026 12 16));
+ Alcotest.(check (option string)) "17 December: in the window" (Some "of-advent-dec-17") (some_str (d 2026 12 17));
+ Alcotest.(check (option string)) "24 December: in the window" (Some "of-advent-dec-24") (some_str (d 2029 12 24));
+ Alcotest.(check (option string)) "25 December: outside the window" None (some_str (d 2026 12 25));
+ Alcotest.(check (option string)) "28 December: outside the window" None (some_str (d 2026 12 28));
+ Alcotest.(check (option string)) "29 December: in the window" (Some "of-christmas-dec-29") (some_str (d 2026 12 29));
+ Alcotest.(check (option string)) "31 December: in the window" (Some "of-christmas-dec-31") (some_str (d 2029 12 31));
+ Alcotest.(check (option string)) "1 January: outside the window" None (some_str (d 2026 1 1));
+ Alcotest.(check (option string)) "2 January: in the window" (Some "of-christmas-jan-2") (some_str (d 2026 1 2));
+ Alcotest.(check (option string)) "5 January: in the window" (Some "of-christmas-jan-5") (some_str (d 2026 1 5));
+ Alcotest.(check (option string))
+ "6 January: deliberately excluded, Epiphany is fixed there unconditionally" None (some_str (d 2026 1 6));
+ Alcotest.(check (option string)) "7 January: in the window" (Some "of-christmas-jan-7") (some_str (d 2026 1 7));
+ Alcotest.(check (option string)) "8 January: outside the window" None (some_str (d 2026 1 8));
+ Alcotest.(check bool) "2028-12-24 is a Sunday, sanity check" true (D.weekday (d 2028 12 24) = D.Sun);
+ Alcotest.(check (option string)) "24 December on a Sunday (2028, Advent 4): guarded off" None
+ (some_str (d 2028 12 24));
+ Alcotest.(check bool) "2025-01-05 is a Sunday, sanity check" true (D.weekday (d 2025 1 5) = D.Sun);
+ Alcotest.(check (option string)) "5 January on a Sunday (2025): guarded off" None (some_str (d 2025 1 5))
+
+(* The fix's own headline claim, through the REAL resolution chain, not
+ just the slug function in isolation: the same civil date resolves the
+ SAME citation across two years with a different weekday alignment,
+ where it used to drift. Verified by hand against the pre-fix binary
+ before this fix landed: 2024-12-17 served "Zephaniah 3:1-2,9-13/
+ Matth 21:28-32" (that year's own "of-advent-3-tuesday" content) while
+ 2029-12-17 served "Numbers 24:2-7,15-17a/Matth 21:23-27" ("of-advent-3-
+ monday") -- two DIFFERENT citations for the identical calendar date.
+ 2024 (Tuesday) and 2029 (Monday) are two different, non-Sunday
+ weekdays, so neither the sanctoral layer nor the Sunday guard is in
+ play -- both years reach {!Lect_of.readings}'s own step 3 through the
+ plain temporal-ferial branch, exactly like {!test_step3_temporal_ferial}
+ above. *)
+let test_o_antiphon_no_drift () =
+ let check_year y expected_weekday_slug =
+ let lectionary = real_lectionary () in
+ let layer = real_sanctoral_layer () in
+ let index = L.index layer ~easter:Computus.gregorian_easter ~years:[ y - 1; y ] in
+ let date = d y 12 17 in
+ let expected_slug = (T.temporal date).Colitur_kernel.Temporal.office.Cel.slug in
+ Alcotest.(check string) (Printf.sprintf "%d-12-17: expected temporal slug" y) expected_weekday_slug
+ (Slug.to_string expected_slug);
+ match readings_on lectionary index date with
+ | Some fm, cs ->
+ Alcotest.(check bool) (Printf.sprintf "%d: tagged Own_slug" y) true
+ (fm.Mass_formulary.via = Mass_formulary.Own_slug);
+ Alcotest.(check (option string)) (Printf.sprintf "%d: said the temporal slug" y)
+ (Some (Slug.to_string expected_slug)) (Option.map Slug.to_string fm.Mass_formulary.said);
+ Alcotest.(check (list string)) (Printf.sprintf "%d: 17 December is Gen 49:2,8-10 / Matthew 1:1-17" y)
+ [ "Genesis 49:2,8-10"; "Matthew 1:1-17" ]
+ (List.map (fun (c : Colitur_kernel.Citation.t) -> c.reference) cs)
+ | None, _ -> Alcotest.failf "%d-12-17 should resolve readings" y
+ in
+ check_year 2024 "of-advent-3-tuesday";
+ check_year 2029 "of-advent-3-monday"
+
+(* Same claim, the Christmas-season window, through a REALISTIC step 2 ->
+ step 3 path: 2 January carries the fixed memorial of Saints Basil and
+ Gregory Nazianzen (calendar-2002.sexp), which has no dedicated
+ lectionary entry of its own, so both years fall through to the day's
+ own ferial -- exactly the shape 2024-01-02's own CLI-verified pre-fix
+ drift took (that year served "1 John 3:22-4:6/Matt 4:12-17,23-25",
+ 2029's weekday-keyed content, before this fix). 2024 (Tuesday) and 2025
+ (Thursday) are again two different non-Sunday weekdays. *)
+let test_christmas_date_keyed_no_drift () =
+ let check_year y =
+ let lectionary = real_lectionary () in
+ let layer = real_sanctoral_layer () in
+ let index = L.index layer ~easter:Computus.gregorian_easter ~years:[ y - 1; y ] in
+ let date = d y 1 2 in
+ (match L.on_date index date with
+ | [ e ] ->
+ Alcotest.(check string) (Printf.sprintf "%d-01-02: Basil/Gregory's own entry" y)
+ "saints-basil-the-great-and-gregory-nazianzen-bishops-and-doctors" (Slug.to_string e.L.cel.Cel.slug)
+ | es -> Alcotest.failf "%d-01-02: expected exactly one sanctoral entry, got %d" y (List.length es));
+ match readings_on lectionary index date with
+ | Some fm, cs ->
+ Alcotest.(check bool) (Printf.sprintf "%d: tagged Own_slug (fell through to the ferial)" y) true
+ (fm.Mass_formulary.via = Mass_formulary.Own_slug);
+ Alcotest.(check (list string)) (Printf.sprintf "%d: 2 January is 1 John 2:22-28 / John 1:19-28" y)
+ [ "1 John 2:22-28"; "John 1:19-28" ]
+ (List.map (fun (c : Colitur_kernel.Citation.t) -> c.reference) cs)
+ | None, _ -> Alcotest.failf "%d-01-02 should resolve readings" y
+ in
+ check_year 2024;
+ check_year 2025
+
let suite =
[ Alcotest.test_case "Sunday cycle table (OLM n.66, straddles Advent I)" `Quick test_sunday_cycle_table;
Alcotest.test_case "weekday cycle table (OLM n.69.4, straddles Advent I)" `Quick test_weekday_cycle_table;
@@ -364,9 +464,14 @@ let suite =
Alcotest.test_case "data/of/lectionary.sexp SHA-256 pinned" `Quick test_sha256_pinned;
Alcotest.test_case "lectio source SHA-256 pinned in the header" `Quick test_source_sha256_pinned_in_header;
Alcotest.test_case "a missing lectio source fails loudly, not silently" `Quick test_missing_source_fails_loudly;
- Alcotest.test_case "loads, 754 entries" `Quick test_load;
+ Alcotest.test_case "loads, 770 entries" `Quick test_load;
Alcotest.test_case "2026 coverage: exactly {25 December} unresolved" `Quick test_coverage_2026;
Alcotest.test_case "2026 coverage: 364 days resolve" `Quick test_pinned_resolved_count;
Alcotest.test_case "step 3: temporal ferial fallback" `Quick test_step3_temporal_ferial;
- Alcotest.test_case "step 2: sanctoral proper wins over the ferial" `Quick test_step2_sanctoral_wins
+ Alcotest.test_case "step 2: sanctoral proper wins over the ferial" `Quick test_step2_sanctoral_wins;
+ Alcotest.test_case "date_keyed_slug boundaries (OLM n. 69.3)" `Quick test_date_keyed_slug_boundaries;
+ Alcotest.test_case "O-Antiphon date-keyed reading does not drift (17 December)" `Quick
+ test_o_antiphon_no_drift;
+ Alcotest.test_case "Christmas-season date-keyed reading does not drift (2 January)" `Quick
+ test_christmas_date_keyed_no_drift
]
diff --git a/tools/bootstrap_lectionary_of.ml b/tools/bootstrap_lectionary_of.ml
index f521bb1..da37bd9 100644
--- a/tools/bootstrap_lectionary_of.ml
+++ b/tools/bootstrap_lectionary_of.ml
@@ -459,12 +459,14 @@ let named_overrides =
and Epiphany, 2-5 Jan) -> stretch 1.
- "christmas-after-epiphany-<wd>" (7 Jan..Baptism) -> stretch 2.
lectio ALSO carries "christmas-dec-29/30/31" and "christmas-jan-
- 2..7", a DATE-keyed duplicate of the same stretches -- deliberately
- UNUSED (see [excluded_bases] below): colitur's own slugs are
- weekday-keyed, not date-keyed, so a fixed civil date such as
- "29 December" is not any single colitur slug -- it is a DIFFERENT
- weekday (hence a different of-christmas-0-<weekday>) in different
- years, and Lectionary.t has no per-date lookup at all. *)
+ 2..7" -- content-verified (2026-08-26 review) NOT duplicates of the
+ weekday-keyed families above (17 December's own O-Antiphon sibling
+ proved the same shape first: Gen 49:2,8-10/Matt 1:1-17, found
+ nowhere else in the 754 emitted entries). These are OLM n. 69.3's
+ genuinely date-fixed readings; mapped separately below via
+ {!Rite_of.Lectionary_of.date_keyed_slug}, not through this table --
+ see [excluded_bases]'s own comment for exactly which of the two
+ ranges are and are not still excluded, and why. *)
("christmas-octave-mon", [ "of-christmas-0-monday" ]);
("christmas-octave-tue", [ "of-christmas-0-tuesday" ]);
("christmas-octave-wed", [ "of-christmas-0-wednesday" ]);
@@ -482,24 +484,58 @@ let named_overrides =
("christmas-after-epiphany-wed", [ "of-christmas-2-wednesday" ]);
("christmas-after-epiphany-thu", [ "of-christmas-2-thursday" ]);
("christmas-after-epiphany-fri", [ "of-christmas-2-friday" ]);
- ("christmas-after-epiphany-sat", [ "of-christmas-2-saturday" ])
+ ("christmas-after-epiphany-sat", [ "of-christmas-2-saturday" ]);
+ (* OLM n. 69.3's date-fixed windows (see the block comment above and
+ Rite_of.Lectionary_of.date_keyed_slug's own doc comment for the
+ full citation and argument -- this task's fix, 2026-08-26 review).
+ These slugs are NOT Temporal_of office slugs (no colitur day's
+ [Celebration.slug] is ever literally "of-advent-dec-17") -- they
+ exist purely as [Lectionary.t] lookup keys that
+ [Lectionary_of.readings]'s own step 3 constructs directly from the
+ civil date via [date_keyed_slug], bypassing this table's usual
+ "base name -> real colitur slug" contract. [assert_reachable]
+ below is widened with its own matching date-keyed reachability
+ sweep (calling the SAME function, not a re-implementation) so a
+ typo here still dies loudly rather than shipping a dead key.
+ 6 January is deliberately absent -- see [excluded_bases]. *)
+ ("advent-dec-17", [ "of-advent-dec-17" ]);
+ ("advent-dec-18", [ "of-advent-dec-18" ]);
+ ("advent-dec-19", [ "of-advent-dec-19" ]);
+ ("advent-dec-20", [ "of-advent-dec-20" ]);
+ ("advent-dec-21", [ "of-advent-dec-21" ]);
+ ("advent-dec-22", [ "of-advent-dec-22" ]);
+ ("advent-dec-23", [ "of-advent-dec-23" ]);
+ ("advent-dec-24", [ "of-advent-dec-24" ]);
+ ("christmas-dec-29", [ "of-christmas-dec-29" ]);
+ ("christmas-dec-30", [ "of-christmas-dec-30" ]);
+ ("christmas-dec-31", [ "of-christmas-dec-31" ]);
+ ("christmas-jan-2", [ "of-christmas-jan-2" ]);
+ ("christmas-jan-3", [ "of-christmas-jan-3" ]);
+ ("christmas-jan-4", [ "of-christmas-jan-4" ]);
+ ("christmas-jan-5", [ "of-christmas-jan-5" ]);
+ ("christmas-jan-7", [ "of-christmas-jan-7" ])
]
-(* Deliberately unmapped, with the reason named -- see [named_overrides]'s
- own comment on the Christmas-season families for the date-vs-weekday
- mismatch these two groups share:
+(* Deliberately unmapped, with the reason named. CORRECTED 2026-08-26
+ review: this list previously ALSO carried the 16 O-Antiphon/Christmas-
+ season date-keyed bases, under the claim that they were "date-keyed
+ duplicates" of the weekday-keyed families [named_overrides] already
+ maps. That claim was false -- diffed against lectio's own ini directly,
+ each carries unique per-date content (17 December: Gen 49:2,8-10/
+ Matt 1:1-17, found nowhere else among the 754 emitted entries), which
+ OLM n. 69.3 explains: those ferias are date-fixed, not merely
+ non-alternating within a weekday slot. They are mapped now, via
+ [named_overrides]'s own date-keyed block, not here. Genuinely still
+ unreachable, for three DIFFERENT structural reasons, none of them
+ "duplicate":
- - "advent-dec-17".."advent-dec-24": the O-Antiphon days. colitur's own
- Temporal_of has no per-date slug for late Advent at all (Temporal_ef's
- EF counterpart doesn't either); these ferias fall through to the
- ordinary week-numbered "of-advent-{3,4}-<weekday>" slug, and WHICH
- weekday-string a given December date is varies year to year, so no
- static base-name mapping is possible. A future task wanting these
- eight dates covered needs a genuinely date-keyed lookup path in
- Lectionary_of.readings, not a slug rename here.
- - "christmas-dec-29/30/31", "christmas-jan-2/3/4/5/6/7": the date-keyed
- duplicates of the weekday-keyed families already mapped above. Same
- reasoning, same remedy if ever closed.
+ - "christmas-jan-6": 6 January is always Epiphany in colitur's model
+ (Temporal_of's own [named] fixes it unconditionally, "m = 1 && dd =
+ 6", before the ferial dispatch ever runs) -- "of-christmas-jan-6"
+ would be a real lectio entry with no colitur day that could ever look
+ it up. See Rite_of.Lectionary_of.date_keyed_slug's own doc comment,
+ which deliberately excludes 6 January from its date range for this
+ exact reason.
- "easter-6-thu": Thursday of Easter week 6 is STRUCTURALLY, always,
the Ascension (Easter+39 is always a Thursday) -- Temporal_of's own
[named] intercepts it before the generic ferial branch ever runs, so
@@ -510,19 +546,32 @@ let named_overrides =
(when 24 December is itself the Fourth Sunday of Advent, week 4 has
no ferial days whatsoever) -- "of-advent-4-saturday" is not a slug
Temporal_of can ever produce either.
- Both confirmed, not assumed: [assert_reachable]'s own reachability
- sweep flagged each as a genuine dead key this generator's own pattern
- rule produced (after fixing the suffix-stripping bug that function's
- own comment describes, which had masked them under 580 false
- positives on this generator's first run). *)
-let excluded_bases =
- [ "advent-dec-17"; "advent-dec-18"; "advent-dec-19"; "advent-dec-20"; "advent-dec-21";
- "advent-dec-22"; "advent-dec-23"; "advent-dec-24";
- "christmas-dec-29"; "christmas-dec-30"; "christmas-dec-31";
- "christmas-jan-2"; "christmas-jan-3"; "christmas-jan-4"; "christmas-jan-5"; "christmas-jan-6";
- "christmas-jan-7"; "easter-6-thu"; "advent-4-sat" ]
+ The latter two confirmed, not assumed: [assert_reachable]'s own
+ reachability sweep flagged each as a genuine dead key this
+ generator's own pattern rule produced (after fixing the suffix-
+ stripping bug that function's own comment describes, which had
+ masked them under 580 false positives on this generator's first
+ run). *)
+let excluded_bases = [ "christmas-jan-6"; "easter-6-thu"; "advent-4-sat" ]
-type mapping_report = { mapped : int; excluded : int; unmapped : string list }
+type mapping_report = {
+ mapped : int;
+ excluded : int;
+ unmapped : string list;
+ sanctoral_passthrough : int;
+ (* Count of the [Hashtbl.mem sanctoral_slugs base] branch below --
+ how many of the real merged sanctoral layer's own slugs got a
+ DEDICATED lectio entry this way, out of its full total (passed in
+ separately at the print site, [Hashtbl.length sanctoral_slugs]).
+ Named explicitly, 2026-08-26 review's own Minor finding: the
+ REMAINDER (222 shipped sanctoral slugs total, per that review --
+ 189 without a dedicated entry) is not a gap, it is OLM norms
+ working as designed (a saint with no proper of its own falls
+ through to the day's own ferial, {!Lectionary_of.readings}'s own
+ step 2 -> step 3), but the provenance header never said so before
+ this fix, which reads as an unstated 85% gap rather than the
+ correctly-small dedicated-entry set it actually is. *)
+}
(* Resolves every (base, resolved-citations) pair into zero or more
(colitur-slug, resolved-citations) pairs, and separately tracks what
@@ -535,6 +584,7 @@ type mapping_report = { mapped : int; excluded : int; unmapped : string list }
let map_bases resolved ~sanctoral_slugs =
let unmapped = ref [] in
let excluded = ref 0 in
+ let sanctoral_passthrough = ref 0 in
let out =
List.filter_map
(fun (base, r) ->
@@ -550,14 +600,19 @@ let map_bases resolved ~sanctoral_slugs =
| Some [ slug ] -> Some (slug, r)
| Some _ -> die "%s: named_overrides entry must name exactly one slug" base
| None ->
- if Hashtbl.mem sanctoral_slugs base then Some (base, r)
+ if Hashtbl.mem sanctoral_slugs base then begin
+ incr sanctoral_passthrough;
+ Some (base, r)
+ end
else begin
unmapped := base :: !unmapped;
None
end))
resolved
in
- (out, { mapped = List.length out; excluded = !excluded; unmapped = List.sort compare !unmapped })
+ ( out,
+ { mapped = List.length out; excluded = !excluded; unmapped = List.sort compare !unmapped;
+ sanctoral_passthrough = !sanctoral_passthrough } )
(* ---- Emit one Lectionary.t entry per (slug, resolved) pair ---------- *)
@@ -661,13 +716,42 @@ let strip_emitted_suffix s =
None [ "-a"; "-b"; "-c"; "-ii"; "-i" ]
|> Option.value ~default:s
-let assert_reachable entries ~temporal_slugs ~sanctoral_slugs =
+(* THIS TASK'S fix, 2026-08-26 review: the 16 new date-keyed slugs
+ ([named_overrides]'s own date-keyed block) are not Temporal_of office
+ slugs and not sanctoral slugs, so without this they would all be
+ flagged DEAD by [assert_reachable] below. Calls
+ Rite_of.Lectionary_of.date_keyed_slug directly -- the SAME function
+ {!Rite_of.Lectionary_of.readings} calls at runtime -- rather than
+ re-deriving the date ranges here a second time, so a typo in either
+ [named_overrides]'s literal strings or in [date_keyed_slug]'s own
+ ranges still dies loudly instead of silently drifting apart. Swept over
+ the same 2004-2051 range as [reachable_temporal_slugs] and for the same
+ reason: every distinct date recurs every year, only its weekday
+ alignment (irrelevant here, [date_keyed_slug] itself is weekday-
+ independent except for its Sunday guard) and which years hit a Sunday
+ change. *)
+let reachable_date_keyed_slugs () =
+ let tbl = Hashtbl.create 32 in
+ let mk y m d = match Date.make ~year:y ~month:m ~day:d with Ok t -> t | Error e -> die "%s" e in
+ for y = 2004 to 2051 do
+ let d = ref (mk y 1 1) in
+ let stop = mk y 12 31 in
+ while Date.compare !d stop <= 0 do
+ (match Rite_of.Lectionary_of.date_keyed_slug !d with
+ | Some s -> Hashtbl.replace tbl (Slug.to_string s) true
+ | None -> ());
+ d := Date.add_days !d 1
+ done
+ done;
+ tbl
+
+let assert_reachable entries ~temporal_slugs ~sanctoral_slugs ~date_keyed_slugs =
let base_of s = strip_emitted_suffix (Slug.to_string s) in
let dead =
List.filter
(fun (s, _) ->
let b = base_of s in
- not (Hashtbl.mem temporal_slugs b || Hashtbl.mem sanctoral_slugs b))
+ not (Hashtbl.mem temporal_slugs b || Hashtbl.mem sanctoral_slugs b || Hashtbl.mem date_keyed_slugs b))
entries
in
(match dead with
@@ -676,8 +760,8 @@ let assert_reachable entries ~temporal_slugs ~sanctoral_slugs =
List.iter
(fun (s, _) ->
Printf.eprintf
- "bootstrap_lectionary_of: DEAD KEY -- %S is not a real Temporal_of slug (2004-2051) \
- nor a real sanctoral slug\n"
+ "bootstrap_lectionary_of: DEAD KEY -- %S is not a real Temporal_of slug (2004-2051), \
+ nor a real sanctoral slug, nor a real date_keyed_slug (2004-2051)\n"
(Slug.to_string s))
dead;
die "%d emitted key(s) are unreachable (see above)" (List.length dead));
@@ -715,7 +799,14 @@ let temporal_day_gap ~lectionary ~year_start =
let t = Rite_of.Temporal_of.temporal date in
let base = Slug.to_string t.Temporal.office.Celebration.slug in
let found =
- Lectionary.mem lectionary (slug_or_die base)
+ (* THIS TASK'S fix, 2026-08-26 review: tried first, matching
+ Lectionary_of.readings' own step-3 order, so the O-Antiphon/
+ Christmas-season date-keyed days this fix closed no longer
+ misreport as a gap in the header below. *)
+ (match Rite_of.Lectionary_of.date_keyed_slug date with
+ | Some s -> Lectionary.mem lectionary s
+ | None -> false)
+ || Lectionary.mem lectionary (slug_or_die base)
|| Lectionary.mem lectionary
(slug_or_die
(base ^ "-"
@@ -749,7 +840,8 @@ let () =
let mapped, report = map_bases resolved ~sanctoral_slugs in
let entries = List.concat_map entries_of mapped in
let temporal_slugs = reachable_temporal_slugs () in
- let uncovered_temporal = assert_reachable entries ~temporal_slugs ~sanctoral_slugs in
+ let date_keyed_slugs = reachable_date_keyed_slugs () in
+ let uncovered_temporal = assert_reachable entries ~temporal_slugs ~sanctoral_slugs ~date_keyed_slugs in
let lect = match Lectionary.of_entries entries with Ok l -> l | Error e -> die "%s" e in
let gap_count, gap_total, gap_misses =
temporal_day_gap ~lectionary:lect ~year_start:Rite_of.Temporal_of.year_start
@@ -783,34 +875,64 @@ let () =
; a MISSING source file is fatal, checked before any read is attempted --\n\
; see this generator's own [parse_ini]) and commit the diff instead.\n\
; Every emitted key is asserted, at generation time, to be a slug\n\
- ; Rite_of.Temporal_of actually computes OR a slug the real merged\n\
+ ; Rite_of.Temporal_of actually computes, OR a slug the real merged\n\
; sanctoral layer (data/of/calendar-2002.sexp + all 13 decree overlays)\n\
- ; actually carries -- see [assert_reachable].\n\
+ ; actually carries, OR a date Rite_of.Lectionary_of.date_keyed_slug\n\
+ ; actually reaches -- see [assert_reachable].\n\
+ ;\n\
+ ; CORRECTED 2026-08-26 (review): this file used to exclude 16 lectio\n\
+ ; bases -- the 8 O-Antiphon days (17-24 December) and 8 further\n\
+ ; Christmas-season dates (29-31 December, 2-5 and 7 January) -- as\n\
+ ; \"date-keyed duplicates\" of the weekday-keyed ferial families mapped\n\
+ ; above. That was false: each carries content found nowhere else among\n\
+ ; the emitted entries (17 December: Gen 49:2,8-10/Matt 1:1-17), which\n\
+ ; OLM n. 69.3 explains -- these ferias are fixed by CIVIL DATE, not\n\
+ ; merely non-alternating within a weekday slot like every other Advent/\n\
+ ; Christmastide feria. Every day in 2005-2050 previously served a\n\
+ ; DRIFTING citation there (whichever weekday-keyed family that year's\n\
+ ; own alignment happened to land on) instead of the Missal's fixed one.\n\
+ ; Fixed via a new date-keyed lookup route, tried BEFORE the weekday-\n\
+ ; keyed one (Rite_of.Lectionary_of.date_keyed_slug, readings' own step\n\
+ ; 3) -- Temporal_of's slugs are UNCHANGED, only which lectionary key\n\
+ ; resolves the day's citations. 3 lectio bases remain excluded, for\n\
+ ; three genuinely different structural-unreachability reasons, none of\n\
+ ; them \"duplicate\" -- see [excluded_bases]'s own comment.\n\
;\n\
; Source: %s\n\
; SHA-256: %s\n\
; %d ini sections (988 expected) -> %d resolved (base, cycle-shape) \
-pairs -> %d colitur slugs mapped, %d ini bases deliberately excluded (date-\n\
- ; keyed duplicates/late-Advent, no colitur date-keyed lookup exists --\n\
- ; see [excluded_bases]), %d ini bases genuinely unmapped (no pattern, no\n\
- ; override, no matching sanctoral slug) -> %d emitted lectionary entries.\n\
+pairs -> %d colitur slugs mapped, %d ini bases genuinely excluded (structurally\n\
+ ; unreachable -- see [excluded_bases]), %d ini bases genuinely unmapped\n\
+ ; (no pattern, no override, no matching sanctoral slug) -> %d emitted\n\
+ ; lectionary entries.\n\
;\n\
; COVERAGE, BOTH DIRECTIONS (Step 1 of this task's own brief):\n\
; (1) Temporal-day gap: of the %d days of civil year 2026, %d produce a\n\
- ; Rite_of.Temporal_of slug with NO entry in this file (tried flat,\n\
- ; then both the Sunday- and weekday-cycle letter suffixes -- the\n\
- ; same order Lectionary_of.readings itself tries). Those are days\n\
- ; with no readings via the temporal-slug path (a sanctoral proper\n\
- ; on the same civil day, where one exists, is Lectionary_of's own\n\
+ ; Rite_of.Temporal_of slug with NO entry in this file (tried the\n\
+ ; date-keyed route first, then flat, then both the Sunday- and\n\
+ ; weekday-cycle letter suffixes -- the same order\n\
+ ; Lectionary_of.readings itself tries). Those are days with no\n\
+ ; readings via the temporal-slug path (a sanctoral proper on the\n\
+ ; same civil day, where one exists, is Lectionary_of's own\n\
; separate first chance). Named set below.\n\
; (2) Unmapped lectio keys: %d ini base names (of 988 sections, %d\n\
; distinct bases) map to no colitur slug at all -- data being\n\
; silently dropped if unreported. Named below.\n\
+ ; (3) Sanctoral coverage (Minor, 2026-08-26 review): of the %d shipped\n\
+ ; sanctoral slugs (data/of/calendar-2002.sexp + all 13 decree\n\
+ ; overlays), %d have a DEDICATED entry in this file (a lectio base\n\
+ ; name this generator recognised verbatim as one of them). The\n\
+ ; other %d have none and correctly fall through to the day's own\n\
+ ; ferial (Lectionary_of.readings' own step 2 -> step 3) -- this is\n\
+ ; OLM norms working as designed for a saint with no proper of\n\
+ ; their own, not a gap in this data.\n\
; Regenerate with:\n\
; eval $(opam env) && dune exec tools/bootstrap_lectionary_of.exe -- %s %s\n"
src (sha256 src) (List.length secs) (List.length resolved) report.mapped
(List.length excluded_bases) (List.length report.unmapped) (List.length entries)
- gap_total gap_count (List.length report.unmapped) (List.length resolved) src dst;
+ gap_total gap_count (List.length report.unmapped) (List.length resolved)
+ (Hashtbl.length sanctoral_slugs) report.sanctoral_passthrough
+ (Hashtbl.length sanctoral_slugs - report.sanctoral_passthrough) src dst;
Printf.fprintf oc "; Unmapped lectio bases (%d):\n" (List.length report.unmapped);
List.iter (fun b -> Printf.fprintf oc "; %s\n" b) report.unmapped;
Printf.fprintf oc "; Temporal_of slugs (2004-2051) with no lectionary entry (%d, informational --\n\