From 2ac3e3e8fcc66e20bf1687ae15f033d20b02216d Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 21 Aug 2026 22:18:39 +0200 Subject: feat(kernel): a type for which Mass a day says The lectionary's four-step chain already decides whether a day says its own proper, its own slug's entry, the preceding Sunday's Mass or a Common, and then discards that decision once the citations are out. An ordo needs to print it. --- lib/kernel/mass_formulary.ml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 lib/kernel/mass_formulary.ml (limited to 'lib/kernel/mass_formulary.ml') diff --git a/lib/kernel/mass_formulary.ml b/lib/kernel/mass_formulary.ml new file mode 100644 index 0000000..8151720 --- /dev/null +++ b/lib/kernel/mass_formulary.ml @@ -0,0 +1,8 @@ +type source = Proper | Own_slug | Preceding_sunday | Common [@@deriving sexp] +type t = { said : Slug.t; via : source } [@@deriving sexp] + +let source_to_string = function + | Proper -> "proper" + | Own_slug -> "own" + | Preceding_sunday -> "preceding-sunday" + | Common -> "common" -- cgit v1.3 From 28dc226a31fa0cc72432f5607ab5cd74503ef43d Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 21 Aug 2026 22:54:13 +0200 Subject: fix(kernel,ef): a votive Mass is not the day's own -- add Mass_formulary.Votive Mass_formulary.source was missing a case for a Mass said IN PLACE of the day's own office's Mass while that office is itself kept unchanged -- RG 309(a) and RG 431(e) both classify the RG 78 Saturday Mass of Our Lady this way, in the Missal's own words, as a "Missa votiva IV classis... de B. Maria Virg.", not as the day's own office's Mass. The Latin Mass Society Ordo (docs/research/ordo/lms-ordo-2024-2025.pdf) witnesses it directly, printing that day as "V Mass of BVM". The BVM-Saturday branch in Lectionary_ef.readings was tagged Own_slug for lack of a better constructor when Task 2 landed, flagged there as a judgement call rather than a specified answer. That call was wrong: left as Own_slug, a future comparison against the LMS Ordo (a later task in this plan) would read every BVM Saturday as a manufactured divergence between colitur's "own" Mass and the Ordo's votive one. Retagged to Votive; said is unchanged (still the day's own, reused ferial, temporal slug) since the office itself is unaffected, only the Mass said for it. Added the new constructor's source_to_string case ("votive") and its own test row, and pinned the branch itself in test_lectionary_ef.ml's formulary cases at 1 August 2026, verified directly against the resolver rather than assumed. Both this session's own drifted pins that the earlier commit inherited from the task brief are unaffected by this change, and both are re-confirmed independently correct in this round: dune test and the exhaustive sweep are green, and colitur day stays byte-identical against the pre-fix-round binary. --- lib/kernel/mass_formulary.ml | 3 ++- lib/kernel/mass_formulary.mli | 16 +++++++++++++--- lib/rites/rite_ef/lectionary_ef.ml | 38 ++++++++++++++++++++++++------------- lib/rites/rite_ef/lectionary_ef.mli | 14 ++++++++++---- test/test_lectionary_ef.ml | 22 +++++++++++++++++---- test/test_mass_formulary.ml | 2 +- 6 files changed, 69 insertions(+), 26 deletions(-) (limited to 'lib/kernel/mass_formulary.ml') diff --git a/lib/kernel/mass_formulary.ml b/lib/kernel/mass_formulary.ml index 8151720..cb3532b 100644 --- a/lib/kernel/mass_formulary.ml +++ b/lib/kernel/mass_formulary.ml @@ -1,4 +1,4 @@ -type source = Proper | Own_slug | Preceding_sunday | Common [@@deriving sexp] +type source = Proper | Own_slug | Preceding_sunday | Common | Votive [@@deriving sexp] type t = { said : Slug.t; via : source } [@@deriving sexp] let source_to_string = function @@ -6,3 +6,4 @@ let source_to_string = function | Own_slug -> "own" | Preceding_sunday -> "preceding-sunday" | Common -> "common" + | Votive -> "votive" diff --git a/lib/kernel/mass_formulary.mli b/lib/kernel/mass_formulary.mli index caea3eb..a2faf64 100644 --- a/lib/kernel/mass_formulary.mli +++ b/lib/kernel/mass_formulary.mli @@ -15,12 +15,22 @@ type source = | Own_slug (** the lectionary's entry for the day's own slug *) | Preceding_sunday (** a weekday with no proper resumes the preceding Sunday *) | Common (** a saint's assigned Common *) + | Votive + (** a Mass said IN PLACE of the day's own office's Mass, the office + itself being kept unchanged -- RG 309(a): "in Ecclesia universa, + Missae quae pro sancta Maria in sabbato, iuxta temporum + diversitatem, in Missali assignantur", corroborated by RG 431(e)'s + own classification of that Mass as "Missa votiva IV classis ... + de B. Maria Virg." The office/Mass split this constructor exists + to name is general (any rite may say a votive Mass on a day whose + OFFICE is not itself votive), even though EF's only witness today + is the RG 78/309(a) Saturday Mass of Our Lady. *) [@@deriving sexp] type t = { said : Slug.t; via : source } [@@deriving sexp] -(** The slug whose Mass is said. For {!Proper} and {!Own_slug} 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 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. *) 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 4325bed..13ff5d5 100644 --- a/lib/rites/rite_ef/lectionary_ef.ml +++ b/lib/rites/rite_ef/lectionary_ef.ml @@ -367,21 +367,33 @@ let readings ~lectionary ~commons ~observed ~temporal ~date ~temporal_at = would be dead code; placing it earlier would let it outrank a real saint's proper. - FORMULARY PROVENANCE, a genuine judgement call: {!Mass_formulary.source} - has no fifth constructor for "the RG 309(a) seasonal votive Mass", - so this is tagged [Own_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 - deliberately reuses the ordinary ferial slug, [Temporal_ef]'s own - [bvm_saturday_names]), so [said] is genuinely "the day's own - slug" -- [Own_slug]'s own documented meaning - (mass_formulary.mli's [said] comment) -- even though the - citations themselves come from [bvm_saturday_citations]'s - season table rather than a [Lectionary.find] hit. Flagged in the - task report as an interpretation, not a specified answer. *) + FORMULARY PROVENANCE: [Votive], not [Own_slug] (CORRECTED, fix + round 1, coordinator review -- the first pass tagged this + [Own_slug] for lack of a better constructor and flagged it as a + judgement call; [Mass_formulary.source] has grown a [Votive] + case since, precisely for this branch). RG 309(a) (this + branch's own header comment above) and RG 431(e) ("in Missis + votivis IV classis ... de B. Maria Virg. quae in sabbato + celebrantur", temporal_ef.ml's own colour-chain comment quotes + it in full) both classify this Mass itself, in the Missal's own + words, as a "Missa votiva" -- a votive Mass said IN PLACE of the + day's own office's Mass, the office (RG 78, Officium sanctae + Mariae in sabbato) being kept unchanged. WITNESSED, not merely + argued: the Latin Mass Society Ordo (docs/research/ordo/lms- + ordo-2024-2025.pdf) prints this exact day as "OUR LADY on + SATURDAY IV Cl W / V Mass of BVM" -- "V" is that Ordo's own + abbreviation for Votive. + + [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 + 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. *) if is_bvm_saturday_office observed temporal then let said = temporal.Temporal.office.Celebration.slug in - ( Some { Mass_formulary.said; via = Mass_formulary.Own_slug }, + ( Some { Mass_formulary.said; via = Mass_formulary.Votive }, bvm_saturday_citations temporal.Temporal.season ~month:(Date.month date) ~day:(Date.day date) ) else diff --git a/lib/rites/rite_ef/lectionary_ef.mli b/lib/rites/rite_ef/lectionary_ef.mli index 2f36926..43763ff 100644 --- a/lib/rites/rite_ef/lectionary_ef.mli +++ b/lib/rites/rite_ef/lectionary_ef.mli @@ -109,10 +109,16 @@ val commons_for : commons:Commons.t -> Slug.t -> (Slug.t * Citation.t list) opti - {b Step 2} -- the day's own temporal slug in the lectionary. {!Colitur_kernel.Mass_formulary.Own_slug}, [said] that slug. The RG 309(a) Saturday votive Mass of Our Lady also answers here (structurally, - not as a fifth numbered step): it is tagged the same way, because its - own guard only ever fires when the observed celebration already IS the - day's own temporal office (the office reuses the ordinary ferial - slug) -- see the implementation comment on that branch. + not as a fifth numbered step), but is tagged + {!Colitur_kernel.Mass_formulary.Votive} instead, not [Own_slug]: RG + 309(a)/431(e) classify it, in the Missal's own words, as a "Missa + votiva" said IN PLACE of the day's own office's Mass, the office (RG + 78) itself being kept -- witnessed by the Latin Mass Society Ordo, + which prints this day's Mass as "V" (Votive). [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. - {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/test_lectionary_ef.ml b/test/test_lectionary_ef.ml index bc2be8c..5bdce12 100644 --- a/test/test_lectionary_ef.ml +++ b/test/test_lectionary_ef.ml @@ -582,9 +582,10 @@ let test_commons_load_rejects_bad_data () = (* ---------------------------------------------------------------------- *) (* The formulary itself (Task 2): each step of the chain now reports HOW *) -(* it resolved, not only what it resolved. One day per step -- the same *) -(* dates this file already uses (and hand-verifies) elsewhere for the *) -(* citations those days carry, so no new date needs independent checking. *) +(* it resolved, not only what it resolved. One day per step, plus the RG *) +(* 309(a) votive branch (fix round 1) -- the same dates this file already *) +(* uses (and hand-verifies) elsewhere for the citations those days carry, *) +(* so no new date needs independent checking. *) (* ---------------------------------------------------------------------- *) let formulary_cases = @@ -605,7 +606,20 @@ let formulary_cases = (* 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, "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 + [Own_slug] -- the Missal's own RG 309(a)/431(e) classify this Mass as + a "Missa votiva", said in place of the day's own office's Mass while + the office (RG 78) itself is kept; witnessed by the Latin Mass + Society Ordo, which prints this day's Mass as "V" (Votive). 1 August + 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) ] let test_formulary_reports_its_source () = List.iter diff --git a/test/test_mass_formulary.ml b/test/test_mass_formulary.ml index b940161..824e0c7 100644 --- a/test/test_mass_formulary.ml +++ b/test/test_mass_formulary.ml @@ -14,7 +14,7 @@ let test_round_trips_through_sexp () = let test_to_string_names_the_source () = let cases = [ (MF.Proper, "proper"); (MF.Own_slug, "own"); (MF.Preceding_sunday, "preceding-sunday"); - (MF.Common, "common") ] + (MF.Common, "common"); (MF.Votive, "votive") ] in List.iter (fun (via, expected) -> -- cgit v1.3 From 94ad73cf74d7f1cf02913e2f462c01028a8ef4b4 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 22 Aug 2026 13:48:08 +0200 Subject: 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. --- bin/main.ml | 18 +++++++++++++++-- lib/kernel/mass_formulary.ml | 4 +++- lib/kernel/mass_formulary.mli | 29 +++++++++++++++++++++++---- lib/rites/rite_ef/lectionary_ef.ml | 30 +++++++++++++++++----------- lib/rites/rite_ef/lectionary_ef.mli | 14 ++++++++----- test/cli.t | 36 ++++++++++++++++++++++----------- test/test_lectionary_ef.ml | 40 +++++++++++++++++++++++++++---------- test/test_lms_ordo.ml | 15 ++++++++++++-- test/test_mass_formulary.ml | 16 +++++++++++++-- test/test_validate.ml | 2 +- 10 files changed, 153 insertions(+), 51 deletions(-) (limited to 'lib/kernel/mass_formulary.ml') 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 ())] instead of [(said )], 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 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--sunday-] 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 -- cgit v1.3