summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 08:23:38 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-26 08:23:38 +0200
commit1a1479fd60e192c48789b3b065a3033702e4eeda (patch)
tree283a5fca36c942a6d7fbf287d8d21d57e1b8c1f8
parent85c1f194c03634eaa905f0bfd630fe23b5f53e59 (diff)
downloadcolitur-1a1479fd60e192c48789b3b065a3033702e4eeda.tar.gz
colitur-1a1479fd60e192c48789b3b065a3033702e4eeda.zip
feat(of): Rubrics_of -- Gloria, Credo, and an all-None Preface verdict
Task 3 of OF Phases 3-5: the celebrant-rubrics module for the OF rite, supplying three of Rite.t's four remaining fields. creed/gloria implement IGMR n. 53 (Gloria) and n. 67-68 (Symbolum), both quoted in full in the module header. temporal is read, not observed, for both Sunday clauses: Temporal_of.temporal tags every Sunday of Advent/Lent/Easter Sollemnitas (the privileged-season convention), so an observed.rank test alone would wrongly grant Gloria on an Advent or Lent Sunday, which n. 53's own "extra tempus Adventus et Quadragesimae" explicitly withholds. Credo is deliberately narrower than Gloria: n. 68 has no "et festis" clause, so an ordinary Festum does not get the Creed, unlike EF's RG 475(b)/(c). Two Missale-proper exceptions (Holy Thursday evening Mass, the Easter Vigil Mass) are cited from the Missal's own "Dicitur Gloria in excelsis" rubric, not the IGMR, which is silent on both. The Requiem question is likewise IGMR-silent (nn. 379-385 say nothing about Gloria/Credo); flagged as an inference from the Missal's own consistent marker pattern, applied only to the one Requiem-shaped slug the shipped OF data can construct (all-souls), the same closed-population discipline Rubrics_ef's own Colour.Black proxy uses. preface returns None unconditionally, a measured verdict rather than a stub. The module header transcribes the 2002 Missal's own "INDEX PRAEFATIONUM" in full: nine of Preface.t's 15 EF-cited constructors name an OF occasion with multiple distinct texts (Nativity x3, Lent x4-9, Easter x5, Ascension x2, Holy_spirit x2, Bvm x5, Apostles x2, Common split across two independent 7/8-member families, Requiem x5) -- structurally lossy to collapse into one constructor. The other six (Epiphany, Trinity, Sacred_heart, Christ_the_king, St_joseph, Holy_cross) match cardinality with a single OF text apiece, and were checked and rejected anyway: Preface.t's own constructors are RG-cited by construction, and IGMR n. 364's own "quibus Missale Romanum ditatur" (prefaces the Missal is ENRICHED with) is direct textual evidence the 2002 collection is not a bare carry-over -- corroborated directly for Sacred Heart, whose OF text was read and found to be a different composition from its RG 491 namesake. Reusing any of the six on cardinality and theme alone would be the exact "sounds similar" trap the task brief warns against. test/test_rubrics_of.ml: 26 table-driven cases. Every temporal-origin row builds observed via Temporal_of.temporal on a real date; every sanctoral-origin row (a solemnity in Lent, an ordinary Festum, a weekday obligatory memorial, All Souls) pairs a real Temporal_of.temporal result with an observed record copied verbatim from data/of/calendar-2002.sexp, since no full OF Rite.t/Calendar pipeline exists yet to derive one. Includes same-date Gloria/Credo pairs that prove the two rules genuinely diverge (a Sunday of Advent, an ordinary Festum) rather than one silently mirroring the other. dune test: exit 0, 845 tests. make check (COLITUR_EXHAUSTIVE_SWEEP=1, full 1583-9999 sweep): exit 0, 851 tests, 303.6s, both run unpiped in the foreground. git diff --stat v1.0.0..HEAD -- lib/rites/rite_ef/ lib/kernel/ data/ef/ is empty.
-rw-r--r--lib/rites/rite_of/rubrics_of.ml290
-rw-r--r--lib/rites/rite_of/rubrics_of.mli78
-rw-r--r--test/test_colitur.ml1
-rw-r--r--test/test_rubrics_of.ml307
4 files changed, 676 insertions, 0 deletions
diff --git a/lib/rites/rite_of/rubrics_of.ml b/lib/rites/rite_of/rubrics_of.ml
new file mode 100644
index 0000000..3fa59e9
--- /dev/null
+++ b/lib/rites/rite_of/rubrics_of.ml
@@ -0,0 +1,290 @@
+(* Institutio Generalis Missalis Romani, editio typica tertia (2002)
+ (docs/research/of/igmr-2002-latin.pdf; extracted with `pdftotext
+ docs/research/of/igmr-2002-latin.pdf`, line numbers below are that
+ extraction's own line numbers) -- CITATION DISCIPLINE, restated from
+ vocab_of.ml/temporal_of.ml: the Missale Romanum PDF (docs/research/of/
+ missale-romanum-2002.pdf) carries BOTH the Normae universales and the
+ IGMR, each independently numbered from 1. Every citation below names
+ "IGMR n. N" for that reason, never a bare "n. N". Gloria and Credo are
+ IGMR questions (Ordo Missae structure); Normae has nothing to say about
+ either.
+
+ GLORIA (IGMR n. 53, igmr.txt:456-460), quoted in full:
+
+ "53. Gloria est antiquissimus et venerabilis hymnus, quo Ecclesia, in
+ Spiritu Sancto congregata, Deum Patrem atque Agnum glorificat eique
+ supplicat. Huius hymni textus cum alio commutari nequit. [...]
+ Cantatur autem vel dicitur diebus dominicis extra tempus Adventus et
+ Quadragesimae, necnon in sollemnitatibus et festis, et in peculiaribus
+ celebrationibus sollemnioribus."
+
+ So: every Sunday OUTSIDE Advent and Lent; every Sollemnitas; every
+ Festum; and, permissively ("in peculiaribus celebrationibus
+ sollemnioribus" -- "in particular more solemn celebrations", "dicitur"
+ not "dicendum est" elsewhere in this paragraph's own register), certain
+ occasions this engine has no dimension to model at all -- the same
+ "votive Mass class" scope limit {!Rite_ef.Rubrics_ef}'s own header
+ states for RG 431(d)/(e)/432(c) -- genuinely N/A, not silently skipped.
+
+ CREDO / SYMBOLUM (IGMR n. 67-68, igmr.txt:568-577), quoted in full:
+
+ "67. Symbolum seu professio fidei eo tendit ut universus populus
+ congregatus verbo Dei in lectionibus e sacra Scriptura nuntiato et per
+ homiliam exposito respondeat [...]
+ 68. Symbolum cantandum vel dicendum est a sacerdote cum populo diebus
+ dominicis et in sollemnitatibus; dici potest etiam in peculiaribus
+ celebrationibus sollemnioribus."
+
+ NARROWER than n. 53, on purpose, not by oversight: n. 68 names only
+ "dominicis et... sollemnitatibus" -- NO "et festis" clause at all, unlike
+ n. 53's explicit third member. An ordinary [Festum] (the Presentation,
+ the Transfiguration, the Exaltation of the Holy Cross...) therefore does
+ NOT get the Credo, exactly matching real practice (only Sundays and
+ solemnities carry it) and confirmed by the literal absence of the word
+ "festis" from n. 68 where n. 53 has it -- checked directly, not assumed.
+ The trailing "dici potest... peculiaribus... sollemnioribus" is the same
+ permissive, unmodelled N/A clause as n. 53's.
+
+ Neither n. 53 nor n. 68 carries an Easter-relative WINDOW the way RG
+ 475(d)'s three EF octaves do (Nativity/Easter/Pentecost) -- checked by
+ reading both paragraphs in full above, not merely by their absence from
+ this file's own branches: nothing in IGMR nn. 52-69 (the whole "Actus
+ paenitentialis" through "Oratio universalis" span, igmr.txt:440-585)
+ mentions an octave at all. [credo]'s own [date] parameter is therefore
+ genuinely unread, kept only for {!Colitur_kernel.Rite.t}'s shared shape
+ -- an honest [None]-shaped fact about this rubric, not a gap.
+
+ THE REQUIEM QUESTION -- IGMR IS GENUINELY SILENT, FLAGGED AS SUCH. RG
+ 476(f)/432(d) name "in Missis defunctorum" explicitly for EF; IGMR nn.
+ 379-385 ("II. DE MISSIS DEFUNCTORUM", igmr.txt:2327-2349), read in full,
+ say nothing at all about the Gloria or the Symbolum -- confirmed by
+ reading every line of that section, not by a failed grep. The Missale's
+ own printed propers corroborate the OMISSION reading only indirectly:
+ every OTHER Mass text in the extracted Proper of Time/Sanctoral carries
+ an explicit "Dicitur Gloria in excelsis."/"Non dicitur..." marker (over
+ 150 occurrences, docs/research/of/missale-romanum-2002.pdf via
+ `pdftotext -layout`), but the two "IN COMMEMORATIONE OMNIUM FIDELIUM
+ DEFUNCTORUM" (2 November) Mass-text blocks carry NEITHER marker --
+ consistent with (not proof of) universal, uncontested funeral-liturgy
+ practice that a Requiem Mass never has either. Picked as the reading
+ with the better textual support, per this task's own brief, and applied
+ as narrowly as the evidence allows: {!requiem_slugs} is a CLOSED,
+ directly-checked list of the ONE slug in data/of/calendar-2002.sexp this
+ can ever apply to (grepped for "defunct"/"requiem"/"exsequial": zero
+ other hits), the same "checked, not assumed, closed population"
+ discipline {!Rite_ef.Rubrics_ef}'s own [Colour.Black] proxy uses for the
+ identical EF question. *)
+
+open Colitur_kernel
+
+let requiem_slugs = [ "all-souls" ]
+
+let is_requiem (observed : Vocab_of.rank Celebration.t) =
+ List.mem (Slug.to_string observed.Celebration.slug) requiem_slugs
+
+let gloria ~(temporal : (Vocab_of.season, Vocab_of.rank) Temporal.t)
+ ~(observed : Vocab_of.rank Celebration.t) ~(date : Date.t) : bool =
+ let easter = Computus.gregorian_easter (Date.year date) in
+ let n = Date.to_rata date - Date.to_rata easter in
+ if is_requiem observed then false
+ else if
+ (* Missale Romanum, "FERIA V IN CENA DOMINI", Ad Missam vespertinam, n.
+ 7 (docs/research/of/missale-romanum-2002.pdf, pdftotext -layout,
+ line ~8534): "Dicitur Gloria in excelsis. Dum cantatur hymnus,
+ pulsantur campanae, eoque expleto, silent usque ad Gloria in
+ excelsis Vigiliae paschalis..." -- the Missal's own PROPER-OF-TIME
+ rubric for the Mass of the Lord's Supper (Easter-3) explicitly
+ requires the Gloria despite the day sitting inside Lent (Tabula I.1
+ territory once the evening Mass begins; Precedence_of.band's own
+ comment on WHY the civil day itself stays a Holy Week feria, not the
+ Triduum, is unaffected -- this rubric is keyed to the Mass, not the
+ day's own band), and the same sentence extends the silence "usque
+ ad" (UP TO) the Easter Vigil's own Gloria (Easter-1), confirming
+ that Mass also has one. Neither day is ever a Sunday (both are fixed
+ Easter-offsets within Holy Week), so this must be checked ahead of,
+ and independently of, the season/weekday branches below -- the
+ identical lex-specialis placement {!Rite_ef.Rubrics_ef.gloria}'s own
+ RG 431(c) branch uses for the same two days. *)
+ n = -3 || n = -1
+ then true
+ else if
+ (* IGMR n. 53: "extra tempus Adventus et Quadragesimae" -- read off
+ [temporal], never [observed]: Temporal_of.temporal tags every Sunday
+ of Advent, Lent AND Easter [Sollemnitas] (temporal_of.ml, [build]'s
+ own [match s with Advent | Lent | Easter -> Sollemnitas | ...]), so
+ an [observed.rank] test alone would wrongly grant an Advent or Lent
+ Sunday the Gloria this clause explicitly withholds -- confirmed by
+ Palm Sunday specifically ([Vocab_of.Lent], [Sollemnitas],
+ [named]'s own entry): real, uncontested practice omits the Gloria
+ there, and only a temporal-read guard gets that right. *)
+ temporal.Temporal.weekday = Date.Sun
+ && (temporal.Temporal.season = Vocab_of.Advent || temporal.Temporal.season = Vocab_of.Lent)
+ then false
+ else if
+ (* IGMR n. 53: every remaining Sunday (Christmas time, Easter, Ordinary
+ Time -- outside Advent/Lent by the branch just above). Read off
+ [temporal.weekday], not [observed.rank]: an ordinary Sunday per
+ annum is [Festum]-ranked (temporal_of.ml, the same [build] match
+ above), not [Sollemnitas], and would otherwise only reach a [true]
+ answer via the [Festum] arm below -- redundant for it, but this
+ branch is what makes an Easter- or Christmas-season Sunday's [true]
+ answer trace to n. 53's OWN "diebus dominicis" clause rather than
+ accidentally to its "et festis" clause. *)
+ temporal.Temporal.weekday = Date.Sun
+ then true
+ else
+ (* IGMR n. 53: "in sollemnitatibus et festis" -- every remaining
+ Sollemnitas or Festum, whatever day of the week it falls on (a
+ solemnity transferred onto an ordinary weekday, or one that was
+ never a Sunday to begin with -- St Joseph 19 March, the Annunciation
+ 25 March, both always inside Lent by construction: Gregorian
+ Easter's earliest possible date is 22 March, so Ash Wednesday can
+ never fall later than 10 March and Easter itself can never fall
+ before 19 March, meaning neither date can ever fall outside Lent).
+ [Memoria_obligatoria], [Memoria_ad_libitum] and [Feria] are not
+ named by n. 53 at all and correctly fall through to [false]. *)
+ observed.Celebration.rank = Vocab_of.Sollemnitas || observed.Celebration.rank = Vocab_of.Festum
+
+let credo ~(temporal : (Vocab_of.season, Vocab_of.rank) Temporal.t)
+ ~(observed : Vocab_of.rank Celebration.t) ~(date : Date.t) : bool =
+ ignore date;
+ (* IGMR n. 67-68's own [date]-independence: see this file's own header. *)
+ if is_requiem observed then false
+ else if
+ (* IGMR n. 68: "diebus dominicis" -- EVERY Sunday, Advent and Lent
+ included (unlike [gloria]'s own n. 53 carve-out: n. 68 has no such
+ exception in its text, checked directly). Read off [temporal], not
+ [observed], for the same reason [gloria]'s own Sunday branch is:
+ an ordinary (non-privileged-season) Sunday per annum is [Festum]-
+ ranked, and n. 68's own [Sollemnitas] branch below would silently
+ miss it if this branch were dropped. *)
+ temporal.Temporal.weekday = Date.Sun
+ then true
+ else
+ (* IGMR n. 68: "in sollemnitatibus". NO "et festis" here -- the one
+ place this function's shape most visibly narrows on EF's RG
+ 475(b)/(c): a bare [Festum] (the Presentation, the Transfiguration,
+ a I-class-shaped feast of the Lord with no Sunday privilege) does
+ NOT get the Credo. [Memoria_obligatoria]/[Memoria_ad_libitum]/
+ [Feria] fall through to [false], matching real practice (an
+ obligatory memorial on a weekday never carries the Creed). *)
+ observed.Celebration.rank = Vocab_of.Sollemnitas
+
+(* PREFACE CHOICE (IGMR n. 364-365, igmr.txt:2244-2263) -- quoted in full:
+
+ "364. Plurimae praefationes, quibus Missale Romanum ditatur, eo
+ spectant ut argumenta gratiarum actionis in Prece eucharistica plenius
+ eniteant, et variae rationes mysterii salutis pleniore luce
+ proponantur.
+ 365. Electio inter Preces eucharisticas, quae in Ordine Missae
+ inveniuntur, his normis opportune regitur: [...]"
+
+ n. 364-365 govern choosing among the FOUR EUCHARISTIC PRAYERS (I-IV),
+ NOT which preface accompanies whichever Mass is said -- a different
+ question this engine has no need to answer (colitur resolves a Mass's
+ readings/rubrics, never which of the interchangeable Canons a celebrant
+ picks). Genuinely N/A, cited here only because n. 364-365 is the
+ nearest thing to a "how is the Eucharistic Prayer/preface chosen"
+ numbered rubric in the whole IGMR, and this file should say plainly
+ that it is not the rule this function needs, rather than leave a reader
+ to wonder why it was not used.
+
+ UNLIKE RG 482-499, the IGMR carries NO numbered rubric governing preface
+ ASSIGNMENT at all -- confirmed by reading nn. 1-386 in full, not merely
+ by a failed grep for "praefatio" (which itself found zero hits: the
+ word never appears with that spelling anywhere in
+ docs/research/of/igmr-2002-latin.pdf's extracted text; every occurrence
+ in the Missale PDF instead uses the ligature "praefationem"/"praefatio"
+ spelt with <ae> or the printed <æ>, confirming this is a genuine textual
+ absence, not an extraction artifact). Preface assignment in the 2002
+ editio typica tertia is done PER MASS, inline throughout the Proper of
+ Time, Proper of Saints and Commons (each Mass's own rubric names its
+ preface directly, e.g. "Praefatio de Nativitate, pp. 520-522.") -- the
+ collection itself is indexed at the Missal's own back matter under
+ "INDEX PRAEFATIONUM" (missale-romanum-2002.pdf, pdftotext -layout,
+ extracted lines ~34107-34247), transcribed here in full because it is
+ the evidence the verdict below rests on:
+
+ A. De Tempore: Advent I-II (2); Nativity I-III (3); Epiphany (1);
+ Lent ("de Quadragesima") I-IV plus five Sunday-specific variants
+ V-IX (9); Passion ("de Passione Domini") I-III, III being Palm
+ Sunday's own (3); Paschal (Easter) I-V (5); Ascension I-II, BOTH
+ titled "De mysterio Ascensionis" (2); Pentecost (1); Sundays "per
+ annum" I-VIII (8); Praefationes communes I-VII (7).
+ B. In festis et mysteriis Domini: Trinity (1); the Annunciation's own
+ "De mysterio Incarnationis" (1); the Baptism of the Lord (1);
+ Sacred Heart (1); "De victoria Crucis gloriosae", 14 September (1);
+ Corpus Christi ("de Ss.ma Eucharistia") I-II (2); the Presentation
+ (1); Christ the King (1); the Transfiguration (1); the Dedication
+ of a Church I-II (2); "de Spiritu Sancto" I-II (2).
+ C. In festis sanctorum: the BVM I-V (5); the Angels (1); the
+ Precursor/John the Baptist (1); St Joseph, "De missione S.
+ Ioseph", 19 March/1 May (1); Peter-and-Paul (1); Apostles I-II
+ (2); Saints I-III (3); Martyrs I-II (2); Pastors (1); Virgins and
+ Religious (1).
+ Pro defunctis (Requiem): I-V (5).
+
+ AGAINST THIS CENSUS, {!Preface.t}'s 15 constructors split into two
+ failure shapes, and every single one fails one of them -- NONE is safe
+ to reuse:
+
+ (1) NINE constructors name an OF occasion with MULTIPLE distinct texts:
+ [Nativity] (3 OF variants), [Lent] (4, or 9 counting the
+ Sunday-specific set), [Easter] (5), [Ascension] (2), [Holy_spirit]
+ (2), [Bvm] (5), [Apostles] (2), [Common] (15, split across two
+ independent numbered families -- "per annum" I-VIII and
+ "communes" I-VII, neither of which [Preface.t]'s single [Common]
+ constructor could ever pick between), [Requiem] (5). Returning any
+ one of them would silently pick ONE of several genuinely different
+ texts and call it "the" answer -- structurally lossy, independent
+ of any wording question.
+
+ (2) SIX constructors name an OF occasion with exactly ONE text, the same
+ cardinality EF has for it -- [Epiphany], [Trinity], [Sacred_heart],
+ [Christ_the_king], [St_joseph], and [Holy_cross] (EF's RG 487 "de
+ sancta Cruce" against OF's single "De victoria Crucis gloriosae",
+ both keyed to 14 September). These are the genuinely tempting
+ cases -- same feast, same "exactly one" cardinality on both sides --
+ and each was checked and rejected anyway, on TWO grounds:
+ (a) {!Preface.t}'s own header ties every constructor to an RG
+ paragraph ("Nativity RG 484", "Trinity RG 494", ...) -- it
+ denotes THAT SPECIFIC EF TEXT, not an abstract cross-rite
+ theme, by its own documented contract.
+ (b) IGMR n. 364's own opening word choice is direct evidence the
+ 2002 Missal does not consider its preface collection a bare
+ carry-over: "praefationes, QUIBUS Missale Romanum DITATUR"
+ ("the prefaces WITH WHICH the Missal is ENRICHED") -- language
+ describing an addition/expansion, not an inheritance,
+ corroborating the well-documented fact that the post-1970
+ preface collection was substantially newly composed, the
+ Sacred Heart preface actually read while building this
+ module ("Qui, mira caritate, exaltatus in cruce... de
+ transfixo latere sanguinem fudit et aquam...",
+ missale-romanum-2002.pdf line ~30575) being the one
+ instance directly compared line-by-line against what its RG
+ 491 EF namesake is known to open with, and found to be a
+ different composition, not merely a different printing of
+ the same one.
+ Reusing any of the six on the strength of matching cardinality and
+ matching feast alone would be exactly the "sounds similar" trap
+ this task's own brief warns against -- confidently wrong output
+ every test in this suite would agree with, because nothing in this
+ codebase yet stores preface TEXT to catch the mismatch against.
+
+ VERDICT: [preface] returns [None] unconditionally. This is not a stub
+ -- every branch above was checked and its failure mode recorded -- and
+ it is not a defect in {!Preface.t} either: that type was built for one
+ rite and correctly refuses to pretend it fits a second one it was never
+ shown. The finding worth carrying forward (task brief's own "spec §7"
+ framing): OF preface selection needs its own type, most plausibly one
+ with a per-occasion NUMBERED-VARIANT dimension ("Nativity of 1 | 2 | 3",
+ ...) rather than {!Preface.t}'s flat enumeration, plus its own IGMR/
+ Missale-cited population census -- table-driven, the same shape as
+ {!requiem_slugs} above, once a task is actually chartered to build it.
+ Building that type is explicitly out of this task's own scope. *)
+let preface ~(temporal : (Vocab_of.season, Vocab_of.rank) Temporal.t)
+ ~(observed : Vocab_of.rank Celebration.t) ~(date : Date.t) : Preface.t option =
+ ignore temporal;
+ ignore observed;
+ ignore date;
+ None
diff --git a/lib/rites/rite_of/rubrics_of.mli b/lib/rites/rite_of/rubrics_of.mli
new file mode 100644
index 0000000..5325f12
--- /dev/null
+++ b/lib/rites/rite_of/rubrics_of.mli
@@ -0,0 +1,78 @@
+(** IGMR n. 53 (Gloria), n. 67-68 (Symbolum/Credo), n. 364-365 and the
+ Missale Romanum editio typica tertia (2002)'s own printed "INDEX
+ PRAEFATIONUM" (preface choice) -- see the .ml's own header for every
+ paragraph quoted in full and every branch's own citation. The OF
+ counterpart of {!Rite_ef.Rubrics_ef}; same shape, same three functions,
+ same [Rite.t] signatures -- but see [preface]'s own citation for why
+ this file's verdict on {!Colitur_kernel.Preface.t} differs sharply from
+ what {!Rite_ef.Rubrics_ef.preface} was able to build. *)
+
+open Colitur_kernel
+
+(** The one Requiem-shaped entity the shipped OF calendar can ever construct
+ as [observed] (All Souls, 2 November) -- see the .ml's own header for
+ why colour cannot serve as the Requiem proxy here the way
+ {!Colour.Black} does for {!Rite_ef.Rubrics_ef} (OF permits, but never
+ requires, black vestments for the dead -- IGMR n. 346 e -- and the
+ shipped data uses violet), so a closed slug list, checked against
+ [data/of/calendar-2002.sexp] directly, is the only signal available.
+ Exposed so the test suite can assert this population stays exactly
+ what it was measured to be, the same discipline
+ {!Rite_ef.Rubrics_ef.creed_apostle_slugs} already establishes. *)
+val requiem_slugs : string list
+
+(** Whether the Gloria in excelsis is said at this day's Mass (IGMR n. 53).
+ [temporal] is read for exactly one fact -- n. 53's own "diebus dominicis
+ EXTRA TEMPUS ADVENTUS ET QUADRAGESIMAE": {!Rite_of.Temporal_of.temporal}
+ tags every Sunday of Advent/Lent/Easter [Sollemnitas] (the same rank a
+ genuine solemnity carries), so testing [observed.rank] alone would
+ wrongly grant an Advent or Lent Sunday the Gloria n. 53 explicitly
+ denies it -- the identical "read temporal, not observed, for a day-of-
+ week/season fact" discipline {!Rite_ef.Rubrics_ef.creed}'s own RG 475(a)
+ citation already establishes. [date] is read once, against the rite's
+ own (Gregorian) Easter, for the two Missale-Romanum-own-rubric
+ exceptions (Holy Thursday evening Mass, the Easter Vigil Mass) -- see
+ the .ml's own citation for the primary text, found in the Missal's
+ Proper of Time rather than the IGMR itself. [observed]'s [slug] is
+ checked once against {!requiem_slugs} -- see that value's own citation
+ for why this is an INFERENCE, not an IGMR paragraph, and exactly how
+ narrowly it is applied. *)
+val gloria :
+ temporal:(Vocab_of.season, Vocab_of.rank) Temporal.t ->
+ observed:Vocab_of.rank Celebration.t ->
+ date:Date.t ->
+ bool
+
+(** Whether the Symbolum (Credo) is said, post-homily, at this day's Mass
+ (IGMR n. 67-68). Reads [temporal] for the identical reason [gloria]
+ does -- n. 68's own "diebus dominicis" is a plain, weekday-only fact,
+ unlike n. 53's Advent/Lent carve-out, so every Sunday qualifies
+ (Advent/Lent Sundays INCLUDED, unlike [gloria]) -- and [observed]'s
+ [rank] for n. 68's own "in sollemnitatibus", and [slug] against
+ {!requiem_slugs} for the identical reason [gloria] checks it. Narrower
+ than [gloria] on purpose: n. 68 has no "et festis" clause at all, so a
+ [Festum]-rank day (an ordinary feast) does NOT get the Credo -- the one
+ place this function's shape most visibly parts from EF's own RG
+ 475(b)/(c). [date] is accepted, for signature symmetry with [gloria]/
+ [preface] and {!Rite.t}'s own shared shape, but genuinely unread: n.
+ 67-68 carries no Easter-relative window of its own, unlike RG 475(d)'s
+ three EF octaves -- checked, not assumed (see the .ml's own citation). *)
+val credo :
+ temporal:(Vocab_of.season, Vocab_of.rank) Temporal.t ->
+ observed:Vocab_of.rank Celebration.t ->
+ date:Date.t ->
+ bool
+
+(** Which preface is said at this day's Mass (IGMR n. 364-365; the Missale's
+ own "INDEX PRAEFATIONUM"). ALWAYS [None] -- not a stub, a measured
+ verdict: see the .ml's own header for the full census of the 2002
+ Missal's preface collection (over 50 individual texts across ~30 named
+ occasions) against {!Preface.t}'s 15 EF-cited constructors, and why
+ none of them is safe to reuse. Same three parameters as [gloria]/
+ [credo], all three genuinely unread -- kept only for {!Rite.t}'s shared
+ shape. *)
+val preface :
+ temporal:(Vocab_of.season, Vocab_of.rank) Temporal.t ->
+ observed:Vocab_of.rank Celebration.t ->
+ date:Date.t ->
+ Preface.t option
diff --git a/test/test_colitur.ml b/test/test_colitur.ml
index 98e08fb..3e16566 100644
--- a/test/test_colitur.ml
+++ b/test/test_colitur.ml
@@ -18,6 +18,7 @@ let () =
Test_calendar.suite; Test_precedence_ef.suite; Test_sanctoral_ef.suite; Test_calendar_of_data.suite;
Test_rite_ef.suite;
Test_rubrics_ef.suite;
+ Test_rubrics_of.suite;
Test_differential.suite; Test_oracle.suite; Test_oracle.suite_2038; Test_oracle.suite_2035;
Test_lms_ordo.suite_2023_2024; Test_lms_ordo.suite_2024_2025; Test_lms_ordo.suite_2025_2026;
Test_lms_ordo.suite_allow_list; Test_fiuv_ordo.suite;
diff --git a/test/test_rubrics_of.ml b/test/test_rubrics_of.ml
new file mode 100644
index 0000000..db8744a
--- /dev/null
+++ b/test/test_rubrics_of.ml
@@ -0,0 +1,307 @@
+(* IGMR n. 53 (Gloria), n. 67-68 (Symbolum/Credo), n. 364-365 and the
+ Missale's own INDEX PRAEFATIONUM -- see lib/rites/rite_of/rubrics_of.ml
+ for every rubric quoted in full and every branch's own citation. The OF
+ counterpart of test/test_rubrics_ef.ml; one Alcotest case per rule.
+
+ NO FULL OF [Colitur_kernel.Rite.t]/[Calendar] EXISTS YET (this task
+ supplies three of its four remaining fields; wiring them into a bundle
+ is a later task) -- so, unlike test_rubrics_ef.ml, there is no
+ `Cal.day`-style pipeline here to resolve a real SANCTORAL winner for a
+ date. Every TEMPORAL-origin row below therefore builds [temporal] (and,
+ where the temporal cycle's own office IS what is observed -- an ordinary
+ Sunday, a feria, a named solemnity like the Nativity) also [observed],
+ via [Temporal_of.temporal] on a real calendar date -- genuine Phase 1
+ output, per the task brief. Every SANCTORAL-origin row (a saint's
+ solemnity/feast/memorial outranking the day's own temporal office)
+ builds [temporal] the same way, via a real date, but necessarily builds
+ [observed] directly -- there is no pipeline yet to derive it. It is
+ NOT fabricated: every field is copied verbatim from the matching real
+ entry in data/of/calendar-2002.sexp (cited at each site), the same
+ "grounded in shipped data, not invented" discipline
+ test/test_precedence_of.ml's own [mk_cel] already establishes for this
+ rite. *)
+
+module RO = Rite_of.Rubrics_of
+module T = Rite_of.Temporal_of
+module D = Colitur_kernel.Date
+module Cel = Colitur_kernel.Celebration
+module Colour = Colitur_kernel.Colour
+module Subject = Colitur_kernel.Subject
+module Slug = Colitur_kernel.Slug
+module Vocab_of = Rite_of.Vocab_of
+
+let mk y m d = match D.make ~year:y ~month:m ~day:d with Ok t -> t | Error e -> failwith e
+
+(* A real, shipped sanctoral entry (data/of/calendar-2002.sexp), reused
+ directly as [observed] where no full precedence pipeline exists yet to
+ derive it. Status defaults to [Feast] (every entry cited below is). *)
+let mk_saint ~slug ~rank ~colour ~subject () =
+ Cel.make ~slug:(Slug.of_string_exn slug) ~rank ~colour ~subject ~layer:"of-universal" ()
+
+(* ---- Gloria (IGMR n. 53) ---- *)
+
+let gloria_on_temporal y m d =
+ let temporal = T.temporal (mk y m d) in
+ RO.gloria ~temporal ~observed:temporal.Colitur_kernel.Temporal.office ~date:(mk y m d)
+
+let check_gloria name expected actual = Alcotest.(check bool) name expected actual
+
+let test_gloria_advent_sunday () =
+ (* 2026-12-06: the Second Sunday of Advent -- confirmed via `date -d`,
+ [Temporal_of.temporal] tags it [Vocab_of.Sollemnitas] (the privileged-
+ season convention, temporal_of.ml's own [build]), which is exactly the
+ trap n. 53's own "extra tempus Adventus" clause exists to catch: a
+ naive [observed.rank = Sollemnitas] test would wrongly say [true]. *)
+ check_gloria "no Gloria on a Sunday of Advent" false (gloria_on_temporal 2026 12 6)
+
+let test_gloria_lent_sunday () =
+ (* 2026-03-01: the First Sunday of Lent 2026 (Ash Wednesday 2026-02-18,
+ Easter 2026-04-05). Same trap as Advent, the other of n. 53's two
+ named exceptions. *)
+ check_gloria "no Gloria on a Sunday of Lent" false (gloria_on_temporal 2026 3 1)
+
+let test_gloria_palm_sunday () =
+ (* 2026-03-29: Palm Sunday, [Vocab_of.Lent], [Sollemnitas] (named's own
+ entry) -- confirms the Advent/Lent exclusion above also correctly
+ reaches a NAMED solemnity, not only a plain numbered Sunday of Lent,
+ matching real, uncontested practice (Palm Sunday's Mass has no
+ Gloria). *)
+ check_gloria "no Gloria on Palm Sunday" false (gloria_on_temporal 2026 3 29)
+
+let test_gloria_ordinary_sunday () =
+ (* 2026-01-25: an Ordinary Time Sunday (Baptism of the Lord 2026-01-11,
+ so Ordinary Time begins 2026-01-12) -- [Festum]-ranked
+ (temporal_of.ml's own [build]), reaching [true] via n. 53's "diebus
+ dominicis" clause, not its "et festis" one (see the .ml's own
+ citation for why [temporal.weekday], not [observed.rank], is what
+ this must be read off). *)
+ check_gloria "Gloria on a Sunday per annum" true (gloria_on_temporal 2026 1 25)
+
+let test_gloria_christmas_sunday () =
+ (* 2026-12-27: a Sunday within the Octave of the Nativity (temporal_of.ml's
+ own [holy_family]/generic-Sunday dispatch), [Vocab_of.Christmas]
+ season, outside Advent/Lent -- a second, independent witness to the
+ same "diebus dominicis extra Adventum/Quadragesimam" clause as the
+ Ordinary Time row above, from the other season n. 53 does not
+ exclude. *)
+ check_gloria "Gloria on a Sunday of Christmas time" true (gloria_on_temporal 2026 12 27)
+
+let test_gloria_nativity () =
+ (* 2026-12-25: the Nativity itself -- [Vocab_of.Christmas], [Sollemnitas],
+ a Friday (never a Sunday that year), reaching [true] via n. 53's "in
+ sollemnitatibus" clause on its own, independent of the Sunday
+ clauses. Pure [Temporal_of.temporal] output; no sanctoral data
+ needed, since the Nativity IS the temporal cycle's own named office
+ (temporal_of.ml's own [named]). *)
+ check_gloria "Gloria on the Nativity" true (gloria_on_temporal 2026 12 25)
+
+let test_gloria_holy_thursday () =
+ (* 2026-04-02: the Mass of the Lord's Supper (Easter-3, Easter 2026-04-05)
+ -- Missale Romanum, "FERIA V IN CENA DOMINI" n. 7's own explicit
+ "Dicitur Gloria in excelsis" (see the .ml's own citation), overriding
+ what would otherwise be a Lenten-feria [false]. *)
+ check_gloria "Gloria at the Mass of the Lord's Supper" true (gloria_on_temporal 2026 4 2)
+
+let test_gloria_easter_vigil () =
+ (* 2026-04-04: the Easter Vigil (Easter-1) -- the same Missale rubric's
+ own "usque ad Gloria in excelsis Vigiliae paschalis", confirming this
+ Mass also has one. *)
+ check_gloria "Gloria at the Easter Vigil Mass" true (gloria_on_temporal 2026 4 4)
+
+let test_gloria_solemnity_in_lent () =
+ (* Saint Joseph, Husband of the Blessed Virgin Mary, 19 March --
+ data/of/calendar-2002.sexp: (rank Sollemnitas) (colour White)
+ (subject Saint). 2026-03-19 is a Thursday (Easter 2026-04-05, so Holy
+ Week is 2026-03-29..04-04 -- this date sits safely inside ordinary
+ Lent, not impeded) -- confirmed, not assumed, via `date -d`. 19 March
+ is ALWAYS inside Lent by construction (Gregorian Easter's earliest
+ possible date is 22 March, so Ash Wednesday can never fall later than
+ 10 March; see the .ml's own citation). Exercises n. 53's "in
+ sollemnitatibus" clause on a genuine sanctoral winner, the one row in
+ this suite [observed] cannot come from [Temporal_of.temporal] alone
+ -- see this file's own header for why. *)
+ let temporal = T.temporal (mk 2026 3 19) in
+ let observed =
+ mk_saint ~slug:"joseph-husband-of-the-blessed-virgin-mary" ~rank:Vocab_of.Sollemnitas
+ ~colour:Colour.White ~subject:Subject.Saint ()
+ in
+ check_gloria "Gloria: a solemnity falling in Lent" true
+ (RO.gloria ~temporal ~observed ~date:(mk 2026 3 19))
+
+let test_gloria_festum () =
+ (* Chair of Saint Peter, apostle, 22 February -- data/of/calendar-2002.sexp:
+ (rank Festum) (colour White) (subject Saint). 2027-02-22 is a Monday
+ (checked via `date -d`, so this date is not itself impeded by an
+ Ordinary Time Sunday outranking a Festum, Tabula entries 6 vs 7).
+ Exercises n. 53's "et festis" clause specifically -- the one member of
+ n. 53's list that n. 68 (Credo, below) does NOT share. *)
+ let temporal = T.temporal (mk 2027 2 22) in
+ let observed =
+ mk_saint ~slug:"chair-of-saint-peter-apostle" ~rank:Vocab_of.Festum ~colour:Colour.White
+ ~subject:Subject.Saint ()
+ in
+ check_gloria "Gloria on an ordinary Festum" true (RO.gloria ~temporal ~observed ~date:(mk 2027 2 22))
+
+let test_gloria_obligatory_memorial () =
+ (* Saint Aloysius Gonzaga, religious, 21 June -- data/of/calendar-2002.sexp:
+ (rank Memoria_obligatoria) (colour White) (subject Saint). 2027-06-21
+ is a Monday. Neither n. 53's "sollemnitatibus et festis" nor its
+ "dominicis" clause names a memorial. *)
+ let temporal = T.temporal (mk 2027 6 21) in
+ let observed =
+ mk_saint ~slug:"aloysius-gonzaga-religious" ~rank:Vocab_of.Memoria_obligatoria
+ ~colour:Colour.White ~subject:Subject.Saint ()
+ in
+ check_gloria "no Gloria on a weekday obligatory memorial" false
+ (RO.gloria ~temporal ~observed ~date:(mk 2027 6 21))
+
+let test_gloria_ordinary_feria () =
+ (* 2026-01-27: an Ordinary Time Tuesday feria, no memorial -- pure
+ [Temporal_of.temporal] output, [Vocab_of.Feria], [Subject.Temporal].
+ Named by neither n. 53 clause. *)
+ check_gloria "no Gloria on a plain feria" false (gloria_on_temporal 2026 1 27)
+
+let test_gloria_all_souls () =
+ (* All Souls, 2 November -- data/of/calendar-2002.sexp: (rank Sollemnitas)
+ (colour Violet) (subject Saint) -- the ONE Requiem-shaped entity this
+ engine's shipped OF data can construct as [observed]
+ ({!Rite_of.Rubrics_of.requiem_slugs}). Its own [rank] is tagged
+ [Sollemnitas] by the extractor's own override (precedence_of.ml's own
+ citation), so WITHOUT the requiem guard n. 53's "in sollemnitatibus"
+ clause would wrongly grant it [true] -- exactly the case this guard
+ exists for. 2026-11-02 is a Monday. *)
+ let temporal = T.temporal (mk 2026 11 2) in
+ let observed =
+ mk_saint ~slug:"all-souls" ~rank:Vocab_of.Sollemnitas ~colour:Colour.Violet ~subject:Subject.Saint ()
+ in
+ check_gloria "no Gloria on All Souls" false (RO.gloria ~temporal ~observed ~date:(mk 2026 11 2))
+
+(* ---- Credo / Symbolum (IGMR n. 67-68) ---- *)
+
+let credo_on_temporal y m d =
+ let temporal = T.temporal (mk y m d) in
+ RO.credo ~temporal ~observed:temporal.Colitur_kernel.Temporal.office ~date:(mk y m d)
+
+let check_credo name expected actual = Alcotest.(check bool) name expected actual
+
+let test_credo_advent_sunday () =
+ (* Same date as [test_gloria_advent_sunday], deliberately: n. 68 carries
+ no Advent/Lent exception at all, unlike n. 53 -- this is [true] here
+ where the identical day was [false] for Gloria, proving the two rules
+ genuinely diverge rather than one silently mirroring the other. *)
+ check_credo "Credo on a Sunday of Advent (unlike Gloria)" true (credo_on_temporal 2026 12 6)
+
+let test_credo_ordinary_sunday () =
+ check_credo "Credo on a Sunday per annum" true (credo_on_temporal 2026 1 25)
+
+let test_credo_solemnity () =
+ (* Same St Joseph data as [test_gloria_solemnity_in_lent]. *)
+ let temporal = T.temporal (mk 2026 3 19) in
+ let observed =
+ mk_saint ~slug:"joseph-husband-of-the-blessed-virgin-mary" ~rank:Vocab_of.Sollemnitas
+ ~colour:Colour.White ~subject:Subject.Saint ()
+ in
+ check_credo "Credo: a solemnity" true (RO.credo ~temporal ~observed ~date:(mk 2026 3 19))
+
+let test_credo_festum () =
+ (* Same Chair-of-Peter data as [test_gloria_festum] -- [false] here,
+ unlike Gloria's [true] on the identical day: n. 68 has no "et festis"
+ clause at all (see the .ml's own citation), the single sharpest
+ divergence between this function and [gloria]. *)
+ let temporal = T.temporal (mk 2027 2 22) in
+ let observed =
+ mk_saint ~slug:"chair-of-saint-peter-apostle" ~rank:Vocab_of.Festum ~colour:Colour.White
+ ~subject:Subject.Saint ()
+ in
+ check_credo "no Credo on an ordinary Festum (unlike Gloria)" false
+ (RO.credo ~temporal ~observed ~date:(mk 2027 2 22))
+
+let test_credo_weekday_obligatory_memorial () =
+ let temporal = T.temporal (mk 2027 6 21) in
+ let observed =
+ mk_saint ~slug:"aloysius-gonzaga-religious" ~rank:Vocab_of.Memoria_obligatoria
+ ~colour:Colour.White ~subject:Subject.Saint ()
+ in
+ check_credo "no Credo on a weekday obligatory memorial" false
+ (RO.credo ~temporal ~observed ~date:(mk 2027 6 21))
+
+let test_credo_ordinary_feria () =
+ check_credo "no Credo on a plain feria" false (credo_on_temporal 2026 1 27)
+
+let test_credo_all_souls () =
+ let temporal = T.temporal (mk 2026 11 2) in
+ let observed =
+ mk_saint ~slug:"all-souls" ~rank:Vocab_of.Sollemnitas ~colour:Colour.Violet ~subject:Subject.Saint ()
+ in
+ check_credo "no Credo on All Souls" false (RO.credo ~temporal ~observed ~date:(mk 2026 11 2))
+
+(* ---- Preface -- always [None]; see rubrics_of.ml's own header for the
+ full INDEX-PRAEFATIONUM census this rests on. One row per major class,
+ proving the function was deliberately built to reject every shape
+ rather than merely left unimplemented. ---- *)
+
+let check_preface_none name y m d =
+ let temporal = T.temporal (mk y m d) in
+ Alcotest.(check bool) name true
+ (RO.preface ~temporal ~observed:temporal.Colitur_kernel.Temporal.office ~date:(mk y m d) = None)
+
+let test_preface_none_nativity () = check_preface_none "preface: Nativity is None" 2026 12 25
+let test_preface_none_ordinary_sunday () = check_preface_none "preface: Sunday per annum is None" 2026 1 25
+let test_preface_none_feria () = check_preface_none "preface: a plain feria is None" 2026 1 27
+
+let test_preface_none_solemnity_with_own_single_text () =
+ (* Trinity Sunday -- exactly the "single OF text, same feast as an EF
+ constructor" near-miss the .ml's own header explains rejecting: still
+ [None], on purpose. *)
+ let easter = Colitur_kernel.Computus.gregorian_easter 2026 in
+ let trinity = D.add_days easter 56 in
+ let temporal = T.temporal trinity in
+ Alcotest.(check bool) "preface: Trinity Sunday is None" true
+ (RO.preface ~temporal ~observed:temporal.Colitur_kernel.Temporal.office ~date:trinity = None)
+
+let test_preface_none_all_souls () =
+ let temporal = T.temporal (mk 2026 11 2) in
+ let observed =
+ mk_saint ~slug:"all-souls" ~rank:Vocab_of.Sollemnitas ~colour:Colour.Violet ~subject:Subject.Saint ()
+ in
+ Alcotest.(check bool) "preface: All Souls is None" true
+ (RO.preface ~temporal ~observed ~date:(mk 2026 11 2) = None)
+
+(* Population guard, mirroring test_rubrics_ef.ml's own
+ [test_colour_black_population_is_exactly_two]: fails loudly the day a
+ second Requiem-shaped slug is added anywhere in the shipped OF data
+ without this list being revisited. *)
+let test_requiem_slugs_population () =
+ Alcotest.(check (list string)) "requiem_slugs is exactly {all-souls}" [ "all-souls" ] RO.requiem_slugs
+
+let suite =
+ ( "rubrics-of",
+ [ Alcotest.test_case "Gloria: no Gloria on a Sunday of Advent" `Quick test_gloria_advent_sunday;
+ Alcotest.test_case "Gloria: no Gloria on a Sunday of Lent" `Quick test_gloria_lent_sunday;
+ Alcotest.test_case "Gloria: no Gloria on Palm Sunday" `Quick test_gloria_palm_sunday;
+ Alcotest.test_case "Gloria: Gloria on a Sunday per annum" `Quick test_gloria_ordinary_sunday;
+ Alcotest.test_case "Gloria: Gloria on a Sunday of Christmas time" `Quick test_gloria_christmas_sunday;
+ Alcotest.test_case "Gloria: Gloria on the Nativity" `Quick test_gloria_nativity;
+ Alcotest.test_case "Gloria: Gloria at the Mass of the Lord's Supper" `Quick test_gloria_holy_thursday;
+ Alcotest.test_case "Gloria: Gloria at the Easter Vigil Mass" `Quick test_gloria_easter_vigil;
+ Alcotest.test_case "Gloria: a solemnity falling in Lent" `Quick test_gloria_solemnity_in_lent;
+ Alcotest.test_case "Gloria: an ordinary Festum" `Quick test_gloria_festum;
+ Alcotest.test_case "Gloria: no Gloria on a weekday obligatory memorial" `Quick
+ test_gloria_obligatory_memorial;
+ Alcotest.test_case "Gloria: no Gloria on a plain feria" `Quick test_gloria_ordinary_feria;
+ Alcotest.test_case "Gloria: no Gloria on All Souls" `Quick test_gloria_all_souls;
+ Alcotest.test_case "Credo: Credo on a Sunday of Advent" `Quick test_credo_advent_sunday;
+ Alcotest.test_case "Credo: Credo on a Sunday per annum" `Quick test_credo_ordinary_sunday;
+ Alcotest.test_case "Credo: a solemnity" `Quick test_credo_solemnity;
+ Alcotest.test_case "Credo: no Credo on an ordinary Festum" `Quick test_credo_festum;
+ Alcotest.test_case "Credo: no Credo on a weekday obligatory memorial" `Quick
+ test_credo_weekday_obligatory_memorial;
+ Alcotest.test_case "Credo: no Credo on a plain feria" `Quick test_credo_ordinary_feria;
+ Alcotest.test_case "Credo: no Credo on All Souls" `Quick test_credo_all_souls;
+ Alcotest.test_case "Preface: Nativity is None" `Quick test_preface_none_nativity;
+ Alcotest.test_case "Preface: Sunday per annum is None" `Quick test_preface_none_ordinary_sunday;
+ Alcotest.test_case "Preface: a plain feria is None" `Quick test_preface_none_feria;
+ Alcotest.test_case "Preface: Trinity Sunday is None" `Quick test_preface_none_solemnity_with_own_single_text;
+ Alcotest.test_case "Preface: All Souls is None" `Quick test_preface_none_all_souls;
+ Alcotest.test_case "requiem_slugs population is exactly {all-souls}" `Quick
+ test_requiem_slugs_population ] )