aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-13 19:23:19 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-13 19:23:19 +0200
commita3afb19dc6390c9e33ed5fa0ecbd6e5895e60e8d (patch)
tree9e6108a58da67fcd94ce057a196f70c47ec49677 /test
parent36eba4eff58162cbe4c7b4ff1b9d049432fc1acc (diff)
parentc63e37b8cbef1480b6f5677420425d22fe7061d9 (diff)
downloadcolitur-a3afb19dc6390c9e33ed5fa0ecbd6e5895e60e8d.tar.gz
colitur-a3afb19dc6390c9e33ed5fa0ecbd6e5895e60e8d.zip
Merge branch 'ef-bvm-saturday': the Office of Our Lady on Saturday
Builds RG 91 entry 27 under RG 78 -- 'In sabbatis, in quibus occurrit Officium de feria IV classis, fit de sancta Maria in sabbato' -- a rank test, not a season list. band had been assigning entry 27 312966 times across the domain while the office was never constructed, so an otherwise-unoccupied Saturday got a bare ferial slug and its season's colour. 75863 days change, in two shapes: colour (green or violet to white) on 74642, and colour plus a dropped commemoration on 1221. The 312966 reconciles to the day across four buckets, 210808 of them impeded by a sanctoral winner. The first version introduced a live RG 112(d) violation on every Saturday falling on 16 July: the Office of Our Lady observed, commemorating Our Lady. Both photographic scans print a rubric at that Mass for exactly this collision -- 'Missa dici potest aut de sancta Maria in sabbato, aut propria' -- absent from the electronic transcription, and cited nowhere. It was missed because the check was a grep for subject = Bvm, and Mt Carmel is tagged Saint: the rubric's criterion is whose intercession the oration implores, not colitur's own tag. The I-V Mass numbering is a propers rotation and stays unbuilt; citations remain empty on all 3073916 days. Recorded rather than glossed: the new rule has no comparison-layer witness at all, and lectio's fixture prints a Mt Carmel commemoration on 8 in-window dates where colitur now prints none -- a real divergence that falls through a gap in what the comparators examine. Time after Epiphany's 5015 office days have zero oracle witnesses and rest on the scan alone.
Diffstat (limited to 'test')
-rw-r--r--test/test_differential.ml53
-rw-r--r--test/test_golden.ml105
-rw-r--r--test/test_oracle.ml67
-rw-r--r--test/test_precedence_ef.ml178
-rw-r--r--test/test_temporal_ef.ml148
5 files changed, 508 insertions, 43 deletions
diff --git a/test/test_differential.ml b/test/test_differential.ml
index bea1bf4..5093484 100644
--- a/test/test_differential.ml
+++ b/test/test_differential.ml
@@ -138,6 +138,7 @@ module Slug = Colitur_kernel.Slug
module Date = Colitur_kernel.Date
module Cel = Colitur_kernel.Celebration
module Colour = Colitur_kernel.Colour
+module Subject = Colitur_kernel.Subject
module V = Rite_ef.Vocab_ef
(* Same relative paths test_rite_ef.ml/test_sanctoral_ef.ml use: dune test
@@ -216,6 +217,14 @@ type row = {
slug : string;
rank : string;
colour : string;
+ (* Fix round 1 (coordinator finding F3): ONLY ever populated for colitur's
+ own row (see [colitur_rows_2005_2050] below) -- lectio's own fixture
+ format has no subject column at all, so [row_of_line] (the lectio-side
+ parser) sets this to "-", a placeholder never read on that side. Every
+ Layer C predicate that reads [subject] must therefore read it off the
+ COLITUR row [c], never [l] -- the same asymmetry [row]'s other fields
+ do not have, called out here rather than left implicit. *)
+ subject : string;
}
let read_lines path =
@@ -232,7 +241,7 @@ let read_lines path =
let row_of_line line =
match String.split_on_char ' ' line with
| date :: weekday :: season :: week :: slug :: rank :: colour :: _others ->
- { date; weekday; season; week; slug; rank; colour }
+ { date; weekday; season; week; slug; rank; colour; subject = "-" }
| _ -> Alcotest.failf "malformed fixture line (fewer than 7 fields): %S" line
let lectio_rows () = List.map row_of_line (read_lines fixture_path)
@@ -271,7 +280,8 @@ let colitur_rows_2005_2050 () =
week;
slug = Slug.to_string cel.Cel.slug;
rank = V.rank_to_string cel.Cel.rank;
- colour = Colour.to_string cel.Cel.colour
+ colour = Colour.to_string cel.Cel.colour;
+ subject = Subject.to_string cel.Cel.subject
}
:: !rows
| None -> Alcotest.failf "internal error: no resolved day for %s" (Date.to_iso8601 !d));
@@ -516,6 +526,44 @@ let holy_name_fallback_dates_2005_2050 =
"2034-01-02"; "2035-01-02"; "2036-01-02"; "2040-01-02"; "2041-01-02"; "2045-01-02";
"2046-01-02"; "2047-01-02" ]
+(* C17 -- ef-bvm-saturday task: RG 91 entry 27 ("Officium sanctae Mariae in
+ sabbato" -- see data/ef/expected-divergences.sexp's own C17 note for the
+ full RG 78/RG 120(b) citation). lectio builds no equivalent office at all
+ (checked directly against its own tridentine-calendar.ini/generator, the
+ same "genuine upstream gap, not a colitur bootstrap miss" shape C15/C16
+ already document for their own gaps) -- it shows the ordinary season
+ colour on every otherwise-unoccupied Class4 Saturday where colitur's own
+ RG 78 fix now shows white. Season, slug and rank all still agree --
+ temporal_ef.ml's own [bvm_saturday_names] citation ("Slug" paragraph)
+ deliberately REUSES the ordinary ferial slug rather than minting a new
+ one, precisely so this predicate's diff set stays [Colour_f] alone, never
+ [Slug_f] too. Gated on colitur's own [rank]/[weekday]/[colour] rather
+ than a date list, the same shape M2 (data/ef/expected-divergences-
+ missalemeum.sexp) already uses for the identical reason: this
+ population is large (a large fraction of all Saturdays domain-wide) and
+ entirely formulaic (RG 78's own condition, "otherwise unoccupied
+ IV-class Saturday", reduces exactly to this triple), not a short,
+ individually-interesting list of dates the way C14/C15/C16 above are. *)
+let is_bvm_saturday_row (c : row) diffs =
+ (* Fix round 1 (coordinator finding F3): [c.subject] added -- the ONLY
+ field in this predicate that pins WHICH celebration is observed, not
+ merely its shape. Every other Layer C entry pins a colitur slug (C1's
+ own [jan_6_13_slug], C6's [nativity_octave_day_slugs], C8/C14/C15/C16's
+ own literal slug checks); this entry could not, because the office
+ deliberately REUSES the ordinary ferial slug (Rite_ef.Temporal_ef's own
+ [bvm_saturday_names] citation, "Slug" paragraph) -- there is no fixed
+ slug string to pin. [subject = "bvm"] is the field that DOES uniquely
+ identify the office (set nowhere else the differential's own
+ [colitur_rows_2005_2050] can produce a Saturday/Class4/white
+ combination for), closing the gap a shape-only predicate left open:
+ without this conjunct, a FUTURE bug that made some OTHER white,
+ Class4, Saturday candidate exist (Christmastide/Paschaltide, where
+ [season_colour] is already white, so a real bug there could slip
+ through unnoticed by colour alone) would be silently absorbed here
+ too. *)
+ diffs = [ Colour_f ] && String.equal c.weekday "saturday" && String.equal c.rank "class-4"
+ && String.equal c.colour "white" && String.equal c.subject "bvm"
+
(* [layer_c_reason l c diffs] returns the [data/ef/expected-divergences.sexp]
[id] this row-pair's remaining (post Layer A/B) diff set belongs to, or
[None] if nothing here explains it (a genuine, uncovered failure). *)
@@ -571,6 +619,7 @@ let layer_c_reason (l : row) (c : row) diffs =
&& subset diffs [ Slug_f; Rank ]
&& String.equal c.slug "ef-holy-name"
then Some "C16"
+ else if is_bvm_saturday_row c diffs then Some "C17"
else None
(* ---------------------------------------------------------------------- *)
diff --git a/test/test_golden.ml b/test/test_golden.ml
index 594ea6b..fdfc01d 100644
--- a/test/test_golden.ml
+++ b/test/test_golden.ml
@@ -851,8 +851,22 @@ let test_iii_class_feast_wins_class4_feria () =
"2008-04-05 saturday season=paschaltide week=2 slug=vincent-ferrer rank=class-3 colour=white subject=saint name_la=- comms=[] in=- \
out=[]";
let d4 = fetch 2038 3 6 in
+ (* ef-bvm-saturday task: 6 March 2038 is a SATURDAY, so the displaced
+ temporal candidate is now RG 78's own BVM Saturday office
+ ({!Rite_ef.Temporal_ef}'s own [bvm_saturday_names] citation) rather than
+ a bare ferial -- but per that citation's own "Slug" paragraph the SLUG
+ is deliberately left unchanged (reused, not replaced), precisely so
+ this test's own [omitted_has] probe still needs no update: the displaced
+ candidate is still named "ef-septuagesima-2-saturday", only its
+ colour/subject/name would have differed had it won. RG 91 entry 24
+ (Sts Perpetua and Felicity) still outranks it either way -- entry 24 <
+ entry 27, both above entry 28 -- so the disposition/win outcome this
+ test proves is genuinely unchanged by this task, now more precisely
+ entry 24 > entry 27 rather than > 28 for this one date (a Saturday), not
+ that the test itself needed touching. *)
Alcotest.(check bool)
- "2038-03-06: the displaced class-4 Septuagesima feria is in [omitted] (RG26), proving RG91 entry24>28 fired"
+ "2038-03-06: the displaced class-4 feria (RG78's own BVM-Saturday office, \
+ for this Saturday) is in [omitted] (RG26), proving RG91 entry24>27 fired"
true
(omitted_has d4 "ef-septuagesima-2-saturday");
Alcotest.(check string)
@@ -869,6 +883,81 @@ let test_iii_class_feast_wins_class4_feria () =
"2038-03-09 tuesday season=septuagesima week=3 slug=frances-rome rank=class-3 colour=white subject=saint name_la=- comms=[] in=- \
out=[]"
+(* ------------------------------------------------------------------ *)
+(* RG 91 entry 27 / RG 78-79 (Caput IX, "De sancta Maria in sabbato") --
+ ef-bvm-saturday task, Rite_ef.Temporal_ef's own [bvm_saturday_names]
+ citation has the full primary-source argument. Two real-data pins, the
+ task brief's own worked example and its mirror-image negative case,
+ matching this file's own header rule: EVERY literal below was checked by
+ hand against `colitur day 2026` before being typed in here (this task's
+ own report has the full session log), never copied from a run and
+ rationalised after. *)
+
+(* The task brief's own worked example, checked against the oracle
+ (missalemeum) directly: "2026-08-01|4|w|V Mass of the B. V. M. -- Salve,
+ Sancta Parens|Saturday_after_IX_Sunday_after_Pentecost|Holy Machabees".
+ colitur's own rank (4) and colour (white) now match; the oracle's own
+ numbered-Mass title (RG 309(a)'s own "iuxta temporum diversitatem", not
+ modelled here -- {!bvm_saturday_names}'s own "Slug" paragraph) and the
+ observed day's own English identity remain outside what this comparator
+ can see (M18's own limit) -- this pin instead proves what colitur DOES
+ compute: white/class-4/subject Bvm/the Latin title, and Holy Machabees
+ (real data/ef/sanctoral.sexp entry, Commemoration_only, Class3, 1 August)
+ still admitted as an ORDINARY commemoration alongside it, matching the
+ oracle's own commemoration list exactly. *)
+let test_bvm_saturday_2026 () =
+ check ~msg:"2026-08-01: the BVM Saturday office observed, white overriding Time after Pentecost's green, \
+ Holy Machabees commemorated -- matches the task brief's own oracle example"
+ 2026 8 1
+ "2026-08-01 saturday season=time-after-pentecost week=9 slug=ef-time-after-pentecost-9-saturday \
+ rank=class-4 colour=white subject=bvm name_la=Officium sanctae Mariae in sabbato \
+ comms=[holy-machabees:ordinary] in=- out=[]"
+
+(* The mirror-image case: RG 78's own protasis is FALSE here (a real feast
+ wins the Saturday outright), so the BVM-Saturday-shaped temporal
+ candidate must lose and be omitted under RG 26, exactly like the plain
+ ferial candidate it replaces always was -- no commemoration at all,
+ proving [band]/[disposition] need no special case for this office (the
+ design claim the task report makes explicitly). ALSO the one live data
+ witness where the winning candidate itself carries [subject = Bvm]
+ (data/ef/adjustments.sexp's own `Edit most-holy-name-of-mary
+ ((Set_subject Bvm))`) -- proving no Bvm-vs-Bvm confusion with a
+ RG112(a)-style subject rule: [omitted_has] below confirms the loser is
+ genuinely reported omitted (RG26 fires on [rank = Class4] alone, checked
+ ahead of any subject-based branch in {!Rite_ef.Precedence_ef.disposition}),
+ not silently swallowed by a same-subject exclusion that does not exist
+ for [Bvm] anywhere in this codebase. *)
+let test_bvm_saturday_loses_to_most_holy_name_of_mary_2026 () =
+ let d = fetch 2026 9 12 in
+ Alcotest.(check bool)
+ "2026-09-12: the displaced BVM-Saturday-shaped candidate is in [omitted] (RG26), not silently absent"
+ true
+ (omitted_has d "ef-time-after-pentecost-15-saturday");
+ Alcotest.(check string)
+ "2026-09-12: Most Holy Name of Mary (III class, subject Bvm, data/ef/adjustments.sexp) outranks the \
+ class-4 BVM-Saturday candidate; no commemoration at all"
+ "2026-09-12 saturday season=time-after-pentecost week=15 slug=most-holy-name-of-mary rank=class-3 \
+ colour=white subject=bvm name_la=- comms=[] in=- out=[]"
+ (describe d)
+
+(* RG112(d), fix round 1 (coordinator finding F1): PE.marian_slugs's own
+ citation (precedence_ef.ml) has the full RG112(d) argument, including
+ the 16 July "Missa dici potest aut ... aut ..." rubric found on both
+ photographic scans. 16 July 2033 is a real otherwise-unoccupied
+ Saturday; before this fix, [comms] here read
+ "[our-lady-of-mt-carmel:ordinary]" (confirmed against real pre-fix
+ `colitur day 2033` output). *)
+let test_bvm_saturday_excludes_mt_carmel_2033 () =
+ let d = fetch 2033 7 16 in
+ Alcotest.(check bool)
+ "2033-07-16: Mt Carmel is in [omitted] (RG112(d)), not silently dropped from the record" true
+ (omitted_has d "our-lady-of-mt-carmel");
+ Alcotest.(check string)
+ "2033-07-16: the BVM Saturday office observed, white; NO commemoration of Mt Carmel (RG112(d))"
+ "2033-07-16 saturday season=time-after-pentecost week=5 slug=ef-time-after-pentecost-5-saturday \
+ rank=class-4 colour=white subject=bvm name_la=Officium sanctae Mariae in sabbato comms=[] in=- out=[]"
+ (describe d)
+
let suite =
( "golden pins (known-tricky years)",
[ Alcotest.test_case "Easter extreme: 1598 earliest (22 Mar, Gauss-verified)" `Quick
@@ -925,5 +1014,17 @@ let suite =
Alcotest.test_case
"ef-rebootstrap: RG91 entry24 (III-class feast) outranks entry28 (class-4 feria) -- the six dates \
deferred since 2038's own Easter-extreme pin (2008-04-02/04/05, 2038-03-06/08/09)"
- `Quick test_iii_class_feast_wins_class4_feria
+ `Quick test_iii_class_feast_wins_class4_feria;
+ Alcotest.test_case
+ "RG91 e27/RG78: the BVM Saturday office wins, Holy Machabees commemorated -- matches the task \
+ brief's own oracle example (2026-08-01)"
+ `Quick test_bvm_saturday_2026;
+ Alcotest.test_case
+ "RG91 e27/RG78/RG26: the BVM Saturday office loses to Most Holy Name of Mary (subject Bvm too) \
+ and is omitted, no confusion (2026-09-12)"
+ `Quick test_bvm_saturday_loses_to_most_holy_name_of_mary_2026;
+ Alcotest.test_case
+ "RG112(d), fix round 1 (F1): the BVM Saturday office excludes Mt Carmel's own commemoration \
+ (2033-07-16)"
+ `Quick test_bvm_saturday_excludes_mt_carmel_2033
] )
diff --git a/test/test_oracle.ml b/test/test_oracle.ml
index 58ceb26..c0badfe 100644
--- a/test/test_oracle.ml
+++ b/test/test_oracle.ml
@@ -475,25 +475,30 @@ let contains_substring s ~needle =
own "in dominica quavis" (ANY Sunday, unqualified) -- verdict colitur. *)
let m1_dates = [ "2026-06-28"; "2026-08-09" ]
-(* M2 -- RG 91 entry 27 ("Officium sanctae Mariae in sabbato"): every
- otherwise-unoccupied IV-class Saturday should carry the votive Office of
- the BVM (white; missalemeum's own title cycles "I".."V Mass of the
- B. V. M. - Salve, Sancta Parens"), a genuinely DISTINCT office from an
- ordinary green/violet feria. temporal_ef.ml's [band] already carries an
- entry-27 comment acknowledging this row of RG 91 exists, but nothing in
- [Temporal_ef.temporal]'s actual CONSTRUCTION builds this office -- an
- unimpeded Time-after-Epiphany/-Pentecost Saturday still gets the bare
- ferial slug and [season_colour]'s green. A genuine, primary-attested
- FEATURE GAP (not a citation dispute) -- verdict missalemeum: colitur is
- short a whole office here, not merely differently-opinioned. Registered
- as an open item (register §6) rather than built in this task: it needs
- its own proper texts/citations, the same scope line "Major Litanies"
- (M5 below) already sits on. Matched on the oracle's own title substring,
- not a hardcoded date list -- the title IS the distinguishing evidence,
- and every Paschaltide occurrence (where colitur's season colour is
- already white, RG 119) never reaches this predicate at all, since its
- diffs are already empty before [layer_m_reason] is ever called. *)
-let bvm_saturday_title = "Salve, Sancta Parens"
+(* M2 -- CLOSED, REMOVED (ef-bvm-saturday task), same shape as M12/M17's own
+ removal elsewhere in this suite (not re-adjudicated to a different
+ verdict, gone because the underlying divergence stopped occurring): RG 91
+ entry 27 ("Officium sanctae Mariae in sabbato") is now built
+ (Rite_ef.Temporal_ef's own [bvm_saturday_names] citation has the full RG
+ 78/RG 120(b) argument) -- white, unconditionally, on every otherwise-
+ unoccupied IV-class Saturday, so [Colour_f] no longer appears in this
+ predicate's own diff set on ANY of these 22 dates (checked: 0 of them
+ retain it). What is left on every one of them is [Observed_identity_
+ unresolved] ALONE -- colitur's own BVM-Saturday candidate is
+ temporal-origin and deliberately carries no ENGLISH name (Latin only,
+ the same zero-circularity discipline Holy Family/Holy Name/the Sacred
+ Triduum already established) -- which is EXACTLY [M18]'s own shape
+ ([diffs = [ Observed_identity_unresolved ]], nothing else disagreeing),
+ not a distinct citation any more. Kept as a separate id here would have
+ been the same "count proving cardinality where identity was required"
+ trap this project's own review process watches for: [subset] admits a
+ SMALLER diff set than the one named, so this branch would have kept
+ silently absorbing these 22 rows under a stale "colour differs" citation
+ even though that citation's own reason no longer fires -- checked
+ directly, not assumed: removing this branch and re-running moves the
+ count from M2's own former 22 to M18's own count rising by exactly 22
+ (373 -> 395, see M18's own note below), with the "no unexplained
+ differences" test staying green throughout. *)
(* M3 -- RG 87 (Minor Litanies/Rogations): the SAME gap the lectio
differential's own C8 already names (data/ef/expected-divergences.sexp)
@@ -822,7 +827,12 @@ let m19_commemoration_matches (c : colitur_row) =
| [ (slug, _, _, _) ] -> String.equal slug "commemoration-of-st-peter"
| _ -> false
-let layer_m_reason (c : colitur_row) (o : oracle_row) diffs =
+(* [_o] (the oracle's own row) is unused now that M2 -- its one reader, via
+ [o.o_title] -- is closed/removed (see the comment above [m1_dates]):
+ every remaining predicate below reads only [c]/[diffs]. Kept as a named,
+ typed parameter (not dropped from the signature) so a FUTURE oracle-title
+ predicate has somewhere to plug back in without a signature change. *)
+let layer_m_reason (c : colitur_row) (_o : oracle_row) diffs =
if diffs = [] then None
(* M1's own subset widened (this task): colitur's observed day on both
dates is TEMPORAL-origin (the Sunday itself, RG33's own point -- the
@@ -832,17 +842,12 @@ let layer_m_reason (c : colitur_row) (o : oracle_row) diffs =
else if List.mem c.c_date m1_dates
&& subset diffs [ Rank; Colour_f; Comm_presence; Observed_identity_unresolved ]
then Some "M1"
- (* M2's own subset widened (this task): every BVM-Saturday date's observed
- celebration is temporal-origin (the ordinary Saturday feria colitur
- computes, not the distinct Office of the BVM missalemeum's title
- names -- M2's own gap), so [Observed_identity_unresolved] now fires
- alongside [Colour_f] where it used to fire alone, AND alone on its own
- on every Paschaltide occurrence M2's own comment already says never
- reached this predicate before (colour already agrees there; only the
- new axis has anything left to say). *)
- else if contains_substring o.o_title ~needle:bvm_saturday_title
- && subset diffs [ Colour_f; Observed_identity_unresolved ]
- then Some "M2"
+ (* M2 -- CLOSED, REMOVED (ef-bvm-saturday task): see this file's own M2
+ note above (near [m1_dates]) for the full account -- the RG 91 entry 27
+ office is now built, so every one of these 22 dates now falls through
+ to [M18]'s own generic predicate below instead ([diffs = [
+ Observed_identity_unresolved ]] exactly), which is what actually
+ catches them now. *)
(* M3's own subset widened (this task): the Rogation Monday feria colitur
observes is temporal-origin, same root cause as M1/M2 above. *)
else if List.mem c.c_date m3_dates && subset diffs [ Colour_f; Observed_identity_unresolved ] then
diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml
index ed9a933..6f8b5b5 100644
--- a/test/test_precedence_ef.ml
+++ b/test/test_precedence_ef.ml
@@ -291,10 +291,21 @@ let cases =
cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "vigil-of-st-lawrence",
260 );
(* Entry 27 -- RG 91 entry 27 (§4): an otherwise-unoccupied IV-class
- Saturday. *)
- ( "27 Office of the BVM on Saturday", off 62,
- cand ~rank:V.Class4 "ef-time-after-pentecost-1-sat",
- 270 );
+ Saturday. ef-bvm-saturday task: sourced from [Temporal_ef.temporal]'s
+ own real output (see [of_temporal], the same review-finding-3
+ discipline entry 18's/21's own rows above use), not a hand-typed
+ slug/rank pair -- this row used to be the one place in this whole
+ suite where [band]'s entry-27 branch was exercised by a value nothing
+ in [Temporal_ef.temporal] could actually produce (the office itself
+ was unbuilt): [band] read [rank = Class4 && weekday = Sat]
+ correctly, but nothing proved [temporal] ever handed it a candidate
+ shaped that way for the RIGHT reason. Now it does: [of_temporal (off
+ 62)] is 6 June 2026, a real otherwise-contested Time-after-Pentecost
+ Saturday (Norbert, Class3, wins the DAY outright in the real
+ pipeline -- irrelevant here, since this row tests [band] on the
+ TEMPORAL CANDIDATE alone, which [Temporal_ef.temporal] builds as the
+ BVM Saturday office regardless of what wins). *)
+ ( "27 Office of the BVM on Saturday (from Temporal_ef.temporal)", off 62, of_temporal (off 62), 270 );
(* Entry 28 -- RG 91 entry 28 (§4): the unqualified IV-class catch-all. *)
("28 IV-class feria", off 65, cand ~rank:V.Class4 "ef-time-after-pentecost-1-tue", 280);
(* Not an RG 91 row at all: a I-class candidate marked as a vigil, which
@@ -794,6 +805,52 @@ let disposition_cases =
"conversion-of-st-paul",
cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer
"commemoration-of-the-baptism-of-the-lord",
+ "Commemorate(Ordinary)" );
+ (* RG112(d), fix round 1 (coordinator finding F1) -- PE.marian_slugs's
+ own citation has the full argument (both scans, including the 16
+ July "Missa dici potest aut ... aut ..." rubric). [of_temporal] on a
+ REAL 16-July-Saturday (2033) sources the winner from
+ Temporal_ef.temporal's own real output, the same review-finding-3
+ discipline this file's Ember/vigil rows already use -- not a
+ hand-typed slug/subject pair that could silently drift from what the
+ office actually builds. The loser is the real data/ef/sanctoral.sexp
+ entry, [Commemoration_only], the exact shape that made this branch
+ need to be checked BEFORE the Commemoration_only catch-all. *)
+ ( "RG112(d): the BVM Saturday Office excludes a commemoration invoking \
+ the SAME BVM's intercession -- Omit, not Commemorate (Mt Carmel, \
+ 16 July 2033, a real otherwise-unoccupied Saturday)",
+ of_temporal (mk 2033 7 16),
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint
+ ~layer:PE.universal_layer "our-lady-of-mt-carmel",
+ "Omit" );
+ (* Conjunct 1/2, LOSER identity: an ORDINARY Commemoration_only saint
+ NOT on PE.marian_slugs must still reach the ordinary commemorate
+ flow even when the winner is the BVM Saturday Office -- proves
+ RG112(d) excludes a specific same-BVM collision, not every
+ commemoration the office happens to outrank (the real live shape,
+ 1 August, Holy Machabees, PE.marian_slugs's own comment). *)
+ ( "RG112(d) does NOT fire when the LOSER is not a Marian commemoration \
+ -- an ordinary saint stays Commemorate(Ordinary) even under the BVM \
+ Saturday Office (1 August 2026, Holy Machabees)",
+ of_temporal (mk 2026 8 1),
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint
+ ~layer:PE.universal_layer "holy-machabees",
+ "Commemorate(Ordinary)" );
+ (* Conjunct 2/2, WINNER identity: an ordinary (non-Bvm, non-Marian-
+ titled) winner must not exclude Mt Carmel either -- RG112(d) is
+ keyed on the WINNER also being "de B. Maria Virg.", not merely on
+ the loser's own identity. SYNTHETIC: no live date has an ordinary
+ Class3 saint winning outright while Mt Carmel also loses that same
+ day (Mt Carmel's own fixed date, 16 July, has no other real
+ sanctoral entry to construct this from) -- proves the conjunct is
+ real, not merely untested, the same "SYNTHETIC" discipline the
+ RG112(a) rows above already use for their own second conjunct. *)
+ ( "SYNTHETIC: RG112(d) does NOT fire when only the LOSER is Marian -- \
+ an ordinary winner leaves Mt Carmel Commemorate(Ordinary)",
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer
+ "ef-synthetic-ordinary-winner",
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint
+ ~layer:PE.universal_layer "our-lady-of-mt-carmel",
"Commemorate(Ordinary)" )
]
@@ -1421,6 +1478,107 @@ let test_ii_class_sunday_privileged_witness_admitted_end_to_end () =
[ ("ef-some-saint", "omitted: admission limit reached") ]
(List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted)
+(* RG 91 entry 27 / RG 78-79 (Caput IX, "De sancta Maria in sabbato") reached
+ end-to-end through the REAL pipeline -- this is THE hazard this task's own
+ brief names explicitly: {!PE.band}'s entry-27 branch has been reachable
+ only in isolation (the table-driven row above) since it was written, never
+ through a real [Temporal_ef.temporal] candidate winning an actual day, so
+ this test is checked for whether it exercises entry 27 for the RIGHT
+ reason -- proved by the mutation test recorded in the task report
+ (reverting temporal_ef.ml's own [is_bvm_saturday] branches back to the
+ generic ferial fallback reddens THIS test, not merely the [band]-table
+ row, which would still pass unchanged since it is sourced from real
+ [T.temporal] output either way).
+
+ 1 August 2026, matching the task brief's own worked example against the
+ oracle exactly: a Time-after-Pentecost Saturday, otherwise unoccupied but
+ for Holy Machabees (Commemoration_only, Class3, real data/ef/sanctoral.sexp
+ entry, 1 August) -- the office wins outright (band 270, ahead of Holy
+ Machabees's own [unclassified]) and the Commemoration_only loser still
+ rides along as an ordinary commemoration, the SAME shape the oracle's own
+ row for this date shows ("V Mass of the B. V. M. -- Salve, Sancta Parens",
+ commemorating Holy Machabees). *)
+let test_bvm_saturday_wins_and_commemorates_end_to_end () =
+ let date = mk 2026 8 1 in
+ let day_ctx = ctx date in
+ let bvm_saturday = of_temporal date in
+ let holy_machabees =
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~layer:PE.universal_layer
+ "holy-machabees"
+ in
+ let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ holy_machabees ] in
+ Alcotest.(check string) "the BVM Saturday office is observed, not Holy Machabees"
+ "ef-time-after-pentecost-9-saturday" (S.to_string resolution.P.observed.P.cel.Cel.slug);
+ Alcotest.(check string) "observed rank is class-4, unconditional" "class-4"
+ (V.rank_to_string resolution.P.observed.P.cel.Cel.rank);
+ Alcotest.(check string) "observed colour is white, overriding the season's own green" "white"
+ (Col.to_string resolution.P.observed.P.cel.Cel.colour);
+ Alcotest.(check bool) "observed subject is Bvm" true (resolution.P.observed.P.cel.Cel.subject = Sub.Bvm);
+ Alcotest.(check (list (pair string string))) "Holy Machabees still rides along as an ordinary commemoration"
+ [ ("holy-machabees", "Ordinary") ]
+ (List.map
+ (fun (c, p) -> (S.to_string c.P.cel.Cel.slug, match p with P.Privileged -> "Privileged" | P.Ordinary -> "Ordinary"))
+ resolution.P.commemorations);
+ Alcotest.(check int) "nothing omitted" 0 (List.length resolution.P.omitted)
+
+(* The other half of the SAME hazard: a real Saturday where RG 78's own
+ protasis is FALSE (a genuine sanctoral feast wins the day outright), so
+ the BVM-Saturday-shaped temporal candidate must lose, and be OMITTED under
+ RG 26 (a temporal Class4 non-vigil loser is never commemorated) exactly
+ like the plain ferial candidate it replaces always was -- proving [band]/
+ [disposition] need no special case for this office at all, the design
+ claim the task report makes explicitly. 12 September 2026 is a Saturday
+ where `most-holy-name-of-mary` (data/ef/adjustments.sexp's own [Set_subject
+ Bvm] entry, Class3 Feast) wins outright -- ALSO tagged [subject = Bvm],
+ the one live data witness that could in principle confuse a
+ subject-keyed disposition rule with RG 26's own rank-keyed one: this test
+ proves they do NOT collide (RG 26 fires first, unconditionally, on the
+ loser's own [rank = Class4], never reaching a subject comparison at all). *)
+let test_bvm_saturday_loses_to_a_real_feast_end_to_end () =
+ let date = mk 2026 9 12 in
+ let day_ctx = ctx date in
+ let bvm_saturday = of_temporal date in
+ let name_of_mary =
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Bvm ~layer:PE.universal_layer
+ "most-holy-name-of-mary"
+ in
+ let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ name_of_mary ] in
+ Alcotest.(check string) "Most Holy Name of Mary is observed, not the BVM Saturday office"
+ "most-holy-name-of-mary" (S.to_string resolution.P.observed.P.cel.Cel.slug);
+ Alcotest.(check (list string)) "nothing is commemorated" []
+ (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations);
+ Alcotest.(check (list (pair string string)))
+ "the BVM-Saturday-shaped temporal candidate is omitted under RG26, not commemorated -- no Bvm-vs-Bvm \
+ confusion with RG112(a)-style subject logic"
+ [ ("ef-time-after-pentecost-15-saturday", "omitted: yielded to a higher day") ]
+ (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted)
+
+(* RG112(d), fix round 1 (coordinator finding F1) -- PE.marian_slugs's own
+ citation has the full argument. End-to-end through the REAL pipeline
+ (PE.band, PE.disposition AND PE.admit together): 16 July 2033, a real
+ otherwise-unoccupied Saturday, with the real data/ef/sanctoral.sexp
+ [our-lady-of-mt-carmel] entry as the day's only competing candidate.
+ Before this fix, [resolution.P.commemorations] held
+ [our-lady-of-mt-carmel:Ordinary] (confirmed live against real 2005/2033
+ `colitur day` output before this fix existed); after, it is empty and
+ Mt Carmel is reported omitted, not silently dropped. *)
+let test_bvm_saturday_excludes_mt_carmel_end_to_end () =
+ let date = mk 2033 7 16 in
+ let day_ctx = ctx date in
+ let bvm_saturday = of_temporal date in
+ let mt_carmel =
+ cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint
+ ~layer:PE.universal_layer "our-lady-of-mt-carmel"
+ in
+ let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ mt_carmel ] in
+ Alcotest.(check string) "the BVM Saturday office is observed"
+ "ef-time-after-pentecost-5-saturday" (S.to_string resolution.P.observed.P.cel.Cel.slug);
+ Alcotest.(check (list string)) "Mt Carmel is NOT commemorated (RG112(d))" []
+ (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations);
+ Alcotest.(check (list (pair string string))) "Mt Carmel is reported omitted, not silently dropped"
+ [ ("our-lady-of-mt-carmel", "omitted: yielded to a higher day") ]
+ (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted)
+
(* RG 16(a) (register §6.0) reached end-to-end through the REAL pipeline
([PE.band], [PE.disposition] AND [PE.admit] together, not any one of
them in isolation as this file's own hand-tagged rows test them
@@ -1895,6 +2053,18 @@ let suite =
"II-class Sunday override: RG109(b) witness admitted over an ordinary saint, end-to-end"
`Quick test_ii_class_sunday_privileged_witness_admitted_end_to_end;
Alcotest.test_case
+ "RG91 e27/RG78: the BVM Saturday office wins and commemorates Holy Machabees, end-to-end \
+ (2026-08-01)"
+ `Quick test_bvm_saturday_wins_and_commemorates_end_to_end;
+ Alcotest.test_case
+ "RG91 e27/RG78/RG26: the BVM Saturday office loses to a real feast and is omitted, end-to-end \
+ (2026-09-12)"
+ `Quick test_bvm_saturday_loses_to_a_real_feast_end_to_end;
+ Alcotest.test_case
+ "RG112(d), fix round 1 (F1): the BVM Saturday office excludes Mt Carmel, end-to-end \
+ (2033-07-16)"
+ `Quick test_bvm_saturday_excludes_mt_carmel_end_to_end;
+ Alcotest.test_case
"RG16(a): a Feast of the Lord suppresses the Sunday's own commemoration entirely, \
end-to-end (2028-08-06)"
`Quick test_rg16a_lord_feast_suppresses_sunday_end_to_end;
diff --git a/test/test_temporal_ef.ml b/test/test_temporal_ef.ml
index 5394173..4e97d33 100644
--- a/test/test_temporal_ef.ml
+++ b/test/test_temporal_ef.ml
@@ -296,13 +296,31 @@ let test_holy_name_sunday_shape () =
compared against)"
None (Colitur_kernel.Names.find (office (d 2026 1 4)).Cel.names en);
(* Negative controls: every OTHER day in the 2-5 January window that year
- stays [subject Temporal] -- the override is scoped to exactly the one
- Sunday [T.holy_name_sunday] names, not the whole window. *)
+ is NOT Lord -- RG 17(a)'s own override is scoped to exactly the one
+ Sunday [T.holy_name_sunday] names, not the whole window. 2 and 5 January
+ (Friday, Monday) stay plain [Temporal] ferias, as before. 3 January is a
+ SATURDAY -- an unrelated rubric (RG 78, "De sancta Maria in sabbato",
+ {!bvm_saturday_names}'s own citation in temporal_ef.ml) legitimately
+ retags it [Bvm], not [Temporal]: it is an otherwise-unoccupied IV-class
+ Saturday (Christmastide's "ef-christmas-1-*" ferial branch, [ferial_rank]'s
+ unconditional Class4 there), so RG 78 fires regardless of RG 17(a) being
+ in play the same week -- the two rubrics are independent and this is not
+ a collision. This is deliberately NOT folded into the [Temporal] check
+ above: asserting "stays Temporal" for 3 January would be WRONG once RG
+ 78 is built, not merely incomplete. *)
List.iter
(fun dd ->
Alcotest.(check bool) (Printf.sprintf "2026-01-%02d: NOT Lord (not the Sunday)" dd) true
- (subject_of (d 2026 1 dd) = Sub.Temporal))
+ (subject_of (d 2026 1 dd) <> Sub.Lord))
[ 2; 3; 5 ];
+ Alcotest.(check bool) "2026-01-02: plain Temporal feria (not a Saturday)" true
+ (subject_of (d 2026 1 2) = Sub.Temporal);
+ Alcotest.(check bool) "2026-01-05: plain Temporal feria (not a Saturday)" true
+ (subject_of (d 2026 1 5) = Sub.Temporal);
+ Alcotest.(check bool) "2026-01-03: a Saturday -- RG 78's BVM Saturday office fires here \
+ independently of RG 17(a)"
+ true
+ (subject_of (d 2026 1 3) = Sub.Bvm);
(* 2027: 3 January is the Sunday (`date -d 2027-01-03 +%u` = 7) -- the
oracle window's own second instance of the Sunday shape. missalemeum's
2027-01-03 row independently corroborates: rank 2, white, title "Holy
@@ -489,14 +507,43 @@ let test_colours () =
(6 Jan) is a Tuesday, so the actual first-Sunday-after-Epiphany origin is
11 Jan -- exercising all four sub-stretches of the fix. *)
let test_christmastide_feria_slugs () =
+ (* 26 Dec 2026 is a SATURDAY -- but, fix round 1 (coordinator finding F2),
+ it can NEVER actually hold the BVM Saturday office in the real
+ resolved pipeline: 26/27/28 December are St Stephen/St John/the Holy
+ Innocents (RG 67-70), fixed, unconditional, real data/ef/sanctoral.sexp
+ entries every single year (rank >= Class2), which always outrank a
+ Class4 temporal candidate -- so RG 78's own protasis ("in quibus
+ OCCURRIT Officium de feria IV classis") is never satisfied by ANY day
+ in the 26-28 December stretch, in any year. [T.temporal] itself has no
+ visibility into the sanctoral layer at all (by design -- the temporal
+ candidate is computed independent of what saint's day it is; Precedence
+ decides the winner afterward), so it still unconditionally tags this
+ date [subject = Bvm] -- a genuine, correct fact about [T.temporal]'s
+ own pure computation, but NOT a claim that this shape is ever the real
+ OBSERVED office, and pinning it with RG-78 citation language as if it
+ were would overclaim. Only the SLUG is asserted here (unaffected
+ either way, unconditionally correct); the override demonstration
+ itself uses 3 January and 10 January below instead -- both, checked
+ directly against data/ef/sanctoral.sexp, have no competing entry in
+ any year, so the BVM Saturday office genuinely IS reachable there. *)
Alcotest.(check string) "26 Dec (Sat)" "ef-christmas-0-saturday" (slug_of (d 2026 12 26));
Alcotest.(check string) "28 Dec (Mon)" "ef-christmas-0-monday" (slug_of (d 2026 12 28));
+ (* 3 Jan 2026 is a SATURDAY, genuinely reachable (data/ef/sanctoral.sexp
+ has no 3 January entry) -- the real demonstration for the "-1-"
+ stretch that 26 December's own Saturday could not honestly provide. *)
+ Alcotest.(check string) "3 Jan (Sat): slug stays the ordinary \"-1-\" naming" "ef-christmas-1-saturday"
+ (slug_of (d 2026 1 3));
+ Alcotest.(check string) "3 Jan (Sat): the BVM Saturday office overrides the season colour (white, RG 120(b))"
+ "white" (colour_of (d 2026 1 3));
+ Alcotest.(check bool) "3 Jan (Sat): subject Bvm" true (subject_of (d 2026 1 3) = Sub.Bvm);
Alcotest.(check string) "2 Jan (Fri)" "ef-christmas-1-friday" (slug_of (d 2026 1 2));
Alcotest.(check string) "5 Jan (Mon)" "ef-christmas-1-monday" (slug_of (d 2026 1 5));
(* 7-10 Jan precede the actual origin Sunday (11 Jan): colitur-only, not
- lectio's "week 1" key, precisely to avoid colliding with 12-13 Jan below. *)
+ lectio's "week 1" key, precisely to avoid colliding with 12-13 Jan below.
+ 10 January is ALSO genuinely reachable (no sanctoral entry that date). *)
Alcotest.(check string) "7 Jan (Wed, before the origin)" "ef-christmas-2-wednesday" (slug_of (d 2026 1 7));
Alcotest.(check string) "10 Jan (Sat, before the origin)" "ef-christmas-2-saturday" (slug_of (d 2026 1 10));
+ Alcotest.(check bool) "10 Jan (Sat): subject Bvm" true (subject_of (d 2026 1 10) = Sub.Bvm);
(* 12-13 Jan are on/after the origin: genuinely week 1, lectio's key. *)
Alcotest.(check string) "12 Jan (Mon, on/after the origin)" "ef-time-after-epiphany-1-monday"
(slug_of (d 2026 1 12));
@@ -813,6 +860,98 @@ let test_sacred_triduum_identity () =
Alcotest.(check bool) "1666-04-22 Holy Thursday: subject Lord" true
(subject_of (d 1666 4 22) = Sub.Lord)
+(* RG 91 entry 27 / RG 78-79 (Caput IX, "De sancta Maria in sabbato") --
+ {!Rite_ef.Temporal_ef.bvm_saturday_names}'s own citation has the full
+ primary-source argument (both photographic scans and the electronic
+ transcription for RG 78/79 themselves; both photographic scans for RG
+ 120(b)'s colour rule and the Missal's own "Missae de sancta Maria in
+ sabbato" heading). This test proves it holds across every season family
+ [ferial_rank] can give Class4 outside a privileged run -- Christmastide,
+ Septuagesima, Time after Epiphany, Time after Pentecost and ordinary
+ Paschaltide -- and that it does NOT fire where RG 78's own protasis is
+ false (Advent/Lent/Passiontide Saturdays, already II/III class; Ember
+ Saturdays, II/I class; a non-Saturday day in the very same week). Every
+ date below was independently checked against `colitur day <year>`'s own
+ real output (data/ef/sanctoral.sexp + adjustments.sexp) before being
+ pinned here, exactly like test_golden.ml's own discipline -- not merely
+ asserted against [T.temporal]'s own predicted shape a second time. *)
+let test_bvm_saturday () =
+ (* Septuagesima, clean (no competing sanctoral candidate that year):
+ 19 February 2028, Septuagesima I Saturday. Colour is the clearest
+ possible witness here -- Septuagesima's own season colour is violet
+ (RG 128(b)), so a white result can only come from RG 120(b)'s override,
+ never from [season_colour] agreeing by coincidence the way
+ Christmastide/Paschaltide do below. *)
+ Alcotest.(check string) "2028-02-19 (Septuagesima I Saturday): slug stays the ordinary ferial key"
+ "ef-septuagesima-1-saturday" (slug_of (d 2028 2 19));
+ Alcotest.(check string) "2028-02-19: class-4, unconditional (RG 91's own IV-class table position)"
+ "class-4" (rank_of (d 2028 2 19));
+ Alcotest.(check string) "2028-02-19: white overrides Septuagesima's own violet (RG 120(b), not RG 128(b))"
+ "white" (colour_of (d 2028 2 19));
+ Alcotest.(check bool) "2028-02-19: subject Bvm" true (subject_of (d 2028 2 19) = Sub.Bvm);
+ Alcotest.(check (option string)) "2028-02-19: Latin name, both photographic scans and the transcription, \
+ word for word"
+ (Some "Officium sanctae Mariae in sabbato") (name_la_of (d 2028 2 19));
+ (* Time after Epiphany, clean: 16 February 2030. Season colour there is
+ green (RG 127), like Time after Pentecost below -- a second,
+ independent witness that the override is season-blind, not merely
+ "true for Septuagesima's violet and Time after Pentecost's green
+ happen to share a fix". *)
+ Alcotest.(check string) "2030-02-16 (Time after Epiphany V Saturday): slug stays the ordinary ferial key"
+ "ef-time-after-epiphany-5-saturday" (slug_of (d 2030 2 16));
+ Alcotest.(check string) "2030-02-16: white overrides Time after Epiphany's own green (RG 127)" "white"
+ (colour_of (d 2030 2 16));
+ Alcotest.(check bool) "2030-02-16: subject Bvm" true (subject_of (d 2030 2 16) = Sub.Bvm);
+ (* Time after Pentecost, clean: 27 June 2026. *)
+ Alcotest.(check string) "2026-06-27 (Time after Pentecost IV Saturday): slug stays the ordinary ferial key"
+ "ef-time-after-pentecost-4-saturday" (slug_of (d 2026 6 27));
+ Alcotest.(check string) "2026-06-27: white overrides Time after Pentecost's own green (RG 127)" "white"
+ (colour_of (d 2026 6 27));
+ Alcotest.(check bool) "2026-06-27: subject Bvm" true (subject_of (d 2026 6 27) = Sub.Bvm);
+ (* Ordinary Paschaltide, clean, outside the privileged Easter octave: 18
+ April 2026. Season colour there is ALREADY white (RG 119) -- colour
+ alone cannot distinguish the override firing from it simply not being
+ needed, so subject/name are the load-bearing assertions on this one,
+ not colour (the SAME reasoning [test_holy_family]'s own "Christmastide
+ already white" gap does not arise for -- this is a genuine, deliberate
+ exception recorded here, not an oversight). *)
+ Alcotest.(check string) "2026-04-18 (Paschaltide II Saturday): slug stays the ordinary ferial key"
+ "ef-easter-2-saturday" (slug_of (d 2026 4 18));
+ Alcotest.(check string) "2026-04-18: white (coincides with Paschaltide's own RG 119 white -- not a \
+ distinguishing witness on its own)"
+ "white" (colour_of (d 2026 4 18));
+ Alcotest.(check bool) "2026-04-18: subject Bvm (the real witness this date proves, since colour alone \
+ cannot)"
+ true (subject_of (d 2026 4 18) = Sub.Bvm);
+ Alcotest.(check (option string)) "2026-04-18: Latin name present too" (Some "Officium sanctae Mariae in sabbato")
+ (name_la_of (d 2026 4 18));
+ (* Negative controls: RG 78's own protasis ("in quibus occurrit Officium
+ de feria IV classis") is false for these -- each is already II or III
+ class, never reaching Class4 at all, so none of them is overridden. *)
+ Alcotest.(check string) "2026-03-14 (an ordinary Lent Saturday): stays III class, violet, Temporal"
+ "class-3" (rank_of (d 2026 3 14));
+ Alcotest.(check string) "2026-03-14: violet, NOT overridden (RG 78 never applies -- already III class)"
+ "violet" (colour_of (d 2026 3 14));
+ Alcotest.(check bool) "2026-03-14: subject stays Temporal" true (subject_of (d 2026 3 14) = Sub.Temporal);
+ Alcotest.(check string) "2026-12-12 (an ordinary Advent Saturday, before 17 Dec): stays III class"
+ "class-3" (rank_of (d 2026 12 12));
+ Alcotest.(check bool) "2026-12-12: subject stays Temporal (not overridden)" true
+ (subject_of (d 2026 12 12) = Sub.Temporal);
+ Alcotest.(check string) "2026-02-28 (Lent Ember Saturday): stays II class, NOT overridden" "class-2"
+ (rank_of (d 2026 2 28));
+ Alcotest.(check bool) "2026-02-28: subject stays Temporal" true (subject_of (d 2026 2 28) = Sub.Temporal);
+ Alcotest.(check string) "2026-09-26 (September Ember Saturday): stays II class, NOT overridden" "class-2"
+ (rank_of (d 2026 9 26));
+ (* A non-Saturday day in the SAME week as a clean BVM Saturday stays an
+ ordinary ferial -- the override is exactly one weekday wide, not the
+ whole week. 26 June 2026 is the Friday immediately before the 27 June
+ Saturday pinned above. *)
+ Alcotest.(check string) "2026-06-26 (Friday, day before a BVM Saturday): plain ferial, not overridden"
+ "ef-time-after-pentecost-4-friday" (slug_of (d 2026 6 26));
+ Alcotest.(check string) "2026-06-26: green, the season's own colour, unaffected" "green"
+ (colour_of (d 2026 6 26));
+ Alcotest.(check bool) "2026-06-26: subject stays Temporal" true (subject_of (d 2026 6 26) = Sub.Temporal)
+
let test_totality () =
(* Every day of 2026 yields an office without raising. Not a slug
re-validation -- Slug.t is a private string validated on construction,
@@ -848,6 +987,7 @@ let suite_extra =
Alcotest.test_case "colours" `Quick test_colours;
Alcotest.test_case "sacred triduum identity (RG 91 entry 2, Gap 1)" `Quick
test_sacred_triduum_identity;
+ Alcotest.test_case "BVM Saturday office (RG 91 entry 27, RG 78-79)" `Quick test_bvm_saturday;
Alcotest.test_case "christmastide feria slugs" `Quick test_christmastide_feria_slugs;
Alcotest.test_case "named days carry their week" `Quick test_named_days_carry_their_week;
Alcotest.test_case "totality" `Quick test_totality;