diff options
| -rw-r--r-- | bin/main.ml | 14 | ||||
| -rw-r--r-- | lib/kernel/calendar.ml | 4 | ||||
| -rw-r--r-- | lib/kernel/liturgical_day.ml | 1 | ||||
| -rw-r--r-- | lib/kernel/liturgical_day.mli | 4 | ||||
| -rw-r--r-- | lib/kernel/rite.ml | 1 | ||||
| -rw-r--r-- | lib/kernel/rite.mli | 15 | ||||
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.mli | 16 | ||||
| -rw-r--r-- | lib/rites/rite_ef/rite_ef.ml | 4 | ||||
| -rw-r--r-- | lib/rites/rite_ef/rite_ef.mli | 4 | ||||
| -rw-r--r-- | lib/rites/rite_ef/rubrics_ef.ml | 223 | ||||
| -rw-r--r-- | lib/rites/rite_ef/rubrics_ef.mli | 37 | ||||
| -rw-r--r-- | man/colitur.1 | 40 | ||||
| -rw-r--r-- | test/cli.t | 18 | ||||
| -rw-r--r-- | test/test_calendar.ml | 7 | ||||
| -rw-r--r-- | test/test_colitur.ml | 1 | ||||
| -rw-r--r-- | test/test_rubrics_ef.ml | 240 | ||||
| -rw-r--r-- | test/test_validate.ml | 12 |
17 files changed, 612 insertions, 29 deletions
diff --git a/bin/main.ml b/bin/main.ml index e00e020..f957190 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -388,7 +388,16 @@ let readings_line ~lang ~sigla (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.r asserted by {!Colitur_kernel.Validate}'s own ["formulary"] check -- but the type itself permits [None] (a rite with no lectionary), so this prints "-" rather than pattern-matching partially and crashing on a - guarantee that belongs to DATA, not to the type. *) + guarantee that belongs to DATA, not to the type. + + Task 5 (celebrant-rubrics-phase1): a fourth column, whether the Creed is + said (EF: RG 475-476, {!Rite_ef.Rubrics_ef.creed}) -- "true"/"false" + ([string_of_bool], not "yes"/"no" or "1"/"0": this row has no other + boolean column to be consistent with, so OCaml's own literal is the + 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. *) let rubrics_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) = let said, via = @@ -398,7 +407,8 @@ let rubrics_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_k Colitur_kernel.Mass_formulary.source_to_string f.Colitur_kernel.Mass_formulary.via ) | None -> ("-", "-") in - Printf.printf "%s\t%s\t%s\n" (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) said via + Printf.printf "%s\t%s\t%s\t%s\n" (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) said via + (string_of_bool d.Colitur_kernel.Liturgical_day.creed) (* One civil year, Jan 1 - Dec 31, matching [temporal_report]'s own scan -- NOT one liturgical year: [Colitur_kernel.Calendar.year] resolves a single diff --git a/lib/kernel/calendar.ml b/lib/kernel/calendar.ml index 7fe9f67..d55e38d 100644 --- a/lib/kernel/calendar.ml +++ b/lib/kernel/calendar.ml @@ -547,6 +547,9 @@ let build_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.index) rite.Rite.readings ~observed:resolution.Precedence.observed.Precedence.cel ~temporal ~date ~temporal_at:rite.Rite.temporal in + let creed = + rite.Rite.creed ~temporal ~observed:resolution.Precedence.observed.Precedence.cel ~date + in { Liturgical_day.date; rite = rite.Rite.id; @@ -559,6 +562,7 @@ let build_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.index) omitted; citations; formulary; + creed; } let year (rite : ('s, 'r) Rite.t) (layer : 'r Layer.t) (y : int) : diff --git a/lib/kernel/liturgical_day.ml b/lib/kernel/liturgical_day.ml index 709f878..09ac0a1 100644 --- a/lib/kernel/liturgical_day.ml +++ b/lib/kernel/liturgical_day.ml @@ -26,5 +26,6 @@ type ('s, 'r) t = { formulary : Mass_formulary.t option; (** which Mass the day says, and how that was decided; [None] only for a rite with no lectionary -- see {!Mass_formulary} *) + creed : bool; (** whether the Creed is said at this day's Mass; see {!Rite.t.creed} *) } [@@deriving sexp] diff --git a/lib/kernel/liturgical_day.mli b/lib/kernel/liturgical_day.mli index 4a71d6d..eb45e61 100644 --- a/lib/kernel/liturgical_day.mli +++ b/lib/kernel/liturgical_day.mli @@ -33,5 +33,9 @@ type ('s, 'r) t = { {!Mass_formulary}. [None] only for a rite with no lectionary; for EF it is [Some] on every day of every year 1583..9999, asserted by {!Validate}. *) + creed : bool; + (** Whether the Creed is said at this day's Mass -- {!Rite.t.creed}, + EF: RG 475-476. A decision, not an [option]: [false] for a rite + that has not implemented the rule, same as [creed] itself. *) } [@@deriving sexp] diff --git a/lib/kernel/rite.ml b/lib/kernel/rite.ml index a7d21d3..4999fc2 100644 --- a/lib/kernel/rite.ml +++ b/lib/kernel/rite.ml @@ -18,4 +18,5 @@ type ('s, 'r) t = { date:Date.t -> temporal_at:(Date.t -> ('s, 'r) Temporal.t) -> Mass_formulary.t option * Citation.t list; + creed : temporal:('s, 'r) Temporal.t -> observed:'r Celebration.t -> date:Date.t -> bool; } diff --git a/lib/kernel/rite.mli b/lib/kernel/rite.mli index 45f3ed6..39538d0 100644 --- a/lib/kernel/rite.mli +++ b/lib/kernel/rite.mli @@ -92,4 +92,19 @@ type ('s, 'r) t = { temporal identity (the preceding Sunday's, for the ferial rule) without re-implementing the temporal cycle -- the same shape [transfer_target]'s own [occupant] callback established. *) + creed : temporal:('s, 'r) Temporal.t -> observed:'r Celebration.t -> date:Date.t -> bool; + (** Whether the Creed is said, post-Gospel/homily, at this day's Mass + (EF: RG 475-476). A [bool], not an [option]: this is a decision, + and a rite that has not implemented the rule returns [false] + explicitly rather than leaving the question unanswered. + + [temporal] and [observed] are supplied for the same reason + [readings] gets both: a rubric like this one can turn on either + the day's TEMPORAL-cycle identity (e.g. "is this a Sunday, even + one a feast has displaced") or on the celebration actually + observed, and only the rite knows which. [date] is supplied for + the same reason [readings] gets it too -- a rubric keyed to an + Easter-relative window (e.g. "within the octave of Easter") needs + the civil date and the rite's own Easter to test it, and neither + [temporal] nor [observed] alone carries that arithmetic. *) } diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli index 4c2523a..0100d40 100644 --- a/lib/rites/rite_ef/precedence_ef.mli +++ b/lib/rites/rite_ef/precedence_ef.mli @@ -132,6 +132,22 @@ val sunday_marker : string recognising a live candidate again. *) val major_litanies_slug : string +(** RG 112(d)'s own closed, hand-verified list of sanctoral entries that are + themselves a feast/commemoration OF the Blessed Virgin Mary in her own + right -- see the .ml's own citation for the full derivation and the + entries considered and excluded. A celebration is "of the BVM" for + {!disposition}'s RG 112(d) branch when its [subject] is [Subject.Bvm] OR + its slug is on this list ({!Celebration.t}'s [subject] field is NOT + reliably [Bvm] for most Marian sanctoral entries -- bootstrapped from + lectio, most carry [Saint] instead, this list is the reliable signal). + + Exposed (task 5, the Creed, RG 475(c) "festis II classis... B. Mariae + Virg.") so a second rite-local consumer can test the identical + Marian-identity question without re-deriving its own list and risking + drift from this one -- the same reasoning {!vigil_feast_table} and + {!is_vigil} are already exposed for. *) +val marian_slugs : string list + (** [disposition ~winner ~loser]: RG 92-95, 33, 21-27, 94 (docs/research/ rules-register.md §4, "Occurrence", "Vigils" and "Caput IV, 'De feriis'"). What becomes of a losing candidate, decided by the LOSER's diff --git a/lib/rites/rite_ef/rite_ef.ml b/lib/rites/rite_ef/rite_ef.ml index ec6d2b9..e48ff33 100644 --- a/lib/rites/rite_ef/rite_ef.ml +++ b/lib/rites/rite_ef/rite_ef.ml @@ -8,6 +8,7 @@ module Vocab_ef = Vocab_ef module Temporal_ef = Temporal_ef module Precedence_ef = Precedence_ef module Lectionary_ef = Lectionary_ef +module Rubrics_ef = Rubrics_ef open Colitur_kernel @@ -45,4 +46,5 @@ let context ~lectionary ~commons : (Vocab_ef.season, Vocab_ef.rank) Rite.t = vigil_feast = Precedence_ef.vigil_feast }; season_runs = Vocab_ef.seasons; transfer_target = Precedence_ef.transfer_target; - readings = Lectionary_ef.readings ~lectionary ~commons } + readings = Lectionary_ef.readings ~lectionary ~commons; + creed = Rubrics_ef.creed } diff --git a/lib/rites/rite_ef/rite_ef.mli b/lib/rites/rite_ef/rite_ef.mli index 2183799..a4b5d87 100644 --- a/lib/rites/rite_ef/rite_ef.mli +++ b/lib/rites/rite_ef/rite_ef.mli @@ -10,6 +10,7 @@ module Vocab_ef = Vocab_ef module Temporal_ef = Temporal_ef module Precedence_ef = Precedence_ef module Lectionary_ef = Lectionary_ef +module Rubrics_ef = Rubrics_ef (** The EF rite, bundled (design spec's [RITE] signature, realised as a {!Colitur_kernel.Rite.t} value rather than a functor -- see rite.mli): @@ -32,6 +33,9 @@ module Lectionary_ef = Lectionary_ef slug, else (a weekday with no entry of its own) the preceding Sunday's temporal slug, in data/ef/lectionary.sexp. See {!Lectionary_ef.readings} for why the Common is consulted second rather than last. + - [creed]: {!Rubrics_ef.creed}, RG 475-476 -- whether the Creed is said. + The first rubric in this phase governing a part of Mass rather than + occurrence/precedence. Deliberately carries no [sanctoral]/[lectionary] fields the way the original design-doc sketch of [RITE] does: {!Colitur_kernel.Rite.t} (the diff --git a/lib/rites/rite_ef/rubrics_ef.ml b/lib/rites/rite_ef/rubrics_ef.ml new file mode 100644 index 0000000..eef86cb --- /dev/null +++ b/lib/rites/rite_ef/rubrics_ef.ml @@ -0,0 +1,223 @@ +(* RG 475-476 (docs/research/LT.txt, grep "dicitur symbolum"; scan1.txt line + ~3872-3888), quoted here in full so every branch below can cite its own + letter without re-quoting the whole rubric: + + "475. Post Evangelium aut homiliam, dicitur symbolum: + a) in qualibet dominica, etsi eius Officium alicui festo locum + cedat, vel Missa votiva II classis celebretur; + b) in festis I classis et in Missis votivis I classis; + c) in festis II classis Domini et B. Mariae Virg.; + d) per octavas Nativitatis Domini, Paschatis et Pentecostes, etiam + in festis occurrentibus et in Missis votivis; + e) in festis nataliciis Apostolorum et Evangelistarum, necnon in + festis Cathedrae S. Petri et S. Barnabae Ap. + + 476. Non dicitur symbolum: + a) in Missis sive chrismatis sive in Cena Domini, feria V + Hebdomadae sanctae, et in Missa Vigiliae paschalis; + b) in festis II classis, iis exceptis quae supra, n. 475 c et e, + recensentur; + c) in Missis votivis II classis; + d) in Missis festivis et votivis III et IV classis; + e) ratione alicuius commemorationis in Missa occurrentis; + f) in Missis defunctorum." + + SCOPE NOTE, checked once here rather than at every clause below: this + engine resolves ONE observed office and ONE Mass per civil day (see + Rite.t.readings' own doc comment) -- it has no separate "which votive + Mass is said" dimension. So the "vel/et...votivis" halves of 475(a)/(b), + 476(c) entirely, 476(d)'s "et votivis" half, and 476(f) (Requiem Masses, + also not modelled) are genuinely inapplicable to this implementation -- + a documented scope limit, not a defect. 476(e) needs no branch at all: + [creed] below reads only [observed], never a day's admitted + commemorations, so a commemoration can never change its answer by + construction. *) + +open Colitur_kernel + +(* RG 475(e): "in festis nataliciis Apostolorum et Evangelistarum, necnon in + festis Cathedrae S. Petri et S. Barnabae Ap." NATALICIUM means the feast + of the saint's own death (dies natalis) -- not every feast that merely + names him. That is precisely why the clause has to name the Chair of St + Peter and St Barnabas EXPLICITLY: neither is a natalicium (Peter's own is + 29 June, shared with Paul; Barnabas's is his own day, 11 June, but the + clause names him anyway, redundantly with the natalicium reading, rather + than leave it to inference), so neither would be covered without the + explicit "necnon". + + DERIVED, not copied from any list supplied with this task: grepped + data/ef/sanctoral.sexp directly for every entry whose English or Polish + name mentions "Apostle"/"Evangelist"/"Aposto{l/ł}a", then each + candidate's own date, rank and status checked against the calendarium + and against whether it is that saint's own dies natalis. Every entry + below was cross-checked against the shipped data, not assumed: + + andrew (30 Nov, Class2) -- his natalicium. + barnabas (11 June, Class3) -- named explicitly; also his own natalicium. + bartholomew (24 Aug, Class2) -- his natalicium. + chair-of-st-peter (22 Feb, Class2) -- named explicitly ("Cathedrae S. + Petri"); NOT a natalicium (Peter's own is 29 June, shared with Paul) + -- exactly why the clause has to name it. + james-the-greater (25 July, Class2) -- his natalicium. + john-the-evangelist (27 Dec, Class2) -- his natalicium (the one Apostle + traditionally held to have died a natural death; "natalicium" still + names his own feast day, not only a martyr's). + luke-the-evangelist (18 Oct, Class2) -- his natalicium. + mark (25 April, Class2) -- the Evangelist ("Marka Ewangelisty" in the + data's own Polish name); NOT "mark-i" (7 Oct), a different saint (a + Pope), excluded. + matthew (21 Sep, Class2) -- Apostle and Evangelist, his natalicium. + matthias (24 Feb, Class2) -- his natalicium. + sts-peter-paul (29 June, Class1) -- the natalicium of both. + sts-philip-james (11 May, Class2) -- the natalicium of both (James the + Less; there is no separate "james-the-less" entry in the data). + sts-simon-jude (28 Oct, Class2) -- the natalicium of both. + thomas (21 Dec, Class2) -- his natalicium. + + Checked and DELIBERATELY EXCLUDED (the Trap this clause is built around): + conversion-of-st-paul (25 Jan, Class3) -- not a natalicium: it + commemorates an EVENT of his life, not his death. + in-commemoratione-sancti-pauli-apostoli (30 June, Class3, status + Feast, so it CAN be observed, unlike the two entries below) -- a + secondary commemoration of Paul, not his dies natalis (his own is 29 + June, with Peter); its own name says so ("In Commemoratione", not a + feast of his martyrdom). + "peter" (25 Jan) and "paul" (22 Feb) -- both status + [Commemoration_only] (RG 110's own Peter/Paul companions, on the + Conversion and Chair days respectively: {!Precedence_ef.disposition}'s + own citation), so neither can ever be [observed]; moot either way, + but neither is a natalicium regardless. + mark-i (7 Oct) -- a different saint (Pope St Mark), not the + Evangelist. *) +let creed_apostle_slugs = + [ "andrew"; "barnabas"; "bartholomew"; "chair-of-st-peter"; "james-the-greater"; + "john-the-evangelist"; "luke-the-evangelist"; "mark"; "matthew"; "matthias"; + "sts-peter-paul"; "sts-philip-james"; "sts-simon-jude"; "thomas" ] + +let creed ~(temporal : (Vocab_ef.season, Vocab_ef.rank) Temporal.t) + ~(observed : Vocab_ef.rank Celebration.t) ~(date : Date.t) : bool = + let easter = Computus.gregorian_easter (Date.year date) in + let n = Date.to_rata date - Date.to_rata easter in + let m = Date.month date and dd = Date.day date in + let slug = Slug.to_string observed.Celebration.slug in + if + (* RG 475(d): "per octavas Nativitatis Domini, Paschatis et + Pentecostes, etiam in festis occurrentibus et in Missis votivis" -- + an unconditional window override, checked first: EVEN a saint's + feast that wins the day within one of the three octaves (St Stephen, + 26 December, is the live witness -- RG 67's own "Com. octavae + Nativitatis" note, quoted in full in temporal_ef.ml's [named]) still + says the Creed. Pure date/Easter-offset arithmetic, not season or + rank: Ascension (Easter+39) and the Pentecost Vigil (Easter+48) are + both [Class1] and both fall inside the Paschaltide SEASON but + outside either 8-day OCTAVE, so a rank- or season-based test here + would wrongly include them -- checked and rejected for exactly this + reason. + + Nativity: 25-31 December (its own day plus 7) + 1 January (RG 91 + entry 5's own "Octave Day of the Nativity", the identical table + entry as 24 December's vigil -- temporal_ef.ml's [named]). Easter: + Easter Sunday (offset 0) through Low Sunday (offset 7) -- RG 11's + own "dominicae Paschatis et Pentecostes sunt pariter festa I classis + CUM OCTAVA". Pentecost: Pentecost (offset 49) through Trinity Sunday + (offset 56), the RG 91-entry-14-adjacent "octave day" Trinity Sunday + itself names in temporal_ef.ml. *) + (m = 12 && dd >= 25 && dd <= 31) + || (m = 1 && dd = 1) + || (n >= 0 && n <= 7) + || (n >= 49 && n <= 56) + then true + else if + (* RG 475(a): "in qualibet dominica, ETSI EIUS OFFICIUM ALICUI FESTO + LOCUM CEDAT" -- the Creed is said on any Sunday even when a feast + displaces the Sunday's own office (RG 16(a): a Feast of the Lord I + or II class occurring on a II-class Sunday takes its place "cum + omnibus iuribus et privilegiis"). Read off [temporal]'s own weekday + -- the day's calendar fact, independent of whatever [observed] turns + out to be -- never off [observed]'s slug or rank, which is exactly + what would go silently wrong the day such an impeding feast wins: + see {!Colitur_kernel.Precedence.rules.admit}'s own [~temporal] + parameter (precedence.mli) for the identical argument, made there + for RG 111(b) rather than RG 475(a). *) + temporal.Temporal.weekday = Date.Sun + then true + else if + (* RG 23 (Caput IV, "De Feriis"): "Feriae I classis sunt: a) feria IV + cinerum; b) omnes feriae Hebdomadae sanctae." Ash Wednesday and every + feria of Holy Week (Monday through Saturday -- RG 91 entry 2's + Sacred Triduum, Thursday-Saturday, is a THIRD sub-case of this same + "feria", not a "festum": RG 35, immediately below in Caput VI, + defines "festum" as a distinct liturgical-day category from "feria", + RG 21-27) are FERIAE, never FESTA, however high their RG 91 rank -- + so 475(b)/(c)'s "in festis" never reaches them, regardless of rank. + + This single structural check subsumes 476(a)'s own explicit naming + of the Chrism Mass, the Mass of the Lord's Supper (both Holy + Thursday) and the Easter Vigil Mass (Holy Saturday's date): both are + already excluded here as Holy Week feriae, so 476(a) needs no + separate branch. (Good Friday needs no rubric at all -- RG 28's own + closing sentence on the Paschal Vigil aside, Good Friday's own + liturgical action has no Mass in the 1955-restored Holy Week to + begin with, so the question is moot there independent of this + check -- but this structural test correctly excludes it too, since + it is also named in RG 23(b).) *) + n = -46 || (n >= -6 && n <= -1) + then false + else if + (* RG 28-34 (Caput V, "De Vigiliis"): a vigil is its OWN liturgical-day + category, distinct from "festum" (RG 35, Caput VI) the same way a + feria is (immediately above) -- so 475(b)/(c)'s "in festis" does not + reach a vigil either, regardless of its own RG 91 rank. This is also + why 476(a) has to name the Easter Vigil explicitly: RG 28's own + closing sentence says the Paschal Vigil, uniquely, "non sit dies + liturgicus" [is not a liturgical day] at all, so it is not even a + "vigilia" in RG 29-32's numbered sense -- nothing else in this + taxonomy would have excluded it without that explicit clause, unlike + every OTHER vigil, which is excluded merely by being one. + {!Precedence_ef.is_vigil} already tests both slug conventions this + codebase's data uses (the temporal cycle's "-vigil" suffix and the + sanctoral bootstrap's "vigil-of-" prefix) for the identical RG 33 + question; reused here rather than re-derived, on the same footing as + {!Precedence_ef.marian_slugs} just below. *) + Precedence_ef.is_vigil slug + then false + else if + (* RG 475(b): "in festis I classis". Genuine feasts only, by + construction of the two exclusions immediately above (feriae, + vigils) -- every remaining [Class1] candidate reaching this branch + is a real festum: the Nativity, Epiphany, Ascension, Corpus Christi, + the Sacred Heart, Christ the King, a I-class sanctoral feast (the + Assumption, the Immaculate Conception...), or a I-class Sunday + (already [true] above via 475(a), so this branch is never the FIRST + to grant those a [true], only ever redundant with it). *) + observed.Celebration.rank = Vocab_ef.Class1 + then true + else if + (* RG 475(c): "in festis II classis Domini et B. Mariae Virg." -- + [subject = Lord] is reliably set on genuine II-class sanctoral + feasts of the Lord (Exaltation of the Holy Cross, the Purification, + the Transfiguration, the Commemoration of the Baptism of the Lord, + the Dedication of the Lateran Archbasilica -- checked directly + against data/ef/sanctoral.sexp: six [subject = Lord] entries ship, + none a vigil or feria) and on the two temporal-cycle Class2 Lord + feasts (Holy Family, Holy Name of Jesus). [subject = Bvm], by + contrast, is NOT reliable for the BVM half: checked directly against + the data, almost every Marian sanctoral entry (the Assumption, the + Annunciation, the Immaculate Heart, the Nativity of the BVM...) + ships [subject = Saint] instead -- {!Precedence_ef.marian_slugs} is + the list built (and, here, reused rather than re-derived) precisely + because the [subject] field cannot be trusted alone for this + question; see its own citation in precedence_ef.mli. *) + observed.Celebration.rank = Vocab_ef.Class2 + && (observed.Celebration.subject = Subject.Lord + || observed.Celebration.subject = Subject.Bvm + || List.mem slug Precedence_ef.marian_slugs) + then true + else + (* RG 475(e): see {!creed_apostle_slugs}'s own citation. Checked last + and without a rank guard, on purpose -- Barnabas is only [Class3] + and the Chair of St Peter's own [subject] is [Saint], so neither + would ever be reached by the two branches above; every [Class1] + entry on the list (Sts Peter & Paul) is already [true] via 475(b), + so this branch is redundant, never wrong, for those. *) + List.mem slug creed_apostle_slugs diff --git a/lib/rites/rite_ef/rubrics_ef.mli b/lib/rites/rite_ef/rubrics_ef.mli new file mode 100644 index 0000000..16e7491 --- /dev/null +++ b/lib/rites/rite_ef/rubrics_ef.mli @@ -0,0 +1,37 @@ +(** RG 475-476 (docs/research/LT.txt, grep "dicitur symbolum") -- whether the + Creed is said at Mass. The first rubric in this phase governing a PART OF + MASS rather than occurrence/precedence, hence its own module: see the + .ml's own header for the rubric quoted in full and every branch's + citation. *) + +open Colitur_kernel + +(** RG 475(e)'s "festis nataliciis Apostolorum et Evangelistarum, necnon in + festis Cathedrae S. Petri et S. Barnabae Ap." -- see the .ml's own + citation for how each entry was derived from and verified against + data/ef/sanctoral.sexp, and for what was deliberately excluded. Exposed + so the test suite can assert completeness against the shipped data the + same way {!Precedence_ef.vigil_feast_table} already lets it. *) +val creed_apostle_slugs : string list + +(** Whether the Creed is said, post-Gospel/homily, at the Mass this day + resolves to. + + [temporal] is read for exactly one fact -- RG 475(a)'s own "in qualibet + dominica, ETSI EIUS OFFICIUM ALICUI FESTO LOCUM CEDAT" ("on any Sunday, + EVEN WHEN a feast displaces its own Office"): a Sunday impeded by a + Feast of the Lord (RG 16(a)) still says the Creed, so this reads the + TEMPORAL cycle's own weekday, never [observed]'s. Every other clause + reads [observed] -- the celebration whose Mass is actually said -- and + [date], read once against the rite's own (Gregorian) Easter for the two + Easter-relative window questions (RG 475(d)'s three octaves, RG 23's + Ash-Wednesday-and-Holy-Week feriae). [creed] never reads + {!Liturgical_day.t.commemorations}: RG 476(e), "ratione alicuius + commemorationis in Missa occurrentis" [never say the Creed merely + because of a commemoration], holds by construction rather than by a + checked branch. *) +val creed : + temporal:(Vocab_ef.season, Vocab_ef.rank) Temporal.t -> + observed:Vocab_ef.rank Celebration.t -> + date:Date.t -> + bool diff --git a/man/colitur.1 b/man/colitur.1 index 5a8ebbf..29f5640 100644 --- a/man/colitur.1 +++ b/man/colitur.1 @@ -122,10 +122,11 @@ occurrence, commemoration and transfer. The Mass reading citations, one line per day. .TP .BI rubrics " YEAR" -The Mass formulary actually said, one line per day \(em not always the -day's own: a weekday with no proper resumes the preceding Sunday's, a -saint with no proper says his assigned Common, and RG 78/309(a)'s votive -Saturday Mass of Our Lady is said in place of an unoccupied office's own. +Two rubrics of the Mass, one line per day: which formulary is actually +said \(em not always the day's own: a weekday with no proper resumes the +preceding Sunday's, a saint with no proper says his assigned Common, and +RG 78/309(a)'s votive Saturday Mass of Our Lady is said in place of an +unoccupied office's own \(em and whether the Creed is said (RG 475\-476). See .B OUTPUT FORMAT below. @@ -459,12 +460,12 @@ own trailing field, above. .SS rubrics .RS .nf -date [TAB] formulary\-slug [TAB] source +date [TAB] formulary\-slug [TAB] source [TAB] creed .fi .RE .PP -The day's own Mass formulary: which slug's Mass is actually said, and how -that was decided. +The day's own Mass formulary (which slug's Mass is actually said, and how +that was decided), followed by whether the Creed is said (RG 475\-476). .B rubrics separates its fields with a literal TAB \(em not a plain space like .B day @@ -473,9 +474,9 @@ or like .B readings \(em because a resolved formulary NAME (a column a later version may add, -not this one) can carry both spaces and punctuation a citation never does, -which rules out either separator already in use above. A separate command -for the identical mechanical reason +not either of these) can carry both spaces and punctuation a citation never +does, which rules out either separator already in use above. A separate +command for the identical mechanical reason .B day is separate from .BR readings : @@ -487,12 +488,25 @@ leave it unsplittable by field number. .I source is one of .BR proper ", " own ", " preceding\-sunday ", " common " or " votive . +.I creed +is +.B true +or +.B false +(OCaml's own literal, not +.RB \(lq yes / no \(rq +or +.RB \(lq 1/0 \(rq : +this row has no other boolean field to be consistent with). A day with no +Mass at all for a rite that has not implemented the rule reads +.B false +outright \(em it is a decision, never a third \(lqunknown\(rq state. .RS .nf -2026\-01\-01 [TAB] ef\-circumcision [TAB] own -2038\-03\-08 [TAB] john\-of\-god [TAB] proper -2025\-12\-01 [TAB] ef\-advent\-sunday\-1 [TAB] preceding\-sunday +2026\-01\-01 [TAB] ef\-circumcision [TAB] own [TAB] true +2038\-03\-08 [TAB] john\-of\-god [TAB] proper [TAB] false +2025\-12\-01 [TAB] ef\-advent\-sunday\-1 [TAB] preceding\-sunday [TAB] false .fi .RE .PP @@ -210,9 +210,9 @@ separate command for the same mechanical reason `readings` is: `day`'s row is fixed-width space-separated with a variable-length "+slug" tail. $ colitur rubrics 2026 | head -3 - 2026-01-01 ef-circumcision own - 2026-01-02 ef-christmas-1-friday own - 2026-01-03 ef-christmas-1-saturday votive + 2026-01-01 ef-circumcision own true + 2026-01-02 ef-christmas-1-friday own false + 2026-01-03 ef-christmas-1-saturday votive false $ colitur rubrics 2026 | wc -l 365 @@ -224,19 +224,19 @@ apply to it -- step 2 does (the day's own temporal slug in the lectionary), tagged `own`. Contrast a real sanctoral saint with his own proper: $ colitur rubrics 2038 | grep '^2038-03-08' - 2038-03-08 john-of-god proper + 2038-03-08 john-of-god proper false A saint with no proper of his own says his assigned Common (step 4): $ colitur rubrics 2038 | grep '^2038-03-06' - 2038-03-06 common-of-non-virgins-1 common + 2038-03-06 common-of-non-virgins-1 common false A weekday with no proper of its own resumes the preceding Sunday's, never its own observed slug -- 1 December 2025 is the Monday after Advent I, and Advent's ferias have no Mass of their own (step 3): $ colitur rubrics 2025 | grep '^2025-12-01' - 2025-12-01 ef-advent-sunday-1 preceding-sunday + 2025-12-01 ef-advent-sunday-1 preceding-sunday false 3 January 2026 above ("votive") is the RG 78/309(a) Saturday Mass of Our Lady, said IN PLACE of the day's own office's Mass while the office (an @@ -257,9 +257,9 @@ diocesan overlay's local patron observed instead (no proper or Common of his own in the fixture), the chain falls all the way back to step 3: $ colitur rubrics 2026 --overlay fixtures/overlay-example-diocesan.sexp | grep '^2026-07-11' - 2026-07-11 ef-time-after-pentecost-sunday-6 preceding-sunday + 2026-07-11 ef-time-after-pentecost-sunday-6 preceding-sunday false $ colitur rubrics 2026 | grep '^2026-07-11' - 2026-07-11 ef-time-after-pentecost-6-saturday votive + 2026-07-11 ef-time-after-pentecost-6-saturday votive false `--lang`/`--raw`/`--sigla-*` are refused rather than silently ignored, unlike `readings`: this row resolves no display name and no citation for any of @@ -537,7 +537,7 @@ CSV run rather than one per year: sexp and xml are also available: $ colitur emit --format sexp --from 2027 --to 2027 | wc -l - 8881 + 9010 $ colitur emit --format xml --from 2027 --to 2027 | head -2 <?xml version="1.0" encoding="UTF-8"?> diff --git a/test/test_calendar.ml b/test/test_calendar.ml index 599dfb2..74fe26a 100644 --- a/test/test_calendar.ml +++ b/test/test_calendar.ml @@ -104,6 +104,11 @@ module Fixture = struct the sanctoral side. *) let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = (None, []) + (* No fixture here exercises the Creed rubric either -- a rite that has + not implemented it returns [false] explicitly, {!Rite.t.creed}'s own + documented default. *) + let creed ~temporal:_ ~observed:_ ~date:_ = false + let rite : (season, rank) Rite.t = { Rite.id = "synthetic-calendar"; vocab; year_start; temporal; anchors = (fun _ -> []); (* Not a Roman rite, but a Rite.t must supply SOME Easter now that @@ -111,7 +116,7 @@ module Fixture = struct any for a fixture; nothing here is Easter-relative, so the value is never actually read. *) easter = Colitur_kernel.Computus.gregorian_easter; - rules; season_runs = [ A; B ]; transfer_target; readings } + rules; season_runs = [ A; B ]; transfer_target; readings; creed } let entry ~month ~day ~slug ~rank = { Layer.date = (match Date_spec.fixed ~month ~day with Ok d -> d | Error e -> failwith e); diff --git a/test/test_colitur.ml b/test/test_colitur.ml index 2d7f5e1..60b61fc 100644 --- a/test/test_colitur.ml +++ b/test/test_colitur.ml @@ -13,6 +13,7 @@ let () = Test_config.suite; Test_overlay.suite; Test_overlay_ini.suite; Test_temporal_ef.suite; Test_validate.suite; Test_precedence.suite; Test_calendar.suite; Test_precedence_ef.suite; Test_sanctoral_ef.suite; Test_rite_ef.suite; + Test_rubrics_ef.suite; Test_differential.suite; Test_oracle.suite; Test_oracle.suite_2038; Test_oracle.suite_2035; Test_golden.suite; ("lectionary", Test_lectionary.suite); ("lectionary-ef", Test_lectionary_ef.suite); diff --git a/test/test_rubrics_ef.ml b/test/test_rubrics_ef.ml new file mode 100644 index 0000000..2000d61 --- /dev/null +++ b/test/test_rubrics_ef.ml @@ -0,0 +1,240 @@ +(* RG 475-476, the Creed -- see lib/rites/rite_ef/rubrics_ef.ml for the + rubric quoted in full and every branch's own citation. + + One end-to-end test per clause of 475, plus a 476 negative, resolved + against REAL calendar dates through the shipped sanctoral data (the same + pipeline `colitur day`/`colitur rubrics` use) -- every expected value + below was derived from the rubric's own text and checked against + `colitur day <year>`'s real output (slug/rank/subject/weekday) BEFORE + this module existed, never read off [Rubrics_ef.creed]'s own answer. + Two synthetic unit tests isolate Trap One (RG 475(a) reads [temporal], + never [observed]) directly, without depending on finding a real-calendar + coincidence. *) + +module Cal = Colitur_kernel.Calendar +module LD = Colitur_kernel.Liturgical_day +module Date = Colitur_kernel.Date +module Cel = Colitur_kernel.Celebration +module Colour = Colitur_kernel.Colour +module Subject = Colitur_kernel.Subject +module Slug = Colitur_kernel.Slug +module Temporal = Colitur_kernel.Temporal +module V = Rite_ef.Vocab_ef +module RE = Rite_ef.Rubrics_ef + +let mk y m d = match Date.make ~year:y ~month:m ~day:d with Ok t -> t | Error e -> failwith e + +(* Loaded once, module-level: every test below is a lookup against the same + shipped calendar, and {!Cal.day} recomputes its whole liturgical year on + every call (calendar.mli's own documented cost), so at minimum the layer + itself should not be reloaded and re-merged per test case. *) +let layer = + match Test_support.load_ef_layer () with Ok l -> l | Error e -> Alcotest.failf "%s" e + +let ctx = Test_support.ef_context () + +let creed_on y m d = (Cal.day ctx layer (mk y m d)).LD.creed + +let check name y m d expected = Alcotest.(check bool) name expected (creed_on y m d) + +(* ---- RG 475(a): "in qualibet dominica, etsi eius Officium alicui festo + locum cedat" ---- *) + +let test_475a_ordinary_sunday () = + (* 2026-01-25: an ordinary Time-after-Epiphany Sunday, Class2, green, + "Dominica III post Epiphaniam" -- confirmed via `colitur day 2026`, + no other clause of 475 could apply (Class2, subject Temporal, no + octave, no apostle/vigil slug), so [true] here can only come from + 475(a) itself. *) + check "475(a): an ordinary Sunday" 2026 1 25 true + +(* Trap One, isolated directly: RG 475(a)'s own "ETSI EIUS OFFICIUM ALICUI + FESTO LOCUM CEDAT" -- even when a feast has displaced the Sunday's own + office, the Creed is still said. [observed] below is deliberately shaped + so that NONE of 475(b)/(c)/(e) can produce [true] on their own (Class3, + subject Saint, a slug on no list this module knows); the only way + [creed] can return [true] is by reading [temporal]'s own [weekday], + never [observed]. The synthetic [date] (an ordinary July day, itself a + Wednesday in 2026) is chosen so nothing about the DATE itself suggests + a Sunday either -- proving the function reads [temporal.weekday], not + [Date.weekday date]. *) +let impeded_observed = + Cel.make ~slug:(Slug.of_string_exn "some-impeding-feast-of-the-lord") ~rank:V.Class3 + ~status:Cel.Feast ~colour:Colour.Red ~subject:Subject.Saint ~layer:"synthetic" () + +let synthetic_temporal ~weekday : (V.season, V.rank) Temporal.t = + { Temporal.season = V.Time_after_pentecost; week = Some 1; weekday; office = impeded_observed } + +let test_475a_reads_temporal_not_observed () = + Alcotest.(check bool) "Sunday-shaped [temporal] overrides a non-Sunday-shaped [observed]" true + (RE.creed ~temporal:(synthetic_temporal ~weekday:Date.Sun) ~observed:impeded_observed + ~date:(mk 2026 7 1)); + Alcotest.(check bool) "same [observed], non-Sunday [temporal]: false" false + (RE.creed ~temporal:(synthetic_temporal ~weekday:Date.Wed) ~observed:impeded_observed + ~date:(mk 2026 7 1)) + +(* ---- RG 475(b): "in festis I classis" ---- *) + +let test_475b_class1_feast () = + (* 2026-08-15: the Assumption, Class1. *) + check "475(b): a I-class feast" 2026 8 15 true + +(* ---- RG 475(c): "in festis II classis Domini et B. Mariae Virg." ---- *) + +let test_475c_lord () = + (* 2026-09-14: Exaltation of the Holy Cross, Class2, subject Lord. *) + check "475(c): a II-class feast of the Lord" 2026 9 14 true + +let test_475c_bvm_via_marian_slugs () = + (* 2026-08-22: Immaculate Heart of Mary, Class2 -- ships [subject = + Saint] in data/ef/sanctoral.sexp (confirmed by grep), so this can only + come out [true] via {!Rite_ef.Precedence_ef.marian_slugs}, not via + [subject = Bvm]. Also carries a real commemoration + (+sts-timothy-hippolytus-and-symphorianus-martyrs), a live instance of + 476(e): the commemoration plays no part in this answer. *) + check "475(c): a II-class BVM feast (via marian_slugs, subject=Saint in the data)" 2026 8 22 true + +(* ---- RG 475(d): "per octavas Nativitatis Domini, Paschatis et + Pentecostes, etiam in festis occurrentibus et in Missis votivis" ---- *) + +let test_475d_octave_even_occurring_feast () = + (* 2026-12-26: St Stephen, Class2, "S. Stephani Protomartyris" -- a real + saint's feast OCCURRING within the Octave of the Nativity (RG 67's own + "Com. octavae Nativitatis", carried as +ef-nativity-octave-day-2 in + colitur's own commemoration). RG 475(d)'s own "etiam in festis + occurrentibus" is written for exactly this shape: the Creed is said + regardless. (This is the one place this suite deliberately diverges + from the task brief's own worked example, which expected [false] here + -- the brief mis-cited 26 December as 476(b)'s "plain II-class feast" + case, missing that RG 475(d) explicitly overrides 476(b) inside the + Nativity octave; see the task report.) *) + check "475(d): a saint's feast occurring within the Nativity octave" 2026 12 26 true + +let test_475d_octave_day_boundary () = + (* 2026-01-01: the Octave Day of the Nativity itself (Circumcision), + Class1 -- also [true] via 475(b) alone, kept as a boundary check that + 1 January is correctly included in the 8-day window. *) + check "475(d): 1 January, the Octave Day of the Nativity" 2026 1 1 true + +(* ---- RG 475(e): "in festis nataliciis Apostolorum et Evangelistarum, + necnon in festis Cathedrae S. Petri et S. Barnabae Ap." ---- *) + +let test_475e_apostle_natalicium () = + (* 2026-11-30: St Andrew, Class2, subject Saint -- not covered by 475(c) + (not Domini/BVM), so [true] here can only come from 475(e)'s own + natalicia list. Also carries a real commemoration + (+ef-advent-1-monday), another live 476(e) instance. *) + check "475(e): an Apostle's own natalicium (Andrew)" 2026 11 30 true + +let test_475e_barnabas_named_explicitly () = + (* 2026-06-11: St Barnabas, Class3 -- named explicitly by the clause + ("S. Barnabae Ap."); at Class3 it could not reach [true] via 475(b) or + (c) regardless. *) + check "475(e): St Barnabas, named explicitly" 2026 6 11 true + +let test_475e_chair_of_peter_named_explicitly () = + (* 2027-02-22 (NOT 2026: Feb 22 2026 is impeded by Lent I Sunday, so the + Chair is not observed that year -- checked via `colitur day 2026` + before picking 2027 instead): the Chair of St Peter, Class2, subject + Saint -- NOT a natalicium (Peter's own is 29 June, shared with Paul), + so [true] here can only come from the clause's own explicit "Cathedrae + S. Petri" naming, not from the natalicium reading in general. *) + check "475(e): the Chair of St Peter, named explicitly (not a natalicium)" 2027 2 22 true + +let test_475e_excludes_conversion_of_paul () = + (* Trap Two, directly: 2027-01-25, the Conversion of St Paul, Class3 -- + names an Apostle but is NOT his natalicium (his own is 29 June, with + Peter); Class3 rules out 475(b)/(c), and this slug is deliberately + absent from [creed_apostle_slugs]. Picked 2027 for the same impeded- + Sunday reason as the Chair of Peter above (25 January 2026 is itself a + Sunday). *) + check "475(e) does NOT cover the Conversion of St Paul (not a natalicium)" 2027 1 25 false + +(* ---- RG 23 (feriae) / RG 476(a): Ash Wednesday, Holy Week's own feriae, + the Chrism/Lord's Supper Mass, the Easter Vigil Mass ---- *) + +let test_ash_wednesday_no_creed () = + check "RG 23(a)/476: Ash Wednesday, a I-class FERIA, not a festum" 2026 2 18 false + +let test_holy_thursday_no_creed () = + (* 2026-04-02: "Feria V in Cena Domini" -- RG 23(b)'s own "omnes feriae + Hebdomadae sanctae"; also explicitly named by 476(a) ("sive... in + Cena Domini"). *) + check "RG 23(b)/476(a): Holy Thursday (Mass of the Lord's Supper)" 2026 4 2 false + +let test_holy_saturday_easter_vigil_no_creed () = + (* 2026-04-04: "Sabbato sanctum" -- RG 23(b) again; also explicitly named + by 476(a) ("in Missa Vigiliae paschalis"). *) + check "RG 23(b)/476(a): Holy Saturday (the Easter Vigil Mass)" 2026 4 4 false + +(* ---- RG 476(b) negative: a plain II-class saint, not Domini/BVM, not an + Apostle/Evangelist ---- *) + +let test_476b_plain_class2_saint () = + (* 2026-08-10: St Lawrence, Class2, subject Saint -- a deacon and martyr, + no Apostle/Evangelist connection, not on any list this module reads. *) + check "476(b): a plain II-class saint (Lawrence) does not say the Creed" 2026 8 10 false + +(* ---- RG 28-34 (vigils): checked ahead of 475(c) so a Class2 vigil that + is ALSO on marian_slugs is still excluded ---- *) + +let test_vigil_excluded_even_when_class2_and_marian () = + (* 2026-08-14: Vigil of the Assumption, Class2 -- IS on + {!Rite_ef.Precedence_ef.marian_slugs} (a real Marian entry), so without + the vigil check ahead of 475(c) this would wrongly come out [true]. + Also carries a real commemoration (+eusebius-confessor), a second live + 476(e) instance. *) + check "vigils are excluded even when Class2 and Marian (Vigil of the Assumption)" 2026 8 14 false + +(* ---- RG 476(d): a IV-class office (also exercises RG 78/309(a)'s votive + Office of the BVM on Saturday, itself IV class) ---- *) + +let test_476d_bvm_saturday_office () = + (* 2026-07-11: the unoccupied Saturday's Office of Our Lady, Class4 -- + already a pinned example in test/cli.t (Task 4). *) + check "476(d): the BVM Saturday Office, IV class" 2026 7 11 false + +(* One full civil year, walked day by day: RG 475(a)'s own invariant, "every + Sunday says the Creed, no exceptions" -- the same sanity check the task + asks for at the domain-measurement step, pinned here as a real assertion + rather than left to a one-off shell scan. *) +let test_every_sunday_in_2026_says_the_creed () = + let days = Cal.year ctx layer 2026 in + Array.iter + (fun (d : (V.season, V.rank) LD.t) -> + if d.LD.temporal.Temporal.weekday = Date.Sun then + Alcotest.(check bool) + (Printf.sprintf "%s is a Sunday: creed must be true" (Date.to_iso8601 d.LD.date)) + true d.LD.creed) + days + +let suite = + ( "Rubrics_ef", + [ Alcotest.test_case "475(a): ordinary Sunday" `Quick test_475a_ordinary_sunday; + Alcotest.test_case "475(a): reads [temporal], not [observed] (Trap One)" `Quick + test_475a_reads_temporal_not_observed; + Alcotest.test_case "475(b): I-class feast" `Quick test_475b_class1_feast; + Alcotest.test_case "475(c): II-class feast of the Lord" `Quick test_475c_lord; + Alcotest.test_case "475(c): II-class BVM feast via marian_slugs" `Quick + test_475c_bvm_via_marian_slugs; + Alcotest.test_case "475(d): octave overrides an occurring feast" `Quick + test_475d_octave_even_occurring_feast; + Alcotest.test_case "475(d): 1 January octave-day boundary" `Quick test_475d_octave_day_boundary; + Alcotest.test_case "475(e): an Apostle's own natalicium" `Quick test_475e_apostle_natalicium; + Alcotest.test_case "475(e): Barnabas, named explicitly" `Quick + test_475e_barnabas_named_explicitly; + Alcotest.test_case "475(e): Chair of Peter, named explicitly" `Quick + test_475e_chair_of_peter_named_explicitly; + Alcotest.test_case "475(e) excludes the Conversion of St Paul (Trap Two)" `Quick + test_475e_excludes_conversion_of_paul; + Alcotest.test_case "RG 23/476: Ash Wednesday" `Quick test_ash_wednesday_no_creed; + Alcotest.test_case "RG 23/476(a): Holy Thursday" `Quick test_holy_thursday_no_creed; + Alcotest.test_case "RG 23/476(a): Holy Saturday / Easter Vigil" `Quick + test_holy_saturday_easter_vigil_no_creed; + Alcotest.test_case "476(b): plain II-class saint (negative)" `Quick + test_476b_plain_class2_saint; + Alcotest.test_case "vigils excluded even when Class2 and Marian" `Quick + test_vigil_excluded_even_when_class2_and_marian; + Alcotest.test_case "476(d): BVM Saturday Office, IV class" `Quick test_476d_bvm_saturday_office; + Alcotest.test_case "every Sunday in 2026 says the Creed" `Quick + test_every_sunday_in_2026_says_the_creed ] ) diff --git a/test/test_validate.ml b/test/test_validate.ml index 7387971..ea8fc0f 100644 --- a/test/test_validate.ml +++ b/test/test_validate.ml @@ -324,16 +324,22 @@ module Synthetic = struct { Colitur_kernel.Mass_formulary.said = 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 + implemented it returns [false] explicitly, {!Rite.t.creed}'s own + documented default. Made overridable ([?creed] below) on the same + footing as [?readings] just above, for Task 6's own fixtures. *) + let creed ~temporal:_ ~observed:_ ~date:_ = false + let rite ?(vocab = vocab) ?(anchors = fun _ -> []) ?(season_runs = [ A; B ]) ?(rules = rules) - ?(transfer_target = fun _ origin _ -> origin) ?(readings = readings) temporal : - (season, rank) Rite.t = + ?(transfer_target = fun _ origin _ -> origin) ?(readings = readings) ?(creed = creed) temporal + : (season, rank) Rite.t = { Rite.id = "synthetic"; vocab; year_start; temporal; anchors; rules; season_runs; (* Not a Roman rite, but a Rite.t must supply SOME Easter now that movable Date_spec variants exist. The Gregorian one is as good as any for a fixture; nothing here is Easter-relative, so the value is never actually read. *) easter = Colitur_kernel.Computus.gregorian_easter; - transfer_target; readings } + transfer_target; readings; creed } (* Empty by default: every check built before Task 12 exercises the TEMPORAL-only pass, where an empty layer is exactly the fixture that |
