diff options
| -rw-r--r-- | bin/main.ml | 18 | ||||
| -rw-r--r-- | lib/kernel/mass_formulary.ml | 4 | ||||
| -rw-r--r-- | lib/kernel/mass_formulary.mli | 29 | ||||
| -rw-r--r-- | lib/rites/rite_ef/lectionary_ef.ml | 30 | ||||
| -rw-r--r-- | lib/rites/rite_ef/lectionary_ef.mli | 14 | ||||
| -rw-r--r-- | test/cli.t | 36 | ||||
| -rw-r--r-- | test/test_lectionary_ef.ml | 40 | ||||
| -rw-r--r-- | test/test_lms_ordo.ml | 15 | ||||
| -rw-r--r-- | test/test_mass_formulary.ml | 16 | ||||
| -rw-r--r-- | test/test_validate.ml | 2 |
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 @@ -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 |
