summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bin/main.ml18
-rw-r--r--lib/kernel/mass_formulary.ml4
-rw-r--r--lib/kernel/mass_formulary.mli29
-rw-r--r--lib/rites/rite_ef/lectionary_ef.ml30
-rw-r--r--lib/rites/rite_ef/lectionary_ef.mli14
-rw-r--r--test/cli.t36
-rw-r--r--test/test_lectionary_ef.ml40
-rw-r--r--test/test_lms_ordo.ml15
-rw-r--r--test/test_mass_formulary.ml16
-rw-r--r--test/test_validate.ml2
10 files changed, 153 insertions, 51 deletions
diff --git a/bin/main.ml b/bin/main.ml
index f957190..582c776 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -397,13 +397,27 @@ let readings_line ~lang ~sigla (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.r
least surprising choice for a machine-readable field). Unlike
[formulary], [d.creed] is a plain [bool] with no [option] to guard: a
rite that has not implemented the rule answers [false] outright, so
- there is no third "unknown" state this column could ever need to print. *)
+ there is no third "unknown" state this column could ever need to print.
+
+ Whole-branch review fix round: {!Colitur_kernel.Mass_formulary.t.said}
+ itself gained an [option] (its own citation has the full account --
+ [None] exactly for [Votive], where the shipped data genuinely names no
+ slug for the Mass actually said). This column's own OUTPUT does not
+ change for that reason: when [said] is [None] it falls back to
+ [d.observed]'s own slug -- the SAME value this column always printed
+ for a [Votive] day before [said] became honest, and it is a value this
+ function already has in scope regardless of [via]. So this is not
+ "print a placeholder for the missing case", it is "the value was
+ already available from a different field, and still is". *)
let rubrics_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t)
=
let said, via =
match d.Colitur_kernel.Liturgical_day.formulary with
| Some f ->
- ( Colitur_kernel.Slug.to_string f.Colitur_kernel.Mass_formulary.said,
+ ( Colitur_kernel.Slug.to_string
+ (match f.Colitur_kernel.Mass_formulary.said with
+ | Some s -> s
+ | None -> d.Colitur_kernel.Liturgical_day.observed.Colitur_kernel.Celebration.slug),
Colitur_kernel.Mass_formulary.source_to_string f.Colitur_kernel.Mass_formulary.via )
| None -> ("-", "-")
in
diff --git a/lib/kernel/mass_formulary.ml b/lib/kernel/mass_formulary.ml
index cb3532b..60f9f1c 100644
--- a/lib/kernel/mass_formulary.ml
+++ b/lib/kernel/mass_formulary.ml
@@ -1,5 +1,7 @@
+open Sexplib0.Sexp_conv
+
type source = Proper | Own_slug | Preceding_sunday | Common | Votive [@@deriving sexp]
-type t = { said : Slug.t; via : source } [@@deriving sexp]
+type t = { said : Slug.t option; via : source } [@@deriving sexp]
let source_to_string = function
| Proper -> "proper"
diff --git a/lib/kernel/mass_formulary.mli b/lib/kernel/mass_formulary.mli
index a2faf64..b990e69 100644
--- a/lib/kernel/mass_formulary.mli
+++ b/lib/kernel/mass_formulary.mli
@@ -27,10 +27,31 @@ type source =
is the RG 78/309(a) Saturday Mass of Our Lady. *)
[@@deriving sexp]
-type t = { said : Slug.t; via : source } [@@deriving sexp]
+type t = { said : Slug.t option; via : source } [@@deriving sexp]
-(** The slug whose Mass is said. For {!Proper}, {!Own_slug} and {!Votive}
- this is the day's own; for {!Preceding_sunday} it is that Sunday's
- TEMPORAL slug; for {!Common} it is the Common's own id. *)
+(** The slug whose CITATIONS were actually used to build the day's
+ readings, when the shipped data can name one.
+
+ [Some] for four of the five sources: {!Proper} and {!Own_slug} each
+ carry the day's own slug; {!Preceding_sunday} carries that Sunday's
+ TEMPORAL slug; {!Common} carries the Common's own id.
+
+ [None] for {!Votive}, and ONLY for {!Votive} (whole-branch review fix
+ round, celebrant-rubrics-phase1): RG 309(a)'s five seasonal "Missae de
+ sancta Maria in sabbato" carry no slug of their own anywhere in the
+ shipped data -- their citations come from a season-keyed function
+ ({!Rite_ef.Lectionary_ef.bvm_saturday_citations}), never from a
+ [Lectionary.find] against any slug, so there is genuinely no slug this
+ field could honestly report. An earlier version of this type set
+ [said] to the day's own OFFICE slug for {!Votive} too (RG 78's Office
+ of Our Lady, kept unchanged under the votive Mass) -- readable at the
+ call site as "this is the slug whose Mass is said", which is false for
+ exactly this one case: that slug's own [Lectionary] entry, if it has
+ one at all, is NOT what the day's citations came from. [None] says so
+ directly instead of silently mis-naming a Mass. The day's own OFFICE
+ is not lost by this change -- it is still on the very same
+ {!Liturgical_day.t} this formulary lives on, via [observed.slug],
+ which every caller already has in scope regardless of [via]; this
+ field does not need to duplicate it. *)
val source_to_string : source -> string
diff --git a/lib/rites/rite_ef/lectionary_ef.ml b/lib/rites/rite_ef/lectionary_ef.ml
index 1fba3de..b996412 100644
--- a/lib/rites/rite_ef/lectionary_ef.ml
+++ b/lib/rites/rite_ef/lectionary_ef.ml
@@ -260,7 +260,7 @@ let is_bvm_saturday_office (observed : Vocab_ef.rank Celebration.t)
let readings ~lectionary ~commons ~observed ~temporal ~date ~temporal_at =
match observed.Celebration.citations with
| _ :: _ as cs ->
- (Some { Mass_formulary.said = observed.Celebration.slug; via = Mass_formulary.Proper }, cs)
+ (Some { Mass_formulary.said = Some observed.Celebration.slug; via = Mass_formulary.Proper }, cs)
| [] -> (
(* Step 4: a saint who is the day's observed office and has no proper
says his assigned Common. The assignment is explicit, never
@@ -357,7 +357,7 @@ let readings ~lectionary ~commons ~observed ~temporal ~date ~temporal_at =
if sanctoral_office then commons_for ~commons observed.Celebration.slug else None
with
| Some (common_id, cs) ->
- (Some { Mass_formulary.said = common_id; via = Mass_formulary.Common }, cs)
+ (Some { Mass_formulary.said = Some common_id; via = Mass_formulary.Common }, cs)
| None -> (
(* The votive Mass of Our Lady on Saturday (RG 309(a)) runs HERE:
after the proper (step 1) and the Common (step 4), which answer
@@ -407,23 +407,29 @@ let readings ~lectionary ~commons ~observed ~temporal ~date ~temporal_at =
[bvm_saturday_citations]'s own season-keyed SELECTION, not
merely that some BVM Mass was chosen.
- [said] is UNCHANGED by this correction and stays the day's own
- temporal slug: [is_bvm_saturday_office] only ever fires when
- [sanctoral_office] above is false, i.e. the observed celebration
- already IS the day's own temporal office (the office
+ [said] used to be set HERE to the day's own temporal slug
+ (reasoning: [is_bvm_saturday_office] only ever fires when
+ [sanctoral_office] above is false, i.e. the observed
+ celebration already IS the day's own temporal office, which
deliberately reuses the ordinary ferial slug, [Temporal_ef]'s
- own [bvm_saturday_names]) -- only [via] needed correcting, the
- office/Mass split [Votive] exists to name. *)
+ own [bvm_saturday_names]). CORRECTED (whole-branch review fix
+ round): that value is the OFFICE's slug, not the slug whose
+ Mass is actually said -- the five seasonal Masses RG 309(a)
+ names have no slug of their own anywhere in the shipped data,
+ so [said] is [None] here, honestly, rather than silently
+ naming the wrong thing. See {!Colitur_kernel.Mass_formulary.t}'s
+ own [said] citation for the full account; the office itself is
+ not lost, it is still [observed.slug] on the very same
+ {!Colitur_kernel.Liturgical_day.t} this formulary lives on. *)
if is_bvm_saturday_office observed temporal then
- let said = temporal.Temporal.office.Celebration.slug in
- ( Some { Mass_formulary.said; via = Mass_formulary.Votive },
+ ( Some { Mass_formulary.said = None; via = Mass_formulary.Votive },
bvm_saturday_citations temporal.Temporal.season ~month:(Date.month date)
~day:(Date.day date) )
else
match Lectionary.find lectionary temporal.Temporal.office.Celebration.slug with
| Some cs ->
( Some
- { Mass_formulary.said = temporal.Temporal.office.Celebration.slug;
+ { Mass_formulary.said = Some temporal.Temporal.office.Celebration.slug;
via = Mass_formulary.Own_slug },
cs )
| None -> (
@@ -478,5 +484,5 @@ let readings ~lectionary ~commons ~observed ~temporal ~date ~temporal_at =
with
| Some cs ->
let said = sunday_temporal.Temporal.office.Celebration.slug in
- (Some { Mass_formulary.said; via = Mass_formulary.Preceding_sunday }, cs)
+ (Some { Mass_formulary.said = Some said; via = Mass_formulary.Preceding_sunday }, cs)
| None -> (None, []))))
diff --git a/lib/rites/rite_ef/lectionary_ef.mli b/lib/rites/rite_ef/lectionary_ef.mli
index 0797c0d..715e9c2 100644
--- a/lib/rites/rite_ef/lectionary_ef.mli
+++ b/lib/rites/rite_ef/lectionary_ef.mli
@@ -123,11 +123,15 @@ val commons_for : commons:Commons.t -> Slug.t -> (Slug.t * Citation.t list) opti
naming which of the five seasonal Masses is said, not a marker of the
Mass's kind; see the implementation comment on this branch for the
full account and the validation use this correction still leaves
- available for Task 6.) [said] is still that slug: only the source
- constructor differs from an ordinary Step 2 lookup, because the guard
- that reaches this branch only ever fires when the observed celebration
- already IS the day's own temporal office -- see the implementation
- comment on that branch.
+ available for Task 6.) [said] is [None] here (whole-branch review fix
+ round, celebrant-rubrics-phase1): the five seasonal Masses RG 309(a)
+ names carry no slug of their own anywhere in the shipped data, so
+ there is no slug this field could honestly report -- see
+ {!Colitur_kernel.Mass_formulary.t}'s own [said] citation for the full
+ account. The day's own OFFICE (the guard that reaches this branch
+ only ever fires when the observed celebration already IS the day's
+ own temporal office) is unaffected and is still [observed.slug] on
+ the same {!Colitur_kernel.Liturgical_day.t}.
- {b Step 3} -- for a weekday whose own slug has no entry, the preceding
Sunday's temporal slug (never its observed one; a Sunday is guarded
out because it has no PRECEDING Sunday to resume, not because
diff --git a/test/cli.t b/test/cli.t
index e2d2eb5..971e442 100644
--- a/test/cli.t
+++ b/test/cli.t
@@ -534,20 +534,32 @@ CSV run rather than one per year:
$ colitur emit --format csv --from 2027 --to 2028 | wc -l
732
-sexp and xml are also available. This line count moved 9010 -> 9011
-(whole-branch review fix round, RG 476(f)): [emit --format sexp] pretty-
-prints with [Sexplib.Sexp.to_string_hum], a column-width wrapping printer,
-not a fixed-shape one -- 2027's All Souls' Day (2 November) record grew a
-single wrapped line when its own [creed] field's value changed from
-[true] to [false] (RG 476(f), the Creed is never said at a Requiem Mass;
-see rubrics_ef.ml), because "false" is one character longer than "true"
-and pushed that one line's rendered width over to_string_hum's own wrap
-threshold. Purely cosmetic -- the record's DATA is unchanged in every
-other field, and this is not a claim that [emit]'s FORMAT changed, only
-that one record's pretty-printed SHAPE did:
+sexp and xml are also available. This line count moved twice in the same
+whole-branch review fix round, for two different reasons, both cosmetic:
+[emit --format sexp] pretty-prints with [Sexplib.Sexp.to_string_hum], a
+column-width wrapping printer, not a fixed-shape one, so any change to a
+value's own rendered WIDTH can shift where it wraps.
+
+9010 -> 9011 (RG 476(f)): 2027's All Souls' Day (2 November) record grew
+a single wrapped line when its own [creed] field's value changed from
+[true] to [false] (the Creed is never said at a Requiem Mass; see
+rubrics_ef.ml), because "false" is one character longer than "true" and
+pushed that one line's rendered width over the wrap threshold.
+
+9011 -> 9025 (Mass_formulary.t.said honesty, Fix 3): [said] gained an
+[option] (its own .mli has the full account) -- every day's formulary
+record now prints [(said (<slug>))] instead of [(said <slug>)], one
+character wider, and a [Votive] day (the RG 78 Saturday Mass of Our
+Lady) prints [(said ())] instead of naming a slug at all, since the
+data genuinely names none for the Mass actually said. Both changes ripple
+across many lines' own wrap points, not just the days whose DATA changed
+-- confirmed directly (diffed the full sexp output line by line): every
+difference is exactly this [said] shape change or a consequent wrap
+shift, nothing else. Not a claim that [emit]'s FORMAT changed, only that
+individual records' pretty-printed SHAPE did:
$ colitur emit --format sexp --from 2027 --to 2027 | wc -l
- 9011
+ 9025
$ colitur emit --format xml --from 2027 --to 2027 | head -2
<?xml version="1.0" encoding="UTF-8"?>
diff --git a/test/test_lectionary_ef.ml b/test/test_lectionary_ef.ml
index 04076c7..3e0880e 100644
--- a/test/test_lectionary_ef.ml
+++ b/test/test_lectionary_ef.ml
@@ -591,22 +591,22 @@ let test_commons_load_rejects_bad_data () =
let formulary_cases =
[ (* step 1: a saint with his own proper -- same date as
[test_step1_proper_beats_any_common_john_of_god]. *)
- (2038, 3, 8, "john-of-god", Colitur_kernel.Mass_formulary.Proper);
+ (2038, 3, 8, Some "john-of-god", Colitur_kernel.Mass_formulary.Proper);
(* step 2: the day's own temporal slug -- same date as
[test_step2_lenten_feria_has_its_own], Monday of Lent I. *)
- (2026, 2, 23, "ef-lent-1-monday", Colitur_kernel.Mass_formulary.Own_slug);
+ (2026, 2, 23, Some "ef-lent-1-monday", Colitur_kernel.Mass_formulary.Own_slug);
(* step 3: a feria resuming the preceding Sunday. The task brief's own
snippet pinned this date against week 9 ("ef-time-after-pentecost-
sunday-9"); running the real resolver against 2026 shows 3 August
2026 is Monday of week 10, resuming 2 August's "...sunday-10" --
corrected per the brief's own "find the dates by running the current
binary if they drift" instruction. *)
- (2026, 8, 3, "ef-time-after-pentecost-sunday-10",
+ (2026, 8, 3, Some "ef-time-after-pentecost-sunday-10",
Colitur_kernel.Mass_formulary.Preceding_sunday);
(* step 4: a saint sent to a Common -- same date as
[test_step4_commons_perpetua_and_felicity]; [said] is the Common's
OWN id (data/ef/commons.sexp), not the saint's slug. *)
- (2038, 3, 6, "common-of-non-virgins-1", Colitur_kernel.Mass_formulary.Common);
+ (2038, 3, 6, Some "common-of-non-virgins-1", Colitur_kernel.Mass_formulary.Common);
(* Fix round 1 (coordinator review): the RG 309(a)/RG 78 Saturday votive
Mass of Our Lady, structurally reached between steps 4 and 2 (see
[readings]' own implementation comment) but tagged [Votive], not
@@ -623,9 +623,16 @@ let formulary_cases =
2026 verified directly against the real resolver (`colitur day
2026`), not trusted from a supplied date, given this session's own
drifted-pin history: a IV-class Saturday, "Officium sanctae Mariae in
- sabbato", temporal slug [ef-time-after-pentecost-9-saturday]. [said]
- is unaffected by the retag and stays that same (reused ferial) slug. *)
- (2026, 8, 1, "ef-time-after-pentecost-9-saturday", Colitur_kernel.Mass_formulary.Votive) ]
+ sabbato", temporal slug [ef-time-after-pentecost-9-saturday].
+ (CORRECTED, whole-branch review fix round: [said] used to be
+ claimed "unaffected by the retag" and pinned to that same reused
+ ferial slug -- that was the defect this round fixed. [said] is
+ [None] here: the shipped data names no slug for the votive Mass
+ actually said, only for the office it replaces. The office slug
+ itself is checked separately, below, via [observed.slug], not
+ through [said] -- see {!Colitur_kernel.Mass_formulary.t}'s own
+ citation for why the two are no longer conflated.) *)
+ (2026, 8, 1, None, Colitur_kernel.Mass_formulary.Votive) ]
let test_formulary_reports_its_source () =
List.iter
@@ -634,10 +641,10 @@ let test_formulary_reports_its_source () =
match day.Colitur_kernel.Liturgical_day.formulary with
| None -> Alcotest.failf "%04d-%02d-%02d: no formulary" y m d
| Some f ->
- Alcotest.(check string)
+ Alcotest.(check (option string))
(Printf.sprintf "%04d-%02d-%02d slug" y m d)
expected_slug
- (Colitur_kernel.Slug.to_string f.Colitur_kernel.Mass_formulary.said);
+ (Option.map Colitur_kernel.Slug.to_string f.Colitur_kernel.Mass_formulary.said);
Alcotest.(check string)
(Printf.sprintf "%04d-%02d-%02d source" y m d)
(Colitur_kernel.Mass_formulary.source_to_string expected_via)
@@ -645,6 +652,17 @@ let test_formulary_reports_its_source () =
f.Colitur_kernel.Mass_formulary.via))
formulary_cases
+(* [said = None] on the Votive day above does not mean the office is lost --
+ {!Colitur_kernel.Mass_formulary.t}'s own citation says a caller reads it
+ off [observed.slug] instead, on the very same {!Colitur_kernel.
+ Liturgical_day.t}. Checked directly, not merely asserted: the same
+ 1 August 2026 date, same expected slug the old (pre-fix) [said] field
+ used to carry. *)
+let test_votive_office_slug_still_available_via_observed () =
+ let d = day 2026 8 1 in
+ Alcotest.(check string) "2026-08-01 observed slug" "ef-time-after-pentecost-9-saturday"
+ (Colitur_kernel.Slug.to_string d.Colitur_kernel.Liturgical_day.observed.Colitur_kernel.Celebration.slug)
+
let suite =
[ ("step 1: sanctoral proper", `Quick, test_step1_sanctoral_proper);
("step 2: own temporal proper", `Quick, test_step2_lenten_feria_has_its_own);
@@ -685,4 +703,6 @@ let suite =
("Commons.load rejects the four silent-degradation defects", `Quick,
test_commons_load_rejects_bad_data);
("the formulary reports its own source, one day per step", `Quick,
- test_formulary_reports_its_source) ]
+ test_formulary_reports_its_source);
+ ("the Votive office slug is still available via observed, not said", `Quick,
+ test_votive_office_slug_still_available_via_observed) ]
diff --git a/test/test_lms_ordo.ml b/test/test_lms_ordo.ml
index cff4b3d..a728cbc 100644
--- a/test/test_lms_ordo.ml
+++ b/test/test_lms_ordo.ml
@@ -302,7 +302,9 @@ let describe_creed_mismatch (o : ordo_row) (c : colitur_row) =
Printf.sprintf "%s %S: colitur creed=%b, Ordo creed=%b (formulary=%s)" o.date o.title c.c_creed
(Option.get o.creed)
(match c.c_formulary with
- | Some f -> Colitur_kernel.Slug.to_string f.MF.said
+ | Some { MF.said = Some s; _ } -> Colitur_kernel.Slug.to_string s
+ | Some { MF.said = None; via = MF.Votive } -> "votive (said unnamed in the data)"
+ | Some { MF.said = None; _ } -> "NONE (said, unexpectedly outside Votive)"
| None -> "NONE")
(* The core assertion: every Creed divergence, over all 400 days (Good
@@ -587,7 +589,16 @@ let test_formulary_override_matches () =
:: !bad
| Some { MF.via = MF.Preceding_sunday; said } -> (
bump "preceding_sunday";
- let slug = Colitur_kernel.Slug.to_string said in
+ (* [said] is [Some] for every constructor except [Votive] (see
+ {!Colitur_kernel.Mass_formulary.t}'s own citation) -- a bare
+ [Option.get] here would raise an unhelpful exception if that
+ ever stopped being true; [Alcotest.failf] names the day
+ instead. *)
+ let slug =
+ match said with
+ | Some s -> Colitur_kernel.Slug.to_string s
+ | None -> Alcotest.failf "%s: Preceding_sunday day with said = None (should be impossible)" o.date
+ in
let ordo_says = Printf.sprintf "Mass of %s" in
(* Try the generic [ef-<season>-sunday-<n>] shape first; fall back
to the one NAMED Sunday that also reaches this population
diff --git a/test/test_mass_formulary.ml b/test/test_mass_formulary.ml
index 824e0c7..1316e47 100644
--- a/test/test_mass_formulary.ml
+++ b/test/test_mass_formulary.ml
@@ -4,9 +4,19 @@ module Slug = Colitur_kernel.Slug
let slug s = Slug.of_string_exn s
let test_round_trips_through_sexp () =
- let f = { MF.said = slug "ef-time-after-pentecost-sunday-9"; via = MF.Preceding_sunday } in
+ let f = { MF.said = Some (slug "ef-time-after-pentecost-sunday-9"); via = MF.Preceding_sunday } in
let f' = MF.t_of_sexp (MF.sexp_of_t f) in
- Alcotest.(check string) "slug survives" (Slug.to_string f.MF.said) (Slug.to_string f'.MF.said);
+ Alcotest.(check (option string)) "slug survives"
+ (Option.map Slug.to_string f.MF.said) (Option.map Slug.to_string f'.MF.said);
+ Alcotest.(check bool) "source survives" true (f.MF.via = f'.MF.via)
+
+(* [said] is [None] exactly for {!MF.Votive} -- see the .mli's own citation.
+ Round-tripped separately so the [None] case has its own witness, not only
+ inferred from the [Some] case above. *)
+let test_none_said_round_trips_through_sexp () =
+ let f = { MF.said = None; via = MF.Votive } in
+ let f' = MF.t_of_sexp (MF.sexp_of_t f) in
+ Alcotest.(check bool) "said stays None" true (f'.MF.said = None);
Alcotest.(check bool) "source survives" true (f.MF.via = f'.MF.via)
(* [to_string] is what an ordo line shows. It names the SOURCE, not the slug,
@@ -25,5 +35,7 @@ let test_to_string_names_the_source () =
let suite =
( "Mass_formulary",
[ Alcotest.test_case "sexp round-trips" `Quick test_round_trips_through_sexp;
+ Alcotest.test_case "sexp round-trips, said = None (Votive)" `Quick
+ test_none_said_round_trips_through_sexp;
Alcotest.test_case "source_to_string names each source" `Quick
test_to_string_names_the_source ] )
diff --git a/test/test_validate.ml b/test/test_validate.ml
index ea8fc0f..1ceaca2 100644
--- a/test/test_validate.ml
+++ b/test/test_validate.ml
@@ -321,7 +321,7 @@ module Synthetic = struct
(* The formulary equivalent of [well_formed_citations] above -- shape only,
never rubrically meaningful content. *)
let well_formed_formulary =
- { Colitur_kernel.Mass_formulary.said = Slug.of_string_exn "syn-formulary";
+ { Colitur_kernel.Mass_formulary.said = Some (Slug.of_string_exn "syn-formulary");
via = Colitur_kernel.Mass_formulary.Own_slug }
(* No fixture here exercises the Creed rubric -- a rite that has not