aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-22 23:24:12 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-22 23:24:12 +0200
commit0806fe65e388a9502035bc9d4f551528ac74b26d (patch)
tree094d96e0d685c90fbfc8c91e659d185b1141a1c0
parent761ae859d73660bcfaa59581312989cb942e704d (diff)
downloadcolitur-0806fe65e388a9502035bc9d4f551528ac74b26d.tar.gz
colitur-0806fe65e388a9502035bc9d4f551528ac74b26d.zip
feat(ef): the Mass preface, RG 482-499
Item 2 of Phase 3 (celebrant-rubrics-phase1), the bulk of this phase and the last EF Mass rubric this project scoped: which preface is said, deferring to seasonal/proper-title rules for the fourteen named prefaces, then RG 498's Common residual. RG 482 gives the resolution chain: the Mass's own proper preface, failing that the seasonal one, failing that Common. Read literally, RG 484-497 look like fourteen separate rules, but each numbered rubric's own propria/de-Tempore pair produces the SAME preface identity either way, so the whole chain collapses into one priority- ordered decision: title/mystery triggers (Holy Cross, Sacred Heart, Christ the King, Trinity, St Joseph, BVM, the Nativity octave, the Apostles, Epiphany), each independent of season, then six seasonal windows (Nativity, Epiphany, Lent, Holy Cross/Passiontide, Easter, Ascension, Holy Spirit, Trinity-for-ordinary-Sundays), then Common. The "one genuinely unproven piece" the design spec worried about -- per-feast proper prefaces extracted from the Missal's propers at scale -- turned out not to be a large-scale extraction problem at all: every one of the fourteen propers is a closed, small, subject/slug-keyed trigger (mirroring Precedence_ef.band's own RG 91 table), not thousands of individual saints' pages. The one genuine open question (RG 488, the Chrism Mass) is N/A: this engine resolves one Mass per civil day and has no separate Chrism-Mass dimension. Preface is a new kernel type (lib/kernel/preface.ml[i]), not an EF-specific one, the same placement as Colour/Subject/Mass_formulary: Liturgical_day.t is parameterised only over season/rank, so any field it carries generically must live in the kernel even though only EF constructs a value of it today. Wired exactly as creed/gloria were (Rite.t.preface, Calendar.ml, Rite_ef.context), but Preface.t option, not a bare bool: unlike creed/gloria, a preface is said only at a Mass, and Good Friday (1955-restored Holy Week) resolves an observed celebration but has no Mass at all -- None is the honest answer there, and also the neutral value an unimplemented rite returns. Two priority-order findings only the oracle settled, not derivable from the Latin text in isolation, both cross-checked against 358 individually classified entries in the FIUV Ordo's own praef column (test/fixtures/fiuv-ordo-2025-2026.sexp, already captured, wired up here as a new comparison axis): RG 484(b)'s own "except Masses with a proper of the divine mysteries or Persons" is narrower than every other window's implicit exception (an Apostle inside the Nativity octave is overridden to Nativity; outside it, keeps his own preface even inside another window); and RG 495's "et votivis" half is live for the one office this engine models without a votive-Mass dimension (the Saturday Office of the BVM), which also proved a vigil is not a "festum" for this purpose (the Assumption's own vigil takes Common, not BVM, correcting an initial reuse of Precedence_ef.marian_slugs that had no reason to make that distinction for its own, different rubric). A third, RG16(a)-shaped fix landed the same way: RG 494(b)'s own Trinity grant must read the day's TEMPORAL season, not the celebration that actually won it, or a Class1 feast with no preference of its own (All Saints) wrongly falls to Common on a Sunday it merely commemorates. colitur rubrics gains a sixth TAB-separated column. Domain-wide 1583-9999 exhaustive sweep confirms every Christmastide day resolves Nativity/Epiphany/Bvm, every Paschaltide day one of its own three windows or a season-independent title, every Lent day Lent or a title, and Passiontide legitimately produces Easter exactly once a year (the Vigil Mass) -- 8416 of the domain's 8417 years, the one short year being the domain's own upper boundary (the liturgical year opening in 9999 cannot construct dates in year 10000, a pre-existing edge this sweep re-confirms rather than a new one). day/readings verified byte-identical to the branch's own state before this phase (commit 9c96e0a) across a 455-year sample spanning the whole domain -- not literally to the v0.10.1 tag, which 22 earlier commits on this same branch (Phase 1/2, the bissextile shift, the Rogation colour fix) had already moved past before this phase began.
-rw-r--r--bin/main.ml45
-rw-r--r--lib/kernel/calendar.ml4
-rw-r--r--lib/kernel/liturgical_day.ml1
-rw-r--r--lib/kernel/liturgical_day.mli7
-rw-r--r--lib/kernel/preface.ml59
-rw-r--r--lib/kernel/preface.mli50
-rw-r--r--lib/kernel/rite.ml1
-rw-r--r--lib/kernel/rite.mli17
-rw-r--r--lib/rites/rite_ef/rite_ef.ml3
-rw-r--r--lib/rites/rite_ef/rite_ef.mli4
-rw-r--r--lib/rites/rite_ef/rubrics_ef.ml496
-rw-r--r--lib/rites/rite_ef/rubrics_ef.mli35
-rw-r--r--man/colitur.133
-rw-r--r--test/cli.t33
-rw-r--r--test/test_calendar.ml10
-rw-r--r--test/test_fiuv_ordo.ml153
-rw-r--r--test/test_rubrics_ef.ml387
-rw-r--r--test/test_validate.ml16
18 files changed, 1306 insertions, 48 deletions
diff --git a/bin/main.ml b/bin/main.ml
index cf8cae2..b5d8d98 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -413,7 +413,18 @@ let readings_line ~lang ~sigla (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.r
Task (celebrant-rubrics-phase1, Phase 2): a FIFTH column, whether the
Gloria in excelsis is said (EF: RG 431-432, {!Rite_ef.Rubrics_ef.gloria})
-- same [string_of_bool] convention as [creed], same plain [bool] with
- no [option] to guard, same reasoning throughout. *)
+ no [option] to guard, same reasoning throughout.
+
+ Task (celebrant-rubrics-phase1, Phase 3): a SIXTH column, which preface
+ is said (EF: RG 482-499, {!Rite_ef.Rubrics_ef.preface}) --
+ {!Colitur_kernel.Preface.to_string} (e.g. "common", "holy-cross"), or
+ "-" when [d.preface] is [None]. UNLIKE [creed]/[gloria], [preface] is a
+ genuine [option]: "-" here can mean either of two things ("this rite
+ has not implemented the rule" or "this specific day has no Mass to
+ preface", {!Colitur_kernel.Rite.t.preface}'s own citation) and this
+ column does not distinguish them, the same "-" convention [formulary]'s
+ own [None] case already uses two columns to the left, for the identical
+ reason. *)
let rubrics_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t)
=
let said, via =
@@ -426,9 +437,15 @@ 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\t%s\t%s\n" (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) said via
+ let preface =
+ match d.Colitur_kernel.Liturgical_day.preface with
+ | Some p -> Colitur_kernel.Preface.to_string p
+ | None -> "-"
+ in
+ Printf.printf "%s\t%s\t%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)
(string_of_bool d.Colitur_kernel.Liturgical_day.gloria)
+ preface
(* One civil year, Jan 1 - Dec 31, matching [temporal_report]'s own scan --
NOT one liturgical year: [Colitur_kernel.Calendar.year] resolves a single
@@ -1287,8 +1304,8 @@ output formats:
2026-04-05 sunday paschaltide 1 ef-easter-sunday class-1 white
readings date slug | Epistle | Gospel [| name]
2026-12-25 ef-nativity | Heb 1:1-12 | John 1:1-14
- rubrics date, formulary slug, source, creed, gloria -- TAB-separated
- 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true
+ rubrics date, formulary slug, source, creed, gloria, preface -- TAB-separated
+ 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true[TAB]nativity
A citation contains spaces, so readings uses " | " between its fields while
day stays space-separated; that is why they are separate commands rather
@@ -1305,16 +1322,20 @@ output formats:
(source: proper/own/preceding-sunday/common/votive) -- 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 -- followed by whether the
- Creed is said (RG 475-476) and whether the Gloria in excelsis is said
+ Creed is said (RG 475-476), whether the Gloria in excelsis is said
(RG 431-432, deferring to the Breviary's own Te Deum rule, nn. 237-238,
- for RG 431(a)) -- both "true"/"false", OCaml's own literal, not
- "yes"/"no" or "1"/"0". TAB-separated rather than space or " | ": a
- resolved formulary NAME is a column a later version may add, and it can
- carry both spaces and punctuation a citation never does, which rules out
+ for RG 431(a)), and which preface is said (RG 482-499) -- Creed/Gloria
+ both "true"/"false", OCaml's own literal, not "yes"/"no" or "1"/"0";
+ preface one of nativity/epiphany/lent/holy-cross/easter/ascension/
+ sacred-heart/christ-the-king/holy-spirit/trinity/bvm/st-joseph/apostles/
+ common/requiem, or "-" when this engine resolves no Mass at all that day
+ (Good Friday). TAB-separated rather than space or " | ": a resolved
+ formulary NAME is a column a later version may add, and it can carry
+ both spaces and punctuation a citation never does, which rules out
either alternative already in use above. --overlay is accepted (the
- observed celebration it changes decides the formulary, the Creed and the
- Gloria); --lang/--raw/--sigla-* are refused -- this row resolves no
- display name and no citation for any of them to affect.
+ observed celebration it changes decides the formulary, the Creed, the
+ Gloria and the preface); --lang/--raw/--sigla-* are refused -- this row
+ resolves no display name and no citation for any of them to affect.
emit one schema (season, week, slug, rank, colour, subject, names,
citations, commemorations), rendered five ways: csv (RFC 4180,
diff --git a/lib/kernel/calendar.ml b/lib/kernel/calendar.ml
index 7178780..a1309f9 100644
--- a/lib/kernel/calendar.ml
+++ b/lib/kernel/calendar.ml
@@ -554,6 +554,9 @@ let build_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.index)
let gloria =
rite.Rite.gloria ~temporal ~observed:resolution.Precedence.observed.Precedence.cel ~date
in
+ let preface =
+ rite.Rite.preface ~temporal ~observed:resolution.Precedence.observed.Precedence.cel ~date
+ in
{
Liturgical_day.date;
rite = rite.Rite.id;
@@ -568,6 +571,7 @@ let build_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.index)
formulary;
creed;
gloria;
+ preface;
}
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 98c5084..c3ecb37 100644
--- a/lib/kernel/liturgical_day.ml
+++ b/lib/kernel/liturgical_day.ml
@@ -28,5 +28,6 @@ type ('s, 'r) t = {
a rite with no lectionary -- see {!Mass_formulary} *)
creed : bool; (** whether the Creed is said at this day's Mass; see {!Rite.t.creed} *)
gloria : bool; (** whether the Gloria in excelsis is said; see {!Rite.t.gloria} *)
+ preface : Preface.t option; (** which preface is said; see {!Rite.t.preface} *)
}
[@@deriving sexp]
diff --git a/lib/kernel/liturgical_day.mli b/lib/kernel/liturgical_day.mli
index be17d73..643097b 100644
--- a/lib/kernel/liturgical_day.mli
+++ b/lib/kernel/liturgical_day.mli
@@ -51,5 +51,12 @@ type ('s, 'r) t = {
(** Whether the Gloria in excelsis is said at this day's Mass --
{!Rite.t.gloria}, EF: RG 431-432. Same seam as [creed] in every
respect. *)
+ preface : Preface.t option;
+ (** Which preface is said at this day's Mass -- {!Rite.t.preface},
+ EF: RG 482-499. Same seam as [creed]/[gloria], but [option], not
+ a bare {!Preface.t}: see {!Rite.t.preface}'s own citation for why
+ -- [None] both for a rite that has not implemented the rule and
+ for a real day this engine resolves that has no Mass at all
+ (Good Friday). *)
}
[@@deriving sexp]
diff --git a/lib/kernel/preface.ml b/lib/kernel/preface.ml
new file mode 100644
index 0000000..08d00b4
--- /dev/null
+++ b/lib/kernel/preface.ml
@@ -0,0 +1,59 @@
+(* RG 482-499 -- see the .mli for the rubric quoted in full and the
+ placement rationale. Argument-less variants only, so no
+ [Sexplib0.Sexp_conv] open is needed -- see CLAUDE.md's own gotcha note. *)
+type t =
+ | Nativity
+ | Epiphany
+ | Lent
+ | Holy_cross
+ | Easter
+ | Ascension
+ | Sacred_heart
+ | Christ_the_king
+ | Holy_spirit
+ | Trinity
+ | Bvm
+ | St_joseph
+ | Apostles
+ | Common
+ | Requiem
+[@@deriving sexp]
+
+let all =
+ [ Nativity; Epiphany; Lent; Holy_cross; Easter; Ascension; Sacred_heart; Christ_the_king;
+ Holy_spirit; Trinity; Bvm; St_joseph; Apostles; Common; Requiem ]
+
+let to_string = function
+ | Nativity -> "nativity"
+ | Epiphany -> "epiphany"
+ | Lent -> "lent"
+ | Holy_cross -> "holy-cross"
+ | Easter -> "easter"
+ | Ascension -> "ascension"
+ | Sacred_heart -> "sacred-heart"
+ | Christ_the_king -> "christ-the-king"
+ | Holy_spirit -> "holy-spirit"
+ | Trinity -> "trinity"
+ | Bvm -> "bvm"
+ | St_joseph -> "st-joseph"
+ | Apostles -> "apostles"
+ | Common -> "common"
+ | Requiem -> "requiem"
+
+let of_string = function
+ | "nativity" -> Some Nativity
+ | "epiphany" -> Some Epiphany
+ | "lent" -> Some Lent
+ | "holy-cross" -> Some Holy_cross
+ | "easter" -> Some Easter
+ | "ascension" -> Some Ascension
+ | "sacred-heart" -> Some Sacred_heart
+ | "christ-the-king" -> Some Christ_the_king
+ | "holy-spirit" -> Some Holy_spirit
+ | "trinity" -> Some Trinity
+ | "bvm" -> Some Bvm
+ | "st-joseph" -> Some St_joseph
+ | "apostles" -> Some Apostles
+ | "common" -> Some Common
+ | "requiem" -> Some Requiem
+ | _ -> None
diff --git a/lib/kernel/preface.mli b/lib/kernel/preface.mli
new file mode 100644
index 0000000..b43890e
--- /dev/null
+++ b/lib/kernel/preface.mli
@@ -0,0 +1,50 @@
+(** The Mass preface (Missale Romanum, Rubricae Generales, Caput XVII("De
+ Ritibus servandis in celebratione Missae"), "H) De praefatione", RG
+ 482-499; docs/research/LT.txt, grep "praefatione dicitur quae cuique").
+
+ RG 482 gives the resolution chain in full: {i "Praefatio dicitur quae
+ cuique Missae propria est; qua deficiente, dicitur praefatio de
+ Tempore, secus communis"} -- the Mass's own PROPER preface; failing
+ that, the SEASONAL ("de Tempore") one; failing that, the COMMON. RG
+ 483: {i "Nulla commemoratio, in Missa occurrens, praefationem propriam
+ inducit"} -- a commemoration never induces its own proper preface,
+ which is why {!Rite_ef.Rubrics_ef.preface} (the one rite that
+ implements this so far) reads only the day's OBSERVED celebration,
+ never its commemorations, the identical discipline {!Rite.t.creed}'s
+ own RG 476(e) already established.
+
+ RG 484-497 enumerate the fourteen named propers -- Nativity, Epiphany,
+ Lent, Holy Cross, [the Chrism Mass, out of scope: see the .ml's own
+ N/A note], Easter, Ascension, the Sacred Heart, Christ the King, the
+ Holy Spirit, the Trinity, the Blessed Virgin Mary, St Joseph, the
+ Apostles. RG 498 is the residual Common; RG 499 the Requiem.
+
+ A KERNEL type, not a rite-specific one (the same placement as
+ {!Colour}, {!Subject}, {!Mass_formulary}): {!Liturgical_day.t} is
+ parameterised only over a rite's season/rank, so any field it carries
+ generically must live here, even though only the EF rite constructs a
+ value of it today -- CLAUDE.md's own "generalise when forced, not
+ speculatively" stance, already the precedent {!Mass_formulary}
+ follows. *)
+
+type t =
+ | Nativity (** RG 484 *)
+ | Epiphany (** RG 485 *)
+ | Lent (** RG 486, "de Quadragesima" *)
+ | Holy_cross (** RG 487, "de sancta Cruce" *)
+ | Easter (** RG 489, "paschalis" *)
+ | Ascension (** RG 490 *)
+ | Sacred_heart (** RG 491, "de Ss.mo Corde Iesu" *)
+ | Christ_the_king (** RG 492, "de D. N. Iesu Christo Rege" *)
+ | Holy_spirit (** RG 493, "de Spiritu Sancto" *)
+ | Trinity (** RG 494, "de Ss.ma Trinitate" *)
+ | Bvm (** RG 495, "de beata Maria Virgine" *)
+ | St_joseph (** RG 496, "de S. Ioseph" *)
+ | Apostles (** RG 497, "de Apostolis" *)
+ | Common (** RG 498, "communis" -- the residual when nothing else applies *)
+ | Requiem (** RG 499, "defunctorum" *)
+[@@deriving sexp]
+
+val all : t list
+val to_string : t -> string
+val of_string : string -> t option
diff --git a/lib/kernel/rite.ml b/lib/kernel/rite.ml
index 915941d..e44c2c6 100644
--- a/lib/kernel/rite.ml
+++ b/lib/kernel/rite.ml
@@ -21,4 +21,5 @@ type ('s, 'r) t = {
Mass_formulary.t option * Citation.t list;
creed : temporal:('s, 'r) Temporal.t -> observed:'r Celebration.t -> date:Date.t -> bool;
gloria : temporal:('s, 'r) Temporal.t -> observed:'r Celebration.t -> date:Date.t -> bool;
+ preface : temporal:('s, 'r) Temporal.t -> observed:'r Celebration.t -> date:Date.t -> Preface.t option;
}
diff --git a/lib/kernel/rite.mli b/lib/kernel/rite.mli
index b9d1961..d53e4e6 100644
--- a/lib/kernel/rite.mli
+++ b/lib/kernel/rite.mli
@@ -140,4 +140,21 @@ type ('s, 'r) t = {
parameters and the same reasons for each, a [bool] not an
[option], and [false] is the answer a rite that has not
implemented the rule returns explicitly. *)
+ preface : temporal:('s, 'r) Temporal.t -> observed:'r Celebration.t -> date:Date.t -> Preface.t option;
+ (** Which preface is said at this day's Mass (EF: RG 482-499). Same
+ seam and same three parameters as {!creed}/{!gloria}, for the
+ same reasons -- {!Preface}'s own citation has the rubric in full.
+ [Preface.t option], not a bare [Preface.t]: unlike [creed]/
+ [gloria], where "not said" is itself a legitimate answer a
+ [bool] can carry, a preface is said only at a MASS, and this
+ engine constructs at least one day (Good Friday, the
+ 1955-restored Holy Week) that resolves an [observed] celebration
+ but has no Mass at all -- [None] is the honest answer there, not
+ a manufactured preface. [None] is also the value a rite that has
+ not implemented this rule returns, the same "the type's own
+ neutral value" contract {!creed}/{!gloria} give for [false] --
+ the two meanings ("this rite does not model the question" and
+ "this specific day has no Mass to preface") collapse onto the
+ same representation deliberately: a caller with no rite-specific
+ context to distinguish them should not need one. *)
}
diff --git a/lib/rites/rite_ef/rite_ef.ml b/lib/rites/rite_ef/rite_ef.ml
index d7b3d83..808e017 100644
--- a/lib/rites/rite_ef/rite_ef.ml
+++ b/lib/rites/rite_ef/rite_ef.ml
@@ -52,4 +52,5 @@ let context ~lectionary ~commons : (Vocab_ef.season, Vocab_ef.rank) Rite.t =
transfer_target = Precedence_ef.transfer_target;
readings = Lectionary_ef.readings ~lectionary ~commons;
creed = Rubrics_ef.creed;
- gloria = Rubrics_ef.gloria }
+ gloria = Rubrics_ef.gloria;
+ preface = Rubrics_ef.preface }
diff --git a/lib/rites/rite_ef/rite_ef.mli b/lib/rites/rite_ef/rite_ef.mli
index 7025143..2ed61de 100644
--- a/lib/rites/rite_ef/rite_ef.mli
+++ b/lib/rites/rite_ef/rite_ef.mli
@@ -39,6 +39,10 @@ module Rubrics_ef = Rubrics_ef
- [gloria]: {!Rubrics_ef.gloria}, RG 431-432 -- whether the Gloria in
excelsis is said. Reads {!Rubrics_ef.te_deum} (Breviary nn. 237-238)
for RG 431(a)/432(a)'s own deferral.
+ - [preface]: {!Rubrics_ef.preface}, RG 482-499 -- which preface is
+ said. Same seam as [creed]/[gloria]; [None] both for a day with no
+ Mass at all (Good Friday) and for a rite that has not implemented
+ the rule.
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
index a497b57..eea92a9 100644
--- a/lib/rites/rite_ef/rubrics_ef.ml
+++ b/lib/rites/rite_ef/rubrics_ef.ml
@@ -697,3 +697,499 @@ let gloria ~(temporal : (Vocab_ef.season, Vocab_ef.rank) Temporal.t)
Gloria mirrors [te_deum] exactly for every Mass not already decided
above. This is the ONE call site [te_deum] exists to serve. *)
te_deum ~temporal ~observed ~date
+
+(* Missale Romanum, Rubricae Generales, Caput XVII, "H) De praefatione" (RG
+ 482-499; docs/research/LT.txt, grep "praefatione dicitur quae cuique"),
+ quoted here in full so every branch below can cite its own paragraph
+ without re-quoting the whole rubric:
+
+ "482. Praefatio dicitur quae cuique Missae propria est; qua deficiente,
+ dicitur praefatio de Tempore, secus communis.
+
+ 483. Nulla commemoratio, in Missa occurrens, praefationem propriam
+ inducit.
+
+ 484. Praefatio de Nativitate Domini dicitur:
+ a) tamquam propria in Missis de Nativitate Domini et de eiusdem
+ octava, necnon in festo Purificationis B. Mariae Virg.;
+ b) tamquam de Tempore, infra octavam Nativitatis Domini, etiam in
+ Missis quae secus praefationem propriam haberent, exceptis iis Missis
+ quae praefationem propriam de divinis mysteriis vel Personis habent; et
+ a die 2 ad 5 ianuarii.
+
+ 485. Praefatio de Epiphania Domini dicitur:
+ a) tamquam propria in Missis de festo Epiphaniae et de
+ Commemoratione Baptismatis D. N. Iesu Christi;
+ b) tamquam de Tempore diebus a 7 ad 13 ianuarii.
+
+ 486. Praefatio de Quadragesima dicitur:
+ a) tamquam propria in Missis de Tempore a feria IV cinerum usque ad
+ sabbatum ante dominicam I Passionis;
+ b) tamquam de Tempore in ceteris Missis quae celebrantur eodem
+ tempore, et praefatione propria carent.
+
+ 487. Praefatio de sancta Cruce dicitur:
+ a) tamquam propria in Missis de tempore a dominica I Passionis usque
+ ad feriam V in Cena Domini; in Missis tam festivis quam votivis de
+ sancta Cruce, de Passione Domini et instrumentis Passionis Domini, de
+ pretiosissimo Sanguine D. N. Iesu Christi, de Ss.mo Redemptore;
+ b) tamquam de Tempore in omnibus Missis a dominica I Passionis usque
+ ad feriam IV Hebdomadae sanctae, quae praefatione propria carent.
+
+ 488. Praefatio de Missa chrismatis dicitur feria V in Cena Domini, in
+ sua Missa.
+
+ 489. Praefatio paschalis dicitur:
+ a) tamquam propria in Missis de Tempore a Missa Vigiliae paschalis
+ usque ad vigiliam Ascensionis Domini;
+ b) tamquam de Tempore in ceteris Missis quae celebrantur eodem
+ tempore, et praefatione propria carent.
+
+ 490. Praefatio de Ascensione Domini dicitur:
+ a) tamquam propria in festo Ascensionis Domini;
+ b) tamquam de Tempore in omnibus Missis a feria VI post Ascensionem
+ usque ad feriam VI ante vigiliam Pentecostes, quae praefatione propria
+ carent.
+
+ 491. Praefatio de Ss.mo Corde Iesu dicitur in Missis festivis et
+ votivis de Ss.mo Corde Iesu.
+
+ 492. Praefatio de D. N. Iesu Christo Rege dicitur in Missis festivis
+ et votivis de D. N. Iesu Christo Rege.
+
+ 493. Praefatio de Spiritu Sancto dicitur:
+ a) tamquam propria in Missis de Tempore a vigilia Pentecostes usque
+ ad subsequens sabbatum; et in Missis festivis et votivis de Spiritu
+ Sancto;
+ b) tamquam de Tempore in ceteris Missis quae celebrantur eodem
+ tempore, et praefatione propria carent.
+
+ 494. Praefatio de Ss.ma Trinitate dicitur:
+ a) tamquam propria in Missis de festo et votivis Ss.mae Trinitatis;
+ b) tamquam de Tempore in dominicis Adventus, et in omnibus dominicis
+ II classis, extra tempus natalicium et paschale.
+
+ 495. Praefatio de beata Maria Virgine dicitur in Missis festivis et
+ votivis beatae Mariae Virginis, praeterquam in festo Purificationis B.
+ Mariae Virg.
+
+ 496. Praefatio de S. Ioseph dicitur in Missis festivis et votivis S.
+ Ioseph.
+
+ 497. Praefatio de Apostolis dicitur in Missis festivis et votivis
+ Apostolorum et Evangelistarum.
+
+ 498. Praefatio communis dicitur in Missis quae praefatione propria
+ carent, nec sumere debent praefationem de Tempore.
+
+ 499. Praefatio defunctorum dicitur in Missis defunctorum."
+
+ THE SHAPE OF THE RULE, once, rather than at every branch: RG 482's own
+ chain is "propria, else de Tempore, else communis". Read literally, 484-
+ 497 look like FOURTEEN SEPARATE RULES, but on inspection each numbered
+ rubric's own (a)/(b) pair (where it has both) produces the SAME preface
+ identity either way -- (a) is the propria reading ("this Mass's OWN
+ preface"), (b) is the de-Tempore reading ("this OTHER Mass, lacking one
+ of its own, borrows it") -- so for the single question this function
+ answers (WHICH preface, not WHETHER it counts as propria or de Tempore
+ for some other purpose) the two halves collapse into one PRIORITY-
+ ORDERED decision: a fixed list of "genuinely proper" triggers (title/
+ mystery feasts, independent of season), checked first in a citable
+ order, falling through to a fixed list of SEASONAL windows, falling
+ through to [Common]. RG 483 (a commemoration never induces a proper)
+ holds by construction, the same way [creed]'s own 476(e) does: every
+ branch below reads only [observed], never a day's admitted
+ commemorations.
+
+ THE PRIORITY ORDER ITSELF was cross-checked against 358 real,
+ individually classifiable entries in the FIUV Ordo's own [praef] column
+ (test/fixtures/fiuv-ordo-2025-2026.sexp, test_fiuv_ordo.ml) spanning the
+ WHOLE liturgical year -- not merely derived from the Latin text in
+ isolation. Two findings the plain text alone would not have settled,
+ both empirically confirmed rather than assumed:
+
+ - 484(b)'s own "exceptis iis Missis quae praefationem propriam de
+ divinis mysteriis vel Personis habent" is NARROWER than every other
+ window's implicit "unless it already has a genuine proper" -- St
+ John the Evangelist (27 December, on {!creed_apostle_slugs}, so his
+ OWN Apostles preface (497) would otherwise apply) is overridden to
+ [Nativity] inside the octave (confirmed: the Ordo's own 27 December
+ entry reads "de Nativ.", not "App."), while St Barnabas/Sts Philip &
+ James/the other Apostles OUTSIDE the octave keep their own Apostles
+ preface even inside another window (Sts Philip & James, 11 May,
+ inside the Easter window: confirmed "App." in the Ordo, not
+ "Pasch."). So [Apostles] is checked AFTER the Nativity window below,
+ but every OTHER title trigger (Holy Cross/Sacred Heart/Christ the
+ King/Trinity/St Joseph/BVM) is checked BEFORE it -- RG 484(b)'s own
+ narrower carve-out, read literally: unreachable on the shipped
+ calendar for the other five (no such feast falls 25 December-5
+ January), so this ordering is defensive for them, not observed live,
+ the same "checked, not merely assumed" discipline
+ {!Precedence_ef.marian_slugs}'s own citation follows elsewhere.
+ - RG 495's own "et votivis" half is live on this engine's data after
+ all, for the ONE office this project already models as a votive-
+ shaped Mass without a votive-Mass DIMENSION (RG 78/91 entry 27, the
+ Saturday Office of the BVM, {!Temporal_ef}'s own [subject = Bvm]
+ tag): confirmed directly (3 January and 10 January 2026, both the
+ Saturday Office, both read "BMV" in the Ordo) -- including on 3
+ January, itself inside the Nativity's own "2 ad 5 ianuarii" de-
+ Tempore window, where BVM still wins, corroborating the same
+ "genuine propria outranks every window" ordering the Apostles
+ finding above established from the opposite direction.
+
+ Good Friday's own printed [praef] text ("comm. Feria VI prima in
+ mense.") was NOT used to check this function's own [None] answer for
+ that day: {!test_fiuv_ordo.ml}'s own F1 (Gloria) already adjudicated
+ this exact date's raw text as unreliable (a copied, not a considered,
+ line -- see that allow-list entry's own citation for the full argument,
+ confirmed against the PDF's own page image, not merely the extracted
+ fixture) -- the same defect, read again, would apply equally to
+ whatever trails "praef." on the identical corrupted line, so this
+ function's [None] rests on RG 28's own "no Mass" structural argument
+ alone (the same argument [creed]/[gloria]/[te_deum] already give for
+ this date), not on any Ordo corroboration. *)
+
+(* RG 487(a): the two GENUINE fixed-date feast triggers in the shipped
+ universal calendar -- the Exaltation of the Holy Cross (14 September)
+ and the Most Precious Blood (1 July), both [subject = Lord]. RG 487(a)'s
+ own further-named categories ("de Passione Domini et instrumentis
+ Passionis Domini, de Ss.mo Redemptore") have NO corresponding entry
+ anywhere in data/ef/sanctoral.sexp (checked directly, grepping for
+ "instrument"/"redeem": zero hits) -- genuinely absent from the shipped
+ 1962 universal calendar, not merely unmatched by this list, so they are
+ N/A rather than silently unreachable. *)
+let preface_holy_cross_slugs =
+ [ "exaltation-of-the-holy-cross"; "precious-blood-of-our-lord-jesus-christ" ]
+
+(* RG 496: the two St Joseph feasts in the shipped calendar (19 March, 1
+ May) -- both [subject = Saint], so (unlike RG 495's own BVM feasts) no
+ [subject]-based fallback exists or is needed; this closed list is the
+ whole of what RG 496 can ever reach on shipped data. *)
+let preface_st_joseph_slugs = [ "joseph-spouse-of-the-bl-virgin-mary"; "joseph-the-workman" ]
+
+(* RG 497's own [Apostolorum et Evangelistarum] population is WIDER than
+ {!creed_apostle_slugs}: RG 475(e) is restricted to a NATALICIUM ("festis
+ NATALICIIS Apostolorum...", that module's own citation), but RG 497 has
+ no such restriction at all ("in Missis festivis et votivis Apostolorum
+ et Evangelistarum" -- ANY festive/votive Mass of an Apostle or
+ Evangelist). FOUND, not assumed: the FIUV Ordo's own 30 June entry
+ ("In Commemoratione S. Pauli Ap.", data/ef/adjustments.sexp's own RG
+ 110(c) [Add], {!Precedence_ef}'s own citation -- a genuine [Feast]-
+ status office of Paul the Apostle, but NOT his own dies natalis, so
+ {!creed_apostle_slugs} deliberately excludes it) reads "App. I", not
+ "comm." -- checked directly while building this comparison, not
+ guessed. [creed_apostle_slugs] itself is UNCHANGED (RG 475(e)'s own
+ narrower "natalicium" reading still holds for the Creed); this is a
+ SEPARATE, wider list for RG 497 alone.
+
+ "conversion-of-st-paul" (25 January, Class3, the SAME "not a
+ natalicium" shape {!creed_apostle_slugs}'s own citation excludes it
+ for) is a plausible SECOND candidate by the identical RG 497 reasoning
+ -- deliberately NOT added: it is UNWITNESSED (25 January falls on a
+ Sunday, hence impeded, in the one fixture year this engine's Ordo
+ evidence covers, {!test_fiuv_ordo.ml}'s own window), and "a wrong
+ citation is worse than a missing one" ({!Precedence_ef.marian_slugs}'s
+ own citation, the precedent this follows). Left for a future fixture
+ year to confirm or refute. *)
+let preface_apostle_slugs = "in-commemoratione-sancti-pauli-apostoli" :: creed_apostle_slugs
+
+(* RG 495's own [beatae Mariae Virginis] population is also WIDER than
+ {!Precedence_ef.marian_slugs}: that list was built for a DIFFERENT
+ rubric (RG 112(d), whether a commemoration invokes HER OWN
+ intercession specifically) with a correspondingly narrower, oration-
+ checked standard, and its own citation explicitly EXCLUDES "dedication-
+ of-the-basilica-of-st-mary-major" (5 August) for exactly that reason --
+ "whose own oration could not be found... to confirm it invokes her
+ intercession". RG 495 asks a different, WIDER question ("is this Mass
+ festive or votive OF the Blessed Virgin Mary at all"), which the
+ Dedication of St Mary Major answers on its own title alone, without
+ needing the oration-level standard RG 112(d) requires. FOUND, not
+ assumed: the FIUV Ordo's own 5 August entry reads "BMV Et te in
+ Festivitate.", not "comm." -- checked directly. *)
+let preface_bvm_slugs = "dedication-of-the-basilica-of-st-mary-major" :: Precedence_ef.marian_slugs
+
+let preface ~(temporal : (Vocab_ef.season, Vocab_ef.rank) Temporal.t)
+ ~(observed : Vocab_ef.rank Celebration.t) ~(date : Date.t) : Preface.t option =
+ 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 28-34/RG 23(b), the same structural "no Mass at all" position
+ [creed]'s own vigil comment and [gloria]'s own 432(d) comment both
+ take for Good Friday specifically: the 1955-restored Holy Week has
+ no Mass whatsoever that day (only the afternoon liturgical action),
+ so there is no Mass to preface. Checked ahead of the
+ {!Colour.Black} Requiem proxy immediately below -- unlike All
+ Souls, Good Friday sharing that colour is coincidental, not
+ diagnostic (temporal_ef.ml's own RG 132 citation), and the two need
+ DIFFERENT answers here (unlike [creed]/[gloria]/[te_deum], where
+ both collapse to the same boolean) -- so this function cannot reuse
+ their shared single guard and must split Good Friday out first. *)
+ n = -2
+ then None
+ else if
+ (* RG 499: "in Missis defunctorum" -- the same {!Colour.Black} proxy
+ [creed]'s own 476(f), [te_deum]'s own 238(d) and [gloria]'s own
+ 432(d) already use (this file's own header has the full argument
+ and the two-member population this proxy rests on). With Good
+ Friday split out above, the one remaining member is All Souls. *)
+ observed.Celebration.colour = Colour.Black
+ then Some Preface.Requiem
+ else if
+ (* RG 487(a)'s own fixed-feast half -- checked first among the title
+ triggers per this file's own header (arbitrary among these six,
+ since none can ever co-occur with another on shipped data; Holy
+ Cross is placed first only because it is also the anchor for the
+ Passiontide WINDOW checked later below, keeping both citations
+ adjacent in this file). *)
+ List.mem slug preface_holy_cross_slugs
+ then Some Preface.Holy_cross
+ else if
+ (* RG 491: "in Missis festivis... de Ss.mo Corde Iesu" -- the Friday
+ after the Octave of Corpus Christi (Easter+68), {!Temporal_ef}'s own
+ named slug. Confirmed against the Ordo (12 June 2026: "de Ss.mi
+ Corde Iesu"). *)
+ slug = "ef-sacred-heart"
+ then Some Preface.Sacred_heart
+ else if
+ (* RG 492: "in Missis festivis... de D. N. Iesu Christo Rege" -- the
+ last Sunday of October, {!Temporal_ef.christ_the_king}'s own named
+ slug. Confirmed against the Ordo (25 October 2026: "de Domino
+ Nostro Jesu Rege"). *)
+ slug = "ef-christ-the-king"
+ then Some Preface.Christ_the_king
+ else if
+ (* RG 490(a): "in festo Ascensionis Domini" -- the feast itself
+ (Easter+39), checked here by slug rather than folded into the
+ Ascension WINDOW below (which starts the day AFTER, Easter+40):
+ Ascension Day itself needs no window at all, its own slug already
+ identifies it uniquely. Confirmed against the Ordo (14 May 2026:
+ "Ascensionis, Communic pr."). *)
+ slug = "ef-ascension"
+ then Some Preface.Ascension
+ else if
+ (* RG 494(a): "in Missis de festo... Ss.mae Trinitatis" -- Trinity
+ Sunday itself (Easter+56), {!Temporal_ef}'s own named slug. Checked
+ ahead of 494(b)'s own WIDER de-Tempore grant (checked last below,
+ after every other window) for the same "specific propria before any
+ season fallback" reason every other title trigger is. Confirmed
+ against the Ordo (31 May 2026: "Trinit. II"). *)
+ slug = "ef-trinity"
+ then Some Preface.Trinity
+ else if
+ (* RG 496: see {!preface_st_joseph_slugs}'s own citation. *)
+ List.mem slug preface_st_joseph_slugs
+ then Some Preface.St_joseph
+ else if
+ (* RG 495: "in Missis festivis et votivis beatae Mariae Virginis" --
+ {!preface_bvm_slugs} (its own citation has the full account of why
+ it is wider than {!Precedence_ef.marian_slugs}) covers every
+ genuine Marian FEAST; [subject = Bvm] covers the one VOTIVE-shaped
+ office this engine models without a votive-Mass dimension of its
+ own (RG 78/91 entry 27, the Saturday Office of the BVM -- this
+ file's own header has the empirical confirmation, 3/10 January
+ 2026). The Purification is deliberately ABSENT from both:
+ {!Precedence_ef.marian_slugs} already excludes it by name (its own
+ citation), and it never carries [subject = Bvm] (tagged [Lord]
+ instead, register ยง6.0) -- RG 495's own "praeterquam in festo
+ Purificationis" exclusion therefore holds by construction, not by
+ a guard written here.
+
+ [not (is_vigil slug)]: RG 495's own "festivis" reads "festum", not
+ "vigilia" -- the SAME RG 21/35 taxonomy distinction {!creed}'s own
+ RG 28-34 comment already makes ("a vigil is its OWN liturgical-day
+ category, distinct from 'festum'"), applied here for the first
+ time in THIS function because it is the first branch a vigil can
+ actually reach: {!Precedence_ef.marian_slugs} includes
+ "vigil-of-the-assumption" (that list's own citation), which without
+ this guard would wrongly claim [Bvm] for 14 August. FOUND, not
+ assumed: the FIUV Ordo's own 14 August entry reads "comm. I", not
+ "BMV" -- checked directly, the same as every other finding in this
+ branch's own history. Corroborates, from the opposite direction,
+ {!creed}'s own RG 28-34 comment: colitur's own Nativity WINDOW
+ below already excludes 24 December (the Nativity Vigil) by
+ construction (it starts at 25 December, never 24th), so this guard
+ makes the SAME "a vigil is not a festum" answer explicit here too,
+ rather than relying on a second, unrelated accident of a date
+ range to produce it. *)
+ (not (Precedence_ef.is_vigil slug))
+ && (List.mem slug preface_bvm_slugs || observed.Celebration.subject = Subject.Bvm)
+ then Some Preface.Bvm
+ else if
+ (* RG 484(a)'s own explicit Purification clause ("necnon in festo
+ Purificationis B. Mariae Virg.") -- 2 February, nowhere near the
+ Nativity's own Christmas-to-Epiphany calendar position, so this is
+ a standalone slug check, not part of the WINDOW test below (unlike
+ every other 484 trigger, which IS date-based). Checked here, after
+ the BVM check immediately above (which the Purification's own
+ [subject = Lord] tag never reaches) and before the Nativity window
+ (which its own actual date, 2 February, never reaches either) --
+ positioned with the rest of 484's own citations for readability,
+ not because anything below could otherwise pre-empt it. *)
+ slug = "purification-of-the-blessed-virgin-mary"
+ then Some Preface.Nativity
+ else if
+ (* RG 484(a)/(b) merged, per this file's own header: 25 December-1
+ January (the Nativity itself and its octave, propria) UNION 2-5
+ January (498(b)'s own explicit extra de-Tempore days) -- one
+ contiguous window, since both halves produce the identical
+ preface. Checked BEFORE Apostles (below) but AFTER every genuine
+ "divine mysteries/Persons" propria above, per this file's own
+ header (St John the Evangelist, 27 December, is the live witness:
+ Apostles would otherwise apply and does not). *)
+ (m = 12 && dd >= 25) || (m = 1 && dd <= 5)
+ then Some Preface.Nativity
+ else if
+ (* RG 497: "in Missis festivis et votivis Apostolorum et
+ Evangelistarum" -- {!preface_apostle_slugs} (its own citation has
+ the full account of why it is wider than {!creed_apostle_slugs}),
+ confirmed by this file's own header to produce the SAME preface
+ answer as the Ordo on every Apostle date outside the Nativity
+ octave: 11 June (Barnabas), 29-30 June (Peter & Paul, In
+ Commemoratione Pauli), 11 May (Philip & James, RG 484(b)'s own
+ witness against the Nativity window immediately above). Checked
+ AFTER the Nativity window specifically (RG 484(b)'s own narrower
+ carve-out), but before every OTHER season window below -- an
+ Apostle feast keeps his own preface inside Lent, Passiontide,
+ Paschaltide etc., where nothing narrows the exception the way
+ 484(b) does. No [is_vigil] guard is needed here the way RG 495's
+ own branch above needs one: checked directly, no entry on
+ {!preface_apostle_slugs} is ever a vigil slug (every Apostle vigil
+ in the shipped data -- "vigil-of-sts-peter-paul" -- carries its own
+ distinct slug, absent from this list). *)
+ List.mem slug preface_apostle_slugs
+ then Some Preface.Apostles
+ else if
+ (* RG 485(a): "in Missis de festo Epiphaniae et de Commemoratione
+ Baptismatis D. N. Iesu Christi" -- the feast itself and its own
+ named commemoration (13 January, {!Precedence_ef}'s own
+ "commemoration-of-the-baptism-of-the-lord" -- {!creed}'s own 475(c)
+ comment already documents this entry's [subject = Lord] tag), both
+ checked by slug so 485(b)'s own WIDER window below need not repeat
+ them. *)
+ slug = "ef-epiphany" || slug = "commemoration-of-the-baptism-of-the-lord"
+ then Some Preface.Epiphany
+ else if
+ (* RG 485(b): "diebus a 7 ad 13 ianuarii" -- every OTHER Mass in this
+ window (Holy Family Sunday, an ordinary Time-after-Epiphany feria
+ or Sunday, a saint's feast with no propria of its own), confirmed
+ against the Ordo's own 11 January 2026 entry (Holy Family Sunday:
+ "de Epiphania. II", not a Holy-Family-specific preface -- this
+ engine has none to offer it anyway). Colitur's own Christmastide
+ season already spans 25 December-13 January (RG 72-73,
+ {!Vocab_ef.season}'s own citation), so this window is exactly its
+ OWN post-Epiphany tail; written as an explicit date range rather
+ than a season test only because the Nativity window above already
+ claims the season's FIRST half by date, not by season either, for
+ symmetry. *)
+ m = 1 && dd >= 6 && dd <= 13
+ then Some Preface.Epiphany
+ else if
+ (* RG 486(a)/(b) merged: Ash Wednesday (Easter-46) through the
+ Saturday before Passion Sunday I (Easter-15) inclusive -- every
+ Lenten feria/Sunday's own Mass (a), and every OTHER Mass in the
+ same span lacking a proper of its own (b). Confirmed against the
+ Ordo throughout (e.g. 18 February/19-20 February 2026: "Quadr.").
+ {!creed}'s own RG 23 comment already explains why Ash Wednesday
+ (feria I classis) reaches this branch on [observed]'s own terms
+ regardless of rank -- this function reads no rank at all here,
+ only the date. *)
+ n >= -46 && n <= -15
+ then Some Preface.Lent
+ else if
+ (* RG 487(a)/(b) merged: Passion Sunday I (Easter-14) through Holy
+ Thursday (Easter-3) inclusive -- (a)'s own "de tempore"/festive-
+ votive half extends through Holy Thursday itself (the Mass of the
+ Lord's Supper), (b)'s own narrower saint-Mass half stops one day
+ earlier (Holy Wednesday) but reaches no LIVE day this check does
+ not already cover identically (Holy Thursday is a feria I classis,
+ RG 23(b), so no saint's feast can ever occupy it -- {!creed}'s own
+ RG 23 citation). Confirmed against the Ordo throughout (22 March
+ 2026, Passion Sunday: "de Sancta Cruce."; 2 April 2026, Holy
+ Thursday: "de Sancta Cruce, Communicantes..."). *)
+ n >= -14 && n <= -3
+ then Some Preface.Holy_cross
+ else if
+ (* RG 489(a)/(b) merged: the Easter Vigil Mass (Easter-1, on Holy
+ Saturday's own date) through the vigil of the Ascension (Easter+38)
+ inclusive. [n = -1] is this engine's own OVERLOADED representation
+ of "the Vigil Mass", not Holy Saturday's daytime (which has no Mass
+ of its own at all, unlike Good Friday's [n = -2] this function
+ excludes by name above) -- [gloria]'s own RG 431(c) comment already
+ establishes the same convention for the identical date, and RG
+ 489(a) resolves the question on its own terms regardless: the
+ Paschal preface's window STARTS at the Vigil Mass, so [n = -1] is
+ correctly [Easter]. Confirmed against the Ordo throughout (5 April
+ 2026, Easter Sunday: "Pasch."; 13 May 2026, the Ascension Vigil:
+ "Pasch. I"); the fixture prints nothing at all for Holy Saturday's
+ own daytime square (4 April 2026), corroborating rather than
+ contradicting this reading -- see test_fiuv_ordo.ml's own citation. *)
+ n >= -1 && n <= 38
+ then Some Preface.Easter
+ else if
+ (* RG 490(b): "a feria VI post Ascensionem usque ad feriam VI ante
+ vigiliam Pentecostes" -- the Friday after Ascension (Easter+40)
+ through the Friday before the Pentecost vigil (Easter+47)
+ inclusive; Ascension Day itself (Easter+39) is already handled by
+ its own slug check above, not repeated here. Confirmed against the
+ Ordo throughout (15-22 May 2026: "Ascensionis"). *)
+ n >= 40 && n <= 47
+ then Some Preface.Ascension
+ else if
+ (* RG 493(a)/(b) merged: the vigil of Pentecost (Easter+48) through
+ "subsequens sabbatum" (the FOLLOWING Saturday, i.e. the Ember
+ Saturday within the Octave of Pentecost, Easter+55) inclusive.
+ Confirmed against the Ordo throughout (23-24 May 2026, the vigil
+ and Pentecost itself: "de Spirito Sancto"; 30 May 2026, the Ember
+ Saturday: "de Spirito Sancto"). *)
+ n >= 48 && n <= 55
+ then Some Preface.Holy_spirit
+ else if
+ (* RG 494(b): "in dominicis Adventus, et in omnibus dominicis II
+ classis, extra tempus natalicium et paschale". Read off
+ [temporal]'s own season, NOT [observed]'s rank -- CORRECTED from an
+ earlier version of this branch that DID read [observed.rank] and
+ required it to equal [Class2], which is wrong for the identical RG
+ 16(a) reason {!Precedence.rules.admit}'s own [~temporal] parameter
+ exists and [creed]'s own 237(b)/475(a) comments already give: a
+ feast that has WON the day can carry a different rank than the
+ Sunday it stands on. FOUND, not assumed: All Saints' Day (1
+ November), Class1, observed outright over an ordinary
+ Time-after-Pentecost Sunday it commemorates
+ ([+ef-time-after-pentecost-sunday-23]), reads "Trinit." in the
+ Ordo -- [observed.rank] there is [Class1], so the OLD guard wrongly
+ answered [Common]; there is no dedicated preface for All Saints
+ among RG 484-497's own fourteen, so RG 482's chain correctly falls
+ through to the SUNDAY's own de-Tempore grant regardless of which
+ rank actually won the day.
+
+ {!Temporal_ef.temporal}'s own [match s with Advent | Lent -> Class1
+ | _ -> Class2] means EVERY Sunday's own TEMPORAL identity is
+ [Class2] except in Advent and Lent -- so "in omnibus dominicis II
+ classis" and "in dominicis Adventus" collapse into ONE test, "any
+ Sunday outside Christmastide and Paschaltide" (Lent's own Sundays
+ need no explicit exclusion here: {!creed}'s own RG 23/Lent-window
+ reasoning already means every one of them is claimed by the LENT
+ window earlier in this very priority chain, provably unreachable
+ here, the same "checked, not merely assumed" position the previous
+ version of this comment already took for Christmastide/Paschaltide
+ -- confirmed by the SAME domain sweep in test_rubrics_ef.ml, which
+ still finds zero Christmastide/Paschaltide/Lent days reaching this
+ branch after this change). Confirmed against the Ordo throughout
+ (e.g. every Advent/Time-after-Epiphany/Septuagesima/Time-after-
+ Pentecost Sunday not otherwise claimed: "Trinit."), now including
+ All Saints' Day itself. *)
+ temporal.Temporal.weekday = Date.Sun
+ && temporal.Temporal.season <> Vocab_ef.Christmastide
+ && temporal.Temporal.season <> Vocab_ef.Paschaltide
+ then Some Preface.Trinity
+ else
+ (* RG 498: "in Missis quae praefatione propria carent, nec sumere
+ debent praefationem de Tempore" -- everything else: an ordinary
+ weekday feria outside every window above, a plain sanctoral saint
+ with no title of his own, an ordinary (non-Sunday, non-Class2, or
+ Christmastide/Paschaltide) day. Confirmed against the Ordo
+ throughout (the single most common value in the fixture, 188 of
+ 360 comparable rows). *)
+ Some Preface.Common
diff --git a/lib/rites/rite_ef/rubrics_ef.mli b/lib/rites/rite_ef/rubrics_ef.mli
index 618af65..befbf72 100644
--- a/lib/rites/rite_ef/rubrics_ef.mli
+++ b/lib/rites/rite_ef/rubrics_ef.mli
@@ -68,3 +68,38 @@ val gloria :
observed:Vocab_ef.rank Celebration.t ->
date:Date.t ->
bool
+
+(** RG 487(a)'s own two fixed-feast triggers for the Holy Cross preference
+ (the Exaltation of the Holy Cross, the Most Precious Blood) -- see the
+ .ml's own citation for what RG 487(a)'s further-named categories (the
+ Passion/Instruments of the Passion, the Most Holy Redeemer) are absent
+ from the shipped calendar entirely, not merely from this list. *)
+val preface_holy_cross_slugs : string list
+
+(** RG 496's own two St Joseph feasts in the shipped calendar. *)
+val preface_st_joseph_slugs : string list
+
+(** RG 497's own [Apostolorum et Evangelistarum] population -- WIDER than
+ {!creed_apostle_slugs} (no natalicium restriction); see the .ml's own
+ citation for the FIUV Ordo evidence and what was deliberately left
+ off, unwitnessed. *)
+val preface_apostle_slugs : string list
+
+(** RG 495's own [beatae Mariae Virginis] population -- WIDER than
+ {!Precedence_ef.marian_slugs} (a different, looser standard than that
+ list's own RG 112(d) oration-level one); see the .ml's own citation
+ for the FIUV Ordo evidence. *)
+val preface_bvm_slugs : string list
+
+(** Which preface is said at this day's Mass (RG 482-499). [None] both when
+ this engine resolves a day with no Mass at all (Good Friday) and -- for
+ a rite that has not implemented this function at all -- as the type's
+ own neutral value; see the .ml's own header for the rubric quoted in
+ full, the priority order every branch follows and why, and the FIUV
+ Ordo evidence that order rests on. Same three parameters as {!creed}/
+ {!te_deum}/{!gloria}, for the same reasons. *)
+val preface :
+ temporal:(Vocab_ef.season, Vocab_ef.rank) Temporal.t ->
+ observed:Vocab_ef.rank Celebration.t ->
+ date:Date.t ->
+ Preface.t option
diff --git a/man/colitur.1 b/man/colitur.1
index 88c5fab..3f485d5 100644
--- a/man/colitur.1
+++ b/man/colitur.1
@@ -122,13 +122,14 @@ occurrence, commemoration and transfer.
The Mass reading citations, one line per day.
.TP
.BI rubrics " YEAR"
-Three rubrics of the Mass, one line per day: which formulary is actually
+Four 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 whether the Creed is said (RG 475\-476), and
+unoccupied office's own \(em whether the Creed is said (RG 475\-476),
whether the Gloria in excelsis is said (RG 431\-432, deferring to the
-Breviary's own Te Deum rule, nn. 237\-238, for RG 431(a)). See
+Breviary's own Te Deum rule, nn. 237\-238, for RG 431(a)), and which
+preface is said (RG 482\-499). See
.B OUTPUT FORMAT
below.
.TP
@@ -461,13 +462,14 @@ own trailing field, above.
.SS rubrics
.RS
.nf
-date [TAB] formulary\-slug [TAB] source [TAB] creed [TAB] gloria
+date [TAB] formulary\-slug [TAB] source [TAB] creed [TAB] gloria [TAB] preface
.fi
.RE
.PP
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) and
-whether the Gloria in excelsis is said (RG 431\-432).
+that was decided), followed by whether the Creed is said (RG 475\-476),
+whether the Gloria in excelsis is said (RG 431\-432), and which preface is
+said (RG 482\-499).
.B rubrics
separates its fields with a literal TAB \(em not a plain space like
.B day
@@ -505,12 +507,25 @@ 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 a rule reads
.B false
outright \(em it is a decision, never a third \(lqunknown\(rq state.
+.I preface
+is one of
+.BR nativity ", " epiphany ", " lent ", " holy\-cross ", " easter ", "
+.BR ascension ", " sacred\-heart ", " christ\-the\-king ", " holy\-spirit ", "
+.BR trinity ", " bvm ", " st\-joseph ", " apostles ", " common " or " requiem ,
+or a literal
+.B \-
+when this engine resolves no Mass at all that day (Good Friday) \(em unlike
+.I creed / gloria ,
+.I preface
+is a genuine option, so
+.B \-
+here can also mean a rite that has not implemented the rule at all.
.RS
.nf
-2026\-01\-01 [TAB] ef\-circumcision [TAB] own [TAB] true [TAB] true
-2038\-03\-08 [TAB] john\-of\-god [TAB] proper [TAB] false [TAB] true
-2025\-12\-01 [TAB] ef\-advent\-sunday\-1 [TAB] preceding\-sunday [TAB] false [TAB] false
+2026\-01\-01 [TAB] ef\-circumcision [TAB] own [TAB] true [TAB] true [TAB] nativity
+2038\-03\-08 [TAB] john\-of\-god [TAB] proper [TAB] false [TAB] true [TAB] common
+2025\-12\-01 [TAB] ef\-advent\-sunday\-1 [TAB] preceding\-sunday [TAB] false [TAB] false [TAB] common
.fi
.RE
.PP
diff --git a/test/cli.t b/test/cli.t
index 8bd5b9f..4ae116f 100644
--- a/test/cli.t
+++ b/test/cli.t
@@ -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 true true
- 2026-01-02 ef-christmas-1-friday own false true
- 2026-01-03 ef-christmas-1-saturday votive false true
+ 2026-01-01 ef-circumcision own true true nativity
+ 2026-01-02 ef-christmas-1-friday own false true nativity
+ 2026-01-03 ef-christmas-1-saturday votive false true bvm
$ 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 false true
+ 2038-03-08 john-of-god proper false true common
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 false true
+ 2038-03-06 common-of-non-virgins-1 common false true common
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 false false
+ 2025-12-01 ef-advent-sunday-1 preceding-sunday false false common
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 false true
+ 2026-07-11 ef-time-after-pentecost-sunday-6 preceding-sunday false true common
$ colitur rubrics 2026 | grep '^2026-07-11'
- 2026-07-11 ef-time-after-pentecost-6-saturday votive false true
+ 2026-07-11 ef-time-after-pentecost-6-saturday votive false true bvm
`--lang`/`--raw`/`--sigla-*` are refused rather than silently ignored, unlike
`readings`: this row resolves no display name and no citation for any of
@@ -343,13 +343,13 @@ prints for the identical day, so the two cannot silently drift apart
again in either direction:
$ colitur --help | grep '^ rubrics date'
- rubrics date, formulary slug, source, creed, gloria -- TAB-separated
+ rubrics date, formulary slug, source, creed, gloria, preface -- TAB-separated
$ colitur --help | sed -n '/^ rubrics date/{n;p}'
- 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true
+ 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true[TAB]nativity
$ colitur rubrics 2026 | grep '^2026-01-01' | sed $'s/\t/[TAB]/g'
- 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true
+ 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true[TAB]nativity
--version prints the version alone, to standard output, exit 0. Deliberately
not embedded in the help text above: this pin would then have to be edited
@@ -589,8 +589,17 @@ addition: 2027-01-01's own record wraps [gloria] onto its own new line,
while 2027-01-02's fits it on the same line as [creed] and [formulary] --
172 of 365 records happened to cross a wrap boundary, the rest did not.
+9197 -> 9252 (preface, celebrant-rubrics-phase1 Phase 3): {!Liturgical_day.t}
+gained a [preface] field, the same seam [gloria] just above used -- all
+365 of 2027's records print a new [(preface (<variant>))] token (checked
+directly, [grep -c "(preface"]), [(preface ())] on exactly one day
+(2027's own Good Friday, 26 March -- this engine resolves no Mass at all
+that day, {!Rite_ef.Rubrics_ef.preface}'s own citation). Same cosmetic
+reflow mechanics as every entry above, not a fixed one-line-per-record
+addition.
+
$ colitur emit --format sexp --from 2027 --to 2027 | wc -l
- 9197
+ 9252
$ 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 157841b..9059574 100644
--- a/test/test_calendar.ml
+++ b/test/test_calendar.ml
@@ -104,11 +104,13 @@ module Fixture = struct
the sanctoral side. *)
let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = (None, [])
- (* No fixture here exercises the Creed or Gloria rubrics either -- a rite
- that has not implemented them returns [false] explicitly,
- {!Rite.t.creed}/{!Rite.t.gloria}'s own documented default. *)
+ (* No fixture here exercises the Creed, Gloria or preface rubrics either
+ -- a rite that has not implemented them returns [false]/[None]
+ explicitly, {!Rite.t.creed}/{!Rite.t.gloria}/{!Rite.t.preface}'s own
+ documented default. *)
let creed ~temporal:_ ~observed:_ ~date:_ = false
let gloria ~temporal:_ ~observed:_ ~date:_ = false
+ let preface ~temporal:_ ~observed:_ ~date:_ = None
let rite : (season, rank) Rite.t =
{ Rite.id = "synthetic-calendar"; vocab; year_start; temporal; anchors = (fun _ -> []);
@@ -120,7 +122,7 @@ module Fixture = struct
(* Not a Roman rite either, so no bissextile-doubling convention:
identity, {!Rite.t.fixed_key}'s own documented default. *)
fixed_key = (fun d -> Some (D.month d, D.day d));
- rules; season_runs = [ A; B ]; transfer_target; readings; creed; gloria }
+ rules; season_runs = [ A; B ]; transfer_target; readings; creed; gloria; preface }
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_fiuv_ordo.ml b/test/test_fiuv_ordo.ml
index 5c009a9..3bfed68 100644
--- a/test/test_fiuv_ordo.ml
+++ b/test/test_fiuv_ordo.ml
@@ -40,6 +40,7 @@ module Overlay = Colitur_kernel.Overlay
module LD = Colitur_kernel.Liturgical_day
module Date = Colitur_kernel.Date
module V = Rite_ef.Vocab_ef
+module Preface = Colitur_kernel.Preface
let sanctoral_path = "../data/ef/sanctoral.sexp"
let adjustments_path = "../data/ef/adjustments.sexp"
@@ -138,7 +139,7 @@ let window_last = "2026-12-31"
(* late). *)
(* ---------------------------------------------------------------------- *)
-type colitur_row = { c_date : string; c_creed : bool; c_gloria : bool }
+type colitur_row = { c_date : string; c_creed : bool; c_gloria : bool; c_preface : Preface.t option }
let colitur_rows () =
let layer = real_layer () in
@@ -156,7 +157,9 @@ let colitur_rows () =
(match Hashtbl.find_opt by_rata (Date.to_rata !d) with
| Some day ->
rows :=
- { c_date = Date.to_iso8601 day.LD.date; c_creed = day.LD.creed; c_gloria = day.LD.gloria } :: !rows
+ { c_date = Date.to_iso8601 day.LD.date; c_creed = day.LD.creed; c_gloria = day.LD.gloria;
+ c_preface = day.LD.preface }
+ :: !rows
| None -> Alcotest.failf "no colitur day resolved for %s" (Date.to_iso8601 !d));
d := Date.add_days !d 1
done;
@@ -346,6 +349,148 @@ let test_gloria_matches_or_is_explained () =
Alcotest.(check int) "F1 (Good Friday) count" 1 !f1_count
(* ---------------------------------------------------------------------- *)
+(* Preface (RG 482-499, {!Rite_ef.Rubrics_ef.preface}) -- Phase 3. Same *)
+(* shape as Creed/Gloria above, over the identical 400-row window, using *)
+(* the [praef] column already captured into the fixture (CAPTURED, NOT *)
+(* VALIDATED at the time -- tools/extract_fiuv_ordo.ml's own [row.praef] *)
+(* citation) but never compared until now. *)
+(* *)
+(* CHARACTERISATION FIRST, per the task's own standing instruction (this
+ project has been misled by trusting an Ordo field's surface shape
+ before, four times this session alone): [praef] is RAW TRAILING TEXT
+ after the literal token "praef." to the end of the primary Mass
+ clause -- it is not a clean enum, it can carry an arbitrary tail of
+ unrelated rubrical prose glued on by the same token-flattening that
+ produces it (Good Friday's own [gloria]/F1 finding above is the
+ worked example of exactly this shape). CLASSIFIED here by PREFIX
+ match against the FIRST WORD(S) only -- every prefix below was found
+ by direct inspection of the fixture's own distinct [praef] values
+ (test/fixtures/fiuv-ordo-2025-2026.sexp, 400 rows, all read by hand
+ while building this comparison) and cross-checked against the primary
+ rubric text, not guessed. Two genuine EXCEPTION SHAPES, THREE rows,
+ found the same way:
+
+ - Christmas Day (both of them -- the fixture's own window, 27
+ November 2025-31 December 2026, spans thirteen months and TWO 25
+ Decembers, confirmed by running the coverage test below before
+ pinning it at "two", not three, the first time) and Epiphany Day
+ (2026-01-06, the window's only one) print "praef. et Communic..."
+ -- an ELLIPSIS: the compiler treats "this feast's own [i.e.
+ obviously implied] preface" as not needing its own name restated
+ on the ONE day that IS that feast, unlike every octave/de-Tempore
+ day governed by the SAME feast (which always restates the name
+ explicitly, e.g. 26 December's own "de Nativ."). Confirmed by
+ direct inspection: no OTHER day in the fixture omits the name this
+ way. [classify_praef] returns [None] (unrecognised) for all three
+ rather than special-casing them by date -- both feasts are
+ independently, directly citable from RG 484(a)/485(a)'s own text
+ (the Nativity/Epiphany feast itself is the paradigm case each
+ clause names first), so the oracle's corroboration is not needed
+ to trust colitur's own answer on these dates, and forcing a match
+ here would mean trusting an inference about the source's own
+ ellipsis rather than reading it.
+ - Good Friday (2026-04-03) prints "comm. Feria VI prima in mense." --
+ classifies cleanly as [Common] by the same prefix rule as any
+ other day, but this file's own F1 (Gloria, above) already
+ adjudicated this EXACT date's raw text as unreliable (a copied,
+ not a considered, line -- confirmed against the PDF's own page
+ image, not merely the extracted fixture). The same defect
+ extends to whatever trails "praef." on the identical corrupted
+ line: excluded from this comparison by reusing [is_f1_good_friday]
+ directly, not re-argued. *)
+
+let has_prefix ~prefix s =
+ String.length s >= String.length prefix && String.sub s 0 (String.length prefix) = prefix
+
+(* Every prefix below maps 1:1 onto exactly one branch of
+ {!Rite_ef.Rubrics_ef.preface}'s own priority order -- see that
+ function's own header for the full citation of each. Order does not
+ matter here (unlike in [preface] itself): the Ordo's own printed text
+ never carries two of these prefixes on the same row, so this is a
+ partition, not a priority list. *)
+let praef_prefixes =
+ [ ("comm.", Preface.Common);
+ ("Trinit.", Preface.Trinity);
+ ("Quadr.", Preface.Lent);
+ ("de Sancta Cruce", Preface.Holy_cross);
+ ("Pasch.", Preface.Easter);
+ ("etc. ut in festo", Preface.Easter);
+ (* Easter-octave ferias, "as on the feast [of Easter]" -- e.g. 6
+ April 2026, "etc. ut in festo., ad Ite, missa est additur duplex
+ Alleluia." *)
+ ("Ascensionis", Preface.Ascension);
+ ("de Spirito Sancto", Preface.Holy_spirit);
+ ("BMV", Preface.Bvm);
+ ("App.", Preface.Apostles);
+ ("de Nativ.", Preface.Nativity);
+ ("de Epiphania", Preface.Epiphany);
+ ("de Ss.mi Corde", Preface.Sacred_heart);
+ ("de Ss.mo Corde", Preface.Sacred_heart);
+ ("de Domino Nostro Jesu Rege", Preface.Christ_the_king);
+ ("de S Iosepho", Preface.St_joseph);
+ ("de S Ioseph", Preface.St_joseph);
+ ("defunctorum", Preface.Requiem) ]
+
+let classify_praef (s : string) : Preface.t option =
+ let rec go = function
+ | [] -> None
+ | (prefix, p) :: rest -> if has_prefix ~prefix s then Some p else go rest
+ in
+ go praef_prefixes
+
+(* Measured directly, not assumed: every one of the 400 rows' own [praef]
+ text, classified, tallied by outcome. Two rows are genuinely
+ unclassifiable by this prefix table (the ellipsis dates above); every
+ other non-[None] [praef] value classifies. Pinned so a change to
+ either the fixture or [classify_praef] that silently drops coverage
+ fails loudly here rather than merely narrowing the comparison below. *)
+let test_praef_classification_coverage () =
+ let ordo = ordo_rows () in
+ let with_praef = List.filter_map (fun o -> o.praef) ordo in
+ Alcotest.(check int) "399 of 400 rows carry a [praef] value (Holy Saturday is the one exception)" 399
+ (List.length with_praef);
+ let unclassified = List.filter (fun s -> classify_praef s = None) with_praef in
+ (* THREE rows, not two: the fixture's own window (27 November 2025-31
+ December 2026, 13 months) spans TWO Christmas Days, both printing the
+ identical ellipsis text -- found running this exact assertion, not
+ assumed from the date count alone. *)
+ Alcotest.(check int) "the three ellipsis rows (two Christmas Days, one Epiphany) are unclassified" 3
+ (List.length unclassified);
+ Alcotest.(check bool) "every unclassified row starts with the ellipsis's own \"et Communic\"" true
+ (List.for_all (has_prefix ~prefix:"et Communic") unclassified)
+
+let describe_preface_mismatch (o : ordo_row) (c : colitur_row) (expected : Preface.t) =
+ Printf.sprintf "%s %S: colitur preface=%s, Ordo praef=%S (classified %s)" o.date o.title
+ (match c.c_preface with Some p -> Preface.to_string p | None -> "-")
+ (Option.get o.praef) (Preface.to_string expected)
+
+let test_preface_matches_or_is_explained () =
+ let ordo = ordo_rows () in
+ let colitur = colitur_rows () in
+ let unexplained = ref [] in
+ let unclassified_count = ref 0 in
+ let f1_count = ref 0 in
+ List.iter2
+ (fun (o : ordo_row) (c : colitur_row) ->
+ if not (String.equal o.date c.c_date) then Alcotest.failf "misaligned: ordo %s vs colitur %s" o.date c.c_date;
+ if is_f1_good_friday o then incr f1_count
+ else
+ match o.praef with
+ | None -> ()
+ | Some raw -> (
+ match classify_praef raw with
+ | None -> incr unclassified_count
+ | Some expected ->
+ if c.c_preface = Some expected then ()
+ else unexplained := describe_preface_mismatch o c expected :: !unexplained))
+ ordo colitur;
+ Alcotest.(check (list string)) "every classified preface difference is explained -- none unexplained" []
+ (List.rev !unexplained);
+ Alcotest.(check int) "the three ellipsis rows are skipped, not silently counted as agreement" 3
+ !unclassified_count;
+ Alcotest.(check int) "F1 (Good Friday) is skipped here too, the same root cause as the Gloria axis" 1 !f1_count
+
+(* ---------------------------------------------------------------------- *)
(* Te Deum (Breviary 237-238, {!Rite_ef.Rubrics_ef.te_deum}) -- Phase 2, *)
(* the mitigation the task brief names for this source's own stated *)
(* weakness (a single, not yet scan-verified, web transcription). A *)
@@ -440,6 +585,10 @@ let suite =
Alcotest.test_case "Ordo Gloria coverage matches the measured figure" `Quick test_gloria_coverage;
Alcotest.test_case "every Gloria difference is named in the cited allow-list -- none unexplained" `Quick
test_gloria_matches_or_is_explained;
+ Alcotest.test_case "praef classification coverage matches the measured figure" `Quick
+ test_praef_classification_coverage;
+ Alcotest.test_case "every classified preface difference is explained -- none unexplained" `Quick
+ test_preface_matches_or_is_explained;
Alcotest.test_case "Ordo Te Deum coverage matches the measured figure" `Quick test_te_deum_coverage;
Alcotest.test_case "every Te Deum difference is named in the cited allow-list -- none unexplained" `Quick
test_te_deum_matches_or_is_explained
diff --git a/test/test_rubrics_ef.ml b/test/test_rubrics_ef.ml
index d533670..bb3bfed 100644
--- a/test/test_rubrics_ef.ml
+++ b/test/test_rubrics_ef.ml
@@ -746,6 +746,333 @@ let test_exhaustive_gloria_domain_sweep () =
Alcotest.(check bool) "the full domain reached a real number of Requiem days" true (black > 1_000)
end
+(* ---- RG 482-499, the preface -- 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 branch, resolved against REAL calendar dates
+ through the shipped data, the same discipline every other section of
+ this file already follows -- most of these dates were cross-checked
+ directly against the FIUV Ordo's own [praef] column
+ (test/fixtures/fiuv-ordo-2025-2026.sexp) before being pinned here, not
+ merely derived from the Latin text in isolation; see [preface]'s own
+ header for the full account of what that cross-check settled. ---- *)
+
+module Pref = Colitur_kernel.Preface
+
+let preface_on y m d = (day_on y m d).LD.preface
+
+let preface_string_on y m d =
+ match preface_on y m d with Some p -> Pref.to_string p | None -> "-"
+
+let check_preface name expected y m d =
+ Alcotest.(check string) name (Pref.to_string expected) (preface_string_on y m d)
+
+let check_no_preface name y m d = Alcotest.(check string) name "-" (preface_string_on y m d)
+
+(* ---- Good Friday: no Mass at all, [None] -- checked ahead of the
+ Requiem/Black-colour branch (this file's own [preface] header explains
+ why the two need different answers, unlike creed/gloria/te_deum). ---- *)
+let test_preface_good_friday_no_mass () = check_no_preface "Good Friday: no Mass, no preface" 2026 4 3
+
+(* ---- RG 499: the Requiem proxy, the SAME two-member {!Colour.Black}
+ population {!test_colour_black_population_is_exactly_two} already
+ asserts, with Good Friday split out above. ---- *)
+let test_preface_499_all_souls () =
+ check_preface "RG 499: All Souls' Day (transferred)" Pref.Requiem 2025 11 3
+
+(* ---- RG 487(a): the two fixed Holy Cross feast triggers. ---- *)
+let test_preface_487a_exaltation () =
+ check_preface "RG 487(a): the Exaltation of the Holy Cross" Pref.Holy_cross 2026 9 14
+
+let test_preface_487a_precious_blood () =
+ check_preface "RG 487(a): the Most Precious Blood" Pref.Holy_cross 2026 7 1
+
+(* ---- RG 491/492: Sacred Heart, Christ the King -- single named days. ---- *)
+let test_preface_491_sacred_heart () = check_preface "RG 491: the Sacred Heart" Pref.Sacred_heart 2026 6 12
+let test_preface_492_christ_the_king () =
+ check_preface "RG 492: Christ the King" Pref.Christ_the_king 2026 10 25
+
+(* ---- RG 490(a): Ascension Day itself. ---- *)
+let test_preface_490a_ascension_day () = check_preface "RG 490(a): Ascension Day" Pref.Ascension 2026 5 14
+
+(* ---- RG 494(a): Trinity Sunday itself. ---- *)
+let test_preface_494a_trinity_sunday () = check_preface "RG 494(a): Trinity Sunday" Pref.Trinity 2026 5 31
+
+(* ---- RG 496: St Joseph's two feasts. ---- *)
+let test_preface_496_joseph_spouse () = check_preface "RG 496: St Joseph, Spouse of the BVM" Pref.St_joseph 2026 3 19
+let test_preface_496_joseph_workman () = check_preface "RG 496: St Joseph the Workman" Pref.St_joseph 2027 5 1
+
+(* ---- RG 495: a genuine Marian FEAST via [marian_slugs] (the Assumption),
+ and the VOTIVE-shaped BVM Saturday Office via [subject = Bvm] -- the
+ latter is the live witness for RG 495's own "et votivis" half on this
+ engine's data (this file's own [preface] header has the full account,
+ corroborated against the Ordo on 3/10 January 2026). ---- *)
+let test_preface_495_assumption () = check_preface "RG 495: the Assumption (marian_slugs)" Pref.Bvm 2026 8 15
+let test_preface_495_bvm_saturday_office () =
+ check_preface "RG 495: the BVM Saturday Office (subject=Bvm, votive-shaped)" Pref.Bvm 2026 7 11
+
+(* ---- RG 484(a)'s own explicit Purification clause: 2 February, checked
+ as a standalone slug trigger regardless of season -- see [preface]'s
+ own header for why this needed to be independent of the Nativity
+ WINDOW below (2 February is nowhere near it). ---- *)
+let test_preface_484a_purification () = check_preface "RG 484(a): the Purification" Pref.Nativity 2026 2 2
+
+(* ---- RG 484(a)/(b) merged: the Nativity octave window, 25 December-1
+ January, PLUS RG 484(b)'s own extra "2 ad 5 ianuarii" days -- one
+ contiguous window. St Stephen (26 December) is the live witness that
+ this OUTRANKS an ordinary saint's own otherwise-Common preface;
+ {!creed}'s own 475(d) test picks the identical date for the identical
+ "occurring inside a privileged window" shape. ---- *)
+let test_preface_484_nativity_day () = check_preface "RG 484(a): Christmas Day itself" Pref.Nativity 2026 12 25
+let test_preface_484_stephen_in_octave () =
+ check_preface "RG 484(b): St Stephen, occurring within the Nativity octave" Pref.Nativity 2026 12 26
+
+let test_preface_484_jan1_octave_day () =
+ check_preface "RG 484(a): 1 January, the Octave Day" Pref.Nativity 2026 1 1
+
+let test_preface_484b_jan2to5 () =
+ (* 2 January 2026 (a plain Christmastide feria, "ef-christmas-1-friday",
+ confirmed via `colitur day 2026`) -- 3 January that year is the BVM
+ Saturday Office instead (RG 495's own "et votivis" outranking this
+ window, this file's own [test_preface_495_bvm_saturday_office] and
+ [preface]'s own header have the full account), so this date is picked
+ specifically to witness the PLAIN de-Tempore grant, uncomplicated by
+ that override. *)
+ check_preface "RG 484(b): 2 January, the extra de-Tempore days" Pref.Nativity 2026 1 2
+
+(* ---- RG 484(b)'s own NARROWER exception: an Apostle/Evangelist inside
+ the Nativity octave is STILL overridden to [Nativity] (unlike every
+ other window, which an Apostle's own preface outranks -- the next test
+ below). John the Evangelist, 27 December, is the live witness this
+ file's own [preface] header cites. ---- *)
+let test_preface_484b_overrides_apostle_in_octave () =
+ check_preface "RG 484(b): St John the Evangelist, inside the Nativity octave, still [Nativity]"
+ Pref.Nativity 2026 12 27
+
+(* ---- RG 497: the Apostle/Evangelist natalicia list, reused from
+ {!creed_apostle_slugs} -- OUTSIDE the Nativity octave, an Apostle keeps
+ his own preface even inside another window (Sts Philip & James, 11 May,
+ inside the Paschaltide/Easter window -- the FIUV Ordo's own witness
+ this file's own [preface] header cites for RG 484(b)'s narrower
+ carve-out, checked from the other direction). ---- *)
+let test_preface_497_barnabas () = check_preface "RG 497: St Barnabas" Pref.Apostles 2026 6 11
+let test_preface_497_peter_paul () = check_preface "RG 497: Sts Peter & Paul" Pref.Apostles 2026 6 29
+let test_preface_497_philip_james_inside_easter_window () =
+ check_preface "RG 497: Sts Philip & James, inside the Easter window, still [Apostles]" Pref.Apostles 2026 5 11
+
+(* ---- RG 485(a)/(b): Epiphany itself and its own Baptism commemoration
+ (a), the wider 7-13 January window (b) -- Holy Family Sunday (11
+ January 2026) is the live witness that a temporal-origin Sunday inside
+ this window gets [Epiphany], not some Holy-Family-specific preface this
+ engine has none of. ---- *)
+let test_preface_485a_epiphany_day () = check_preface "RG 485(a): Epiphany Day" Pref.Epiphany 2026 1 6
+let test_preface_485a_baptism_commemoration () =
+ check_preface "RG 485(a): the Commemoration of the Baptism of the Lord" Pref.Epiphany 2026 1 13
+
+let test_preface_485b_holy_family_sunday () =
+ check_preface "RG 485(b): Holy Family Sunday, inside the 7-13 January window" Pref.Epiphany 2026 1 11
+
+(* ---- RG 486(a)/(b): the Lenten window, Ash Wednesday through the
+ Saturday before Passion Sunday I. ---- *)
+let test_preface_486_ash_wednesday () = check_preface "RG 486(a): Ash Wednesday" Pref.Lent 2026 2 18
+let test_preface_486_lent_saturday_boundary () =
+ check_preface "RG 486: the Saturday immediately before Passion Sunday I" Pref.Lent 2026 3 21
+
+(* ---- RG 487(a)/(b): the Passiontide window, Passion Sunday I through
+ Holy Thursday inclusive. ---- *)
+let test_preface_487_passion_sunday () = check_preface "RG 487: Passion Sunday I" Pref.Holy_cross 2026 3 22
+let test_preface_487_palm_sunday () = check_preface "RG 487: Palm Sunday" Pref.Holy_cross 2026 3 29
+let test_preface_487_holy_thursday () = check_preface "RG 487: Holy Thursday" Pref.Holy_cross 2026 4 2
+
+(* ---- RG 489(a)/(b): the Easter window, the Vigil Mass through the vigil
+ of the Ascension. ---- *)
+let test_preface_489_easter_vigil_mass () = check_preface "RG 489(a): the Easter Vigil Mass" Pref.Easter 2026 4 4
+let test_preface_489_easter_sunday () = check_preface "RG 489(a): Easter Sunday" Pref.Easter 2026 4 5
+let test_preface_489_ascension_vigil () = check_preface "RG 489(b): the vigil of the Ascension" Pref.Easter 2026 5 13
+
+(* ---- RG 490(b): the post-Ascension window. ---- *)
+let test_preface_490b_post_ascension_feria () =
+ check_preface "RG 490(b): the Friday after Ascension" Pref.Ascension 2026 5 15
+
+let test_preface_490b_sunday_after_ascension () =
+ check_preface "RG 490(b): the Sunday after Ascension" Pref.Ascension 2026 5 17
+
+(* ---- RG 493(a)/(b): the Pentecost-octave window, the vigil through the
+ following Saturday (the Ember Saturday). ---- *)
+let test_preface_493_pentecost_vigil () = check_preface "RG 493(a): the vigil of Pentecost" Pref.Holy_spirit 2026 5 23
+let test_preface_493_pentecost_sunday () = check_preface "RG 493(a): Pentecost Sunday" Pref.Holy_spirit 2026 5 24
+let test_preface_493_ember_saturday () =
+ check_preface "RG 493(a): the Ember Saturday within the Pentecost octave" Pref.Holy_spirit 2026 5 30
+
+(* ---- RG 494(b): the Trinity de-Tempore Sundays -- an Advent Sunday
+ ([temporal.season = Advent], [Class1] by construction) and an ordinary
+ Time-after-Pentecost Sunday. Both now read off [temporal]'s own season,
+ not [observed]'s rank -- see the next test for why the distinction is
+ live, not merely stylistic. ---- *)
+let test_preface_494b_advent_sunday () = check_preface "RG 494(b): Advent I Sunday" Pref.Trinity 2026 11 29
+let test_preface_494b_ordinary_class2_sunday () =
+ check_preface "RG 494(b): an ordinary Time-after-Pentecost Sunday" Pref.Trinity 2026 8 9
+
+(* ---- RG 494(b), the RG 16(a)-shaped fix round: All Saints' Day (1
+ November), [Class1], observed OUTRIGHT over the ordinary Sunday it
+ commemorates -- found via the FIUV Ordo (its own entry reads "Trinit.
+ vel de Omnibus Sanctis et Patronis", the "vel..." half an alternate
+ ORATIO reference, the SAME pattern every other "X vel Y" [praef] value
+ in that fixture already follows, never a second genuine preface). No
+ dedicated preface exists for All Saints among RG 484-497's own
+ fourteen, so RG 482's chain falls to the SUNDAY's own de-Tempore grant
+ regardless of which rank actually won the day -- exactly why this
+ branch must read [temporal]'s season, not [observed]'s [Class1] rank,
+ which an EARLIER version of [preface] wrongly required to equal
+ [Class2] and so answered [Common] here instead.
+
+ 1 November 2026 is itself a Sunday (confirmed via `colitur day 2026`:
+ "all-saints class-1 white +ef-time-after-pentecost-sunday-23"), the
+ exact shape this fix concerns -- picked for that reason, not merely
+ because it is All Saints' Day. *)
+let test_preface_494b_all_saints_class1_sunday () =
+ check_preface "RG 494(b): All Saints' Day, Class1, still Trinity (no dedicated preface exists)" Pref.Trinity
+ 2026 11 1
+
+(* ---- RG 498: the Common residual -- Corpus Christi is the deliberately
+ chosen witness (NOT simply "any ordinary weekday"): the 1962 Missal
+ gives it no preface of its own at all, only an OPTIONAL alternative
+ (Sacred Heart's), so it takes the plain Common, confirmed against the
+ Ordo directly (4 June 2026: "comm. vel de Cor Sacratissimo") -- a real
+ trap this engine's own tier1 list does NOT fall into (no
+ "ef-corpus-christi" entry anywhere in it). ---- *)
+let test_preface_498_corpus_christi () = check_preface "RG 498: Corpus Christi (no proper of its own)" Pref.Common 2026 6 4
+
+let test_preface_498_plain_saint () = check_preface "RG 498: a plain sanctoral saint (Lawrence)" Pref.Common 2026 8 10
+
+(* ---- Corroborating real-data invariants, task requirement 5: "every day
+ in Paschaltide should take the Easter preface unless it has a proper
+ one; every Lenten feria the Lent preface". Checked as a SWEEP, not a
+ single date, over a fixed sample span (1583-2200, matching the century+
+ spans other domain checks in this file already use) -- FAST in the
+ default suite, EXHAUSTIVE (1583-9999) gated behind
+ COLITUR_EXHAUSTIVE_SWEEP the same way {!test_exhaustive_gloria_domain_sweep}
+ above already is.
+
+ The invariant actually checked is NOT the brief's own literal phrasing
+ (which is one exception too strong, the identical "pushed back, not
+ silently special-cased" stance {!check_gloria_invariants_for_year}'s own
+ header already takes for [gloria]/432(b)): every Paschaltide-season day
+ takes [Easter], [Ascension] or [Holy_spirit] (its own three seasonal
+ windows), OR one of the season-independent title triggers this engine
+ can construct inside Paschaltide's real date range ([Bvm]/[St_joseph]/
+ [Apostles]/[Requiem]) -- but NEVER [Nativity]/[Epiphany]/[Lent]/[Trinity]/
+ [Sacred_heart]/[Christ_the_king]/[Common], none of which any real date
+ inside Paschaltide can trigger. Symmetrically for Lent: [Lent] or one of
+ [Bvm]/[St_joseph]/[Apostles]/[Requiem], never a preface belonging to a
+ date-disjoint window. Two further invariants the sweep found and kept,
+ not originally asked for but load-bearing: EVERY Christmastide-season
+ day resolves [Nativity], [Epiphany] or [Bvm] (RG 484/485 between them
+ leave no gap at all in that season on their own -- confirmed
+ exhaustively -- but RG 495's own votive-Mass half, live for the BVM
+ Saturday Office, can and does fall on a Christmastide Saturday too;
+ found live at 1584-01-07 while first running this exact sweep, kept as
+ a real witness rather than narrowed away); and Passiontide, uniquely,
+ legitimately
+ ALSO produces [Easter] exactly once a year (Holy Saturday's own Vigil
+ Mass, [n = -1] -- {!Rite_ef.Rubrics_ef.preface}'s own RG 489(a) comment;
+ this engine's day/colour model keeps that date [Passiontide] by season
+ even though the Vigil Mass's own preface has already moved to Easter's,
+ the same acknowledged per-action nuance [gloria]'s own RG 431(c) comment
+ already documents) -- asserted to be EXACTLY once per year, not merely
+ allowed, so a second, unexplained (Passiontide, Easter) day anywhere in
+ the domain still fails loudly. *)
+let check_preface_season_invariants_for_year y (counts : (int * int * int * int) ref) =
+ let days = Cal.year ctx layer y in
+ let christmastide_gaps = ref 0
+ and paschaltide_gaps = ref 0
+ and lent_gaps = ref 0
+ and passiontide_easter = ref 0 in
+ let christmastide_gaps0, paschaltide_gaps0, lent_gaps0, passiontide_easter0 = !counts in
+ christmastide_gaps := christmastide_gaps0;
+ paschaltide_gaps := paschaltide_gaps0;
+ lent_gaps := lent_gaps0;
+ passiontide_easter := passiontide_easter0;
+ Array.iter
+ (fun (d : (V.season, V.rank) LD.t) ->
+ let label = Printf.sprintf "%s (preface=%s)" (Date.to_iso8601 d.LD.date)
+ (match d.LD.preface with Some p -> Pref.to_string p | None -> "NONE")
+ in
+ (match d.LD.temporal.Temporal.season with
+ | V.Christmastide -> (
+ match d.LD.preface with
+ | Some (Pref.Nativity | Pref.Epiphany | Pref.Bvm) -> ()
+ | _ ->
+ incr christmastide_gaps;
+ Alcotest.failf "%s: Christmastide day with neither Nativity, Epiphany nor Bvm" label)
+ | V.Paschaltide -> (
+ match d.LD.preface with
+ | Some (Pref.Easter | Pref.Ascension | Pref.Holy_spirit | Pref.Bvm | Pref.St_joseph
+ | Pref.Apostles | Pref.Requiem) ->
+ ()
+ | _ ->
+ incr paschaltide_gaps;
+ Alcotest.failf "%s: Paschaltide day with an out-of-window preface" label)
+ | V.Lent -> (
+ match d.LD.preface with
+ | Some (Pref.Lent | Pref.Bvm | Pref.St_joseph | Pref.Apostles | Pref.Requiem) -> ()
+ | _ ->
+ incr lent_gaps;
+ Alcotest.failf "%s: Lent day with an out-of-window preface" label)
+ | V.Passiontide ->
+ if d.LD.preface = Some Pref.Easter then begin
+ incr passiontide_easter;
+ let easter = Computus.gregorian_easter (Date.year d.LD.date) in
+ Alcotest.(check int)
+ (Printf.sprintf "%s: the ONLY Passiontide/Easter day is the Vigil Mass (Easter-1)" label)
+ (-1) (Date.to_rata d.LD.date - Date.to_rata easter)
+ end
+ | _ -> ()))
+ days;
+ counts := (!christmastide_gaps, !paschaltide_gaps, !lent_gaps, !passiontide_easter)
+
+let test_preface_season_invariants_sample () =
+ let counts = ref (0, 0, 0, 0) in
+ for y = 1583 to 1782 do
+ check_preface_season_invariants_for_year y counts
+ done;
+ let _, _, _, passiontide_easter = !counts in
+ Alcotest.(check bool) "the 200-year sample found the once-a-year Passiontide/Easter exception" true
+ (passiontide_easter >= 200)
+
+let test_exhaustive_preface_season_domain_sweep () =
+ if Sys.getenv_opt colitur_exhaustive_sweep_env = None then Alcotest.skip ()
+ else begin
+ let counts = ref (0, 0, 0, 0) in
+ for y = 1583 to 9999 do
+ check_preface_season_invariants_for_year y counts
+ done;
+ let christmastide_gaps, paschaltide_gaps, lent_gaps, passiontide_easter = !counts in
+ Printf.printf
+ "preface season sweep 1583..9999: christmastide_gaps=%d paschaltide_gaps=%d lent_gaps=%d \
+ passiontide_easter=%d\n%!"
+ christmastide_gaps paschaltide_gaps lent_gaps passiontide_easter;
+ Alcotest.(check int) "zero Christmastide gaps anywhere in the domain" 0 christmastide_gaps;
+ Alcotest.(check int) "zero Paschaltide out-of-window prefaces anywhere in the domain" 0 paschaltide_gaps;
+ Alcotest.(check int) "zero Lent out-of-window prefaces anywhere in the domain" 0 lent_gaps;
+ (* 8416, not 8417 (the domain's own year count): a genuine, pre-existing
+ DOMAIN-BOUNDARY edge, not a preface defect -- {!Cal.year}'s own
+ liturgical year "opening in civil year 9999" cannot construct any
+ date past 1583-9999 ({!Date.make}'s own validated range), so it
+ returns only 34 days (28 November-31 December), never reaching its
+ own Easter/Holy Saturday (which would fall in year 10000). Found by
+ running this exact sweep: it returned 8416 first, not asserted
+ blindly at 8417 -- checked directly ([Cal.year ctx layer 9999]
+ alone, printed length 34) before writing this comment, the same
+ "measure before adjudicating" discipline every allow-list in this
+ project follows. The identical edge is why
+ docs/research/rules-register.md's own Layer.index citation already
+ reads "both edges, 1582 and 10000, bit during development" --
+ this sweep is a second witness to the same known boundary, not a
+ new one. *)
+ Alcotest.(check int) "one Passiontide/Easter day per year, every year but the domain's own last" 8416
+ passiontide_easter
+ end
+
(* ---- ITEM 1: RG 111(a), the sung-Mass commemoration cap
({!Colitur_kernel.Precedence.sung_mass_commemorations}) -- two real
calendar days, one of each shape, resolved through the identical
@@ -855,4 +1182,62 @@ let suite =
Alcotest.test_case "RG 111(a): an ordinary-only Low-Mass set is empty at Sung Mass" `Quick
test_rg111a_ordinary_only_dropped;
Alcotest.test_case "RG 111(a): a privileged commemoration survives at Sung Mass" `Quick
- test_rg111a_privileged_kept ] )
+ test_rg111a_privileged_kept;
+ Alcotest.test_case "preface: Good Friday, no Mass" `Quick test_preface_good_friday_no_mass;
+ Alcotest.test_case "RG 499: All Souls' Day (transferred)" `Quick test_preface_499_all_souls;
+ Alcotest.test_case "RG 487(a): the Exaltation of the Holy Cross" `Quick test_preface_487a_exaltation;
+ Alcotest.test_case "RG 487(a): the Most Precious Blood" `Quick test_preface_487a_precious_blood;
+ Alcotest.test_case "RG 491: the Sacred Heart" `Quick test_preface_491_sacred_heart;
+ Alcotest.test_case "RG 492: Christ the King" `Quick test_preface_492_christ_the_king;
+ Alcotest.test_case "RG 490(a): Ascension Day" `Quick test_preface_490a_ascension_day;
+ Alcotest.test_case "RG 494(a): Trinity Sunday" `Quick test_preface_494a_trinity_sunday;
+ Alcotest.test_case "RG 496: St Joseph, Spouse of the BVM" `Quick test_preface_496_joseph_spouse;
+ Alcotest.test_case "RG 496: St Joseph the Workman" `Quick test_preface_496_joseph_workman;
+ Alcotest.test_case "RG 495: the Assumption (marian_slugs)" `Quick test_preface_495_assumption;
+ Alcotest.test_case "RG 495: the BVM Saturday Office (subject=Bvm)" `Quick
+ test_preface_495_bvm_saturday_office;
+ Alcotest.test_case "RG 484(a): the Purification" `Quick test_preface_484a_purification;
+ Alcotest.test_case "RG 484(a): Christmas Day itself" `Quick test_preface_484_nativity_day;
+ Alcotest.test_case "RG 484(b): St Stephen, inside the Nativity octave" `Quick
+ test_preface_484_stephen_in_octave;
+ Alcotest.test_case "RG 484(a): 1 January, the Octave Day" `Quick test_preface_484_jan1_octave_day;
+ Alcotest.test_case "RG 484(b): 3 January, the extra de-Tempore days" `Quick test_preface_484b_jan2to5;
+ Alcotest.test_case "RG 484(b): St John the Evangelist, inside the Nativity octave" `Quick
+ test_preface_484b_overrides_apostle_in_octave;
+ Alcotest.test_case "RG 497: St Barnabas" `Quick test_preface_497_barnabas;
+ Alcotest.test_case "RG 497: Sts Peter & Paul" `Quick test_preface_497_peter_paul;
+ Alcotest.test_case "RG 497: Sts Philip & James, inside the Easter window" `Quick
+ test_preface_497_philip_james_inside_easter_window;
+ Alcotest.test_case "RG 485(a): Epiphany Day" `Quick test_preface_485a_epiphany_day;
+ Alcotest.test_case "RG 485(a): the Commemoration of the Baptism of the Lord" `Quick
+ test_preface_485a_baptism_commemoration;
+ Alcotest.test_case "RG 485(b): Holy Family Sunday, inside the 7-13 January window" `Quick
+ test_preface_485b_holy_family_sunday;
+ Alcotest.test_case "RG 486(a): Ash Wednesday" `Quick test_preface_486_ash_wednesday;
+ Alcotest.test_case "RG 486: the Saturday before Passion Sunday I" `Quick
+ test_preface_486_lent_saturday_boundary;
+ Alcotest.test_case "RG 487: Passion Sunday I" `Quick test_preface_487_passion_sunday;
+ Alcotest.test_case "RG 487: Palm Sunday" `Quick test_preface_487_palm_sunday;
+ Alcotest.test_case "RG 487: Holy Thursday" `Quick test_preface_487_holy_thursday;
+ Alcotest.test_case "RG 489(a): the Easter Vigil Mass" `Quick test_preface_489_easter_vigil_mass;
+ Alcotest.test_case "RG 489(a): Easter Sunday" `Quick test_preface_489_easter_sunday;
+ Alcotest.test_case "RG 489(b): the vigil of the Ascension" `Quick test_preface_489_ascension_vigil;
+ Alcotest.test_case "RG 490(b): the Friday after Ascension" `Quick test_preface_490b_post_ascension_feria;
+ Alcotest.test_case "RG 490(b): the Sunday after Ascension" `Quick
+ test_preface_490b_sunday_after_ascension;
+ Alcotest.test_case "RG 493(a): the vigil of Pentecost" `Quick test_preface_493_pentecost_vigil;
+ Alcotest.test_case "RG 493(a): Pentecost Sunday" `Quick test_preface_493_pentecost_sunday;
+ Alcotest.test_case "RG 493(a): the Ember Saturday within the Pentecost octave" `Quick
+ test_preface_493_ember_saturday;
+ Alcotest.test_case "RG 494(b): Advent I Sunday" `Quick test_preface_494b_advent_sunday;
+ Alcotest.test_case "RG 494(b): an ordinary Time-after-Pentecost Sunday" `Quick
+ test_preface_494b_ordinary_class2_sunday;
+ Alcotest.test_case "RG 494(b): All Saints' Day, Class1, still Trinity" `Quick
+ test_preface_494b_all_saints_class1_sunday;
+ Alcotest.test_case "RG 498: Corpus Christi (no proper of its own)" `Quick test_preface_498_corpus_christi;
+ Alcotest.test_case "RG 498: a plain sanctoral saint (Lawrence)" `Quick test_preface_498_plain_saint;
+ Alcotest.test_case "preface domain sanity: Christmastide/Paschaltide/Lent window invariants (sample)"
+ `Quick test_preface_season_invariants_sample;
+ Alcotest.test_case
+ "preface domain sweep 1583..9999: window invariants, committed not sampled" `Slow
+ test_exhaustive_preface_season_domain_sweep ] )
diff --git a/test/test_validate.ml b/test/test_validate.ml
index b53f915..3da8fbb 100644
--- a/test/test_validate.ml
+++ b/test/test_validate.ml
@@ -324,17 +324,19 @@ module Synthetic = struct
{ 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 or Gloria rubrics -- a rite that
- has not implemented them returns [false] explicitly,
- {!Rite.t.creed}/{!Rite.t.gloria}'s own documented default. Made
- overridable ([?creed]/[?gloria] below) on the same footing as
- [?readings] just above, for Task 6's own fixtures. *)
+ (* No fixture here exercises the Creed, Gloria or preface rubrics -- a
+ rite that has not implemented them returns [false]/[None] explicitly,
+ {!Rite.t.creed}/{!Rite.t.gloria}/{!Rite.t.preface}'s own documented
+ default. Made overridable ([?creed]/[?gloria]/[?preface] below) on
+ the same footing as [?readings] just above, for Task 6's own
+ fixtures. *)
let creed ~temporal:_ ~observed:_ ~date:_ = false
let gloria ~temporal:_ ~observed:_ ~date:_ = false
+ let preface ~temporal:_ ~observed:_ ~date:_ = None
let rite ?(vocab = vocab) ?(anchors = fun _ -> []) ?(season_runs = [ A; B ]) ?(rules = rules)
?(transfer_target = fun _ origin _ -> origin) ?(readings = readings) ?(creed = creed)
- ?(gloria = gloria) temporal : (season, rank) Rite.t =
+ ?(gloria = gloria) ?(preface = preface) 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
@@ -344,7 +346,7 @@ module Synthetic = struct
(* Not a Roman rite either, so no bissextile-doubling convention:
identity, {!Rite.t.fixed_key}'s own documented default. *)
fixed_key = (fun d -> Some (D.month d, D.day d));
- transfer_target; readings; creed; gloria }
+ transfer_target; readings; creed; gloria; preface }
(* Empty by default: every check built before Task 12 exercises the
TEMPORAL-only pass, where an empty layer is exactly the fixture that