summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-22 13:48:08 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-22 13:48:08 +0200
commit94ad73cf74d7f1cf02913e2f462c01028a8ef4b4 (patch)
tree6b184dc0563f8eb43867ca4f8060230f21dedc47 /test
parentff7fa965d68631b5caac771d6db3a0f2110cb5dd (diff)
downloadcolitur-94ad73cf74d7f1cf02913e2f462c01028a8ef4b4.tar.gz
colitur-94ad73cf74d7f1cf02913e2f462c01028a8ef4b4.zip
fix(kernel): Mass_formulary.t.said is honestly optional -- was false for Votive
The .mli promised said is "the slug whose Mass is said". For Votive (RG 78/309(a), the Saturday votive Mass of Our Lady) it was set to the day's own ferial slug -- whose Mass is exactly the one NOT said. A consumer joining rubrics to readings on that slug would silently get the wrong Mass: 2026-01-03 reports ef-christmas-1-saturday, which has zero entries in data/ef/lectionary.sexp, because the citations actually come from bvm_saturday_citations, a season-keyed function with no slug of its own anywhere in the shipped data. Chose the type-honest fix over the interim documentation one: said is now Slug.t option, None exactly for Votive, because there is genuinely no slug in the shipped data this field could report for that one source. Adding real ids for the five seasonal BVM Masses (the reviewer's first option) is out of scope -- a data restructuring this round explicitly does not carry. Threading the office slug through a second field was considered and rejected as redundant: the day's own office is already available on the same Liturgical_day.t via observed.slug, which every caller already has in scope regardless of via, so said does not need to duplicate it. colitur rubrics stays byte-identical: rubrics_line already has d.observed in scope and falls back to its slug when said is None, printing the exact value it always printed for a Votive row (verified directly, diffed against pre-fix output across four years). colitur day/readings are unaffected (neither reads Mass_formulary at all). colitur emit --format sexp's pretty-printed line count for 2027 moved 9011 -> 9025: every day's formulary record widened by said's own extra option wrapping, and to_string_hum wraps by column width. Cosmetic only, diffed line by line to confirm every change is this shape or a consequent wrap shift; recorded in test/cli.t alongside the 476(f) note it now sits next to.
Diffstat (limited to 'test')
-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
5 files changed, 82 insertions, 27 deletions
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