aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CLAUDE.md120
-rw-r--r--data/ef/expected-divergences-missalemeum.sexp34
-rw-r--r--lib/kernel/validate.ml33
-rw-r--r--lib/kernel/validate.mli26
-rw-r--r--lib/rites/rite_ef/precedence_ef.ml86
-rw-r--r--lib/rites/rite_ef/precedence_ef.mli44
-rw-r--r--test/test_oracle.ml146
-rw-r--r--test/test_precedence_ef.ml21
-rw-r--r--test/test_rite_ef.ml44
9 files changed, 402 insertions, 152 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 0f18537..4fb93f9 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -114,38 +114,53 @@ against missalemeum; layer 5 pins ~30 dates.
bootstrapped from lectio, so an error both inherited is invisible here. Proven:
Holy Thursday was violet in both because both were wrong.
- **Layer 4's commemoration-identity gap is CLOSED** (2026-08-12, Task B, branch
- `ef-rg16a`): it used to compare presence/count only, never *which* commemoration
- won — reversing `admit`'s dignity sort (the engine admitting the *worst*
- commemoration, an outright RG 111 violation) left all eight differential and
- oracle assertions green across 17 531 days. Layer 4 now also compares **identity**
- — colitur's own resolved English name (`Celebration.t.names`, `en`) against
- missalemeum's title text, for every day both streams admit the same count. The
- mapping resolves **every SANCTORAL-origin commemoration** (colitur's own name
- field, bootstrapped from lectio, verified to match missalemeum's titles
- character-for-character); it **cannot resolve a TEMPORAL-origin one** (an
- impeded feria/Ember/Rogation day — `Rite_ef.Temporal_ef` never sets a celebration
- name) — that case is **never silently skipped**: it is a separate, counted,
- allow-listed outcome (`Comm_identity_unresolved`, 19 of 227 non-empty-commemoration
- days over 2026–2027), not a silent pass. Two genuine identity mismatches surfaced
- and were adjudicated (register §4/§6.1, `data/ef/expected-divergences-missalemeum
- .sexp` M16/M17): a known unimplemented office (the Seven Sorrows of Passion
- Friday) and the RG 113 same-band tie-break residual (below), the latter left
- explicitly OPEN, not defaulted to either side. Proof of teeth: reversing
- `admit`'s *dignity*-based sort (the same historical defect shape) now turns
- layer 4 red — `test_no_unexplained_differences` reports an unexplained
- `commemoration-identity-mismatch` day (colitur admitting "St. Thecla" where both
- the rubric and missalemeum require "St. Linus") — reverted after confirming.
+ `ef-rg16a`, two fix rounds): it used to compare presence/count only, never
+ *which* commemoration won — reversing `admit`'s dignity sort (the engine
+ admitting the *worst* commemoration, an outright RG 111 violation) left all
+ eight differential and oracle assertions green across 17 531 days. Layer 4 now
+ also compares **identity** — colitur's own resolved English name
+ (`Celebration.t.names`, `en`) against missalemeum's title text, for every day
+ both streams admit the same count. The mapping resolves **every
+ SANCTORAL-origin commemoration** (colitur's own name field, bootstrapped from
+ lectio, verified to match missalemeum's titles character-for-character); it
+ **cannot resolve a TEMPORAL-origin one** (an impeded feria/Ember/Rogation day —
+ `Rite_ef.Temporal_ef` never sets a celebration name) — that case is **never
+ silently skipped**: it is a separate, counted, allow-listed outcome
+ (`Comm_identity_unresolved`, 19 of 227 non-empty-commemoration days over
+ 2026–2027), not a silent pass. ONE genuine identity mismatch remains adjudicated
+ open (register §4/§6.1, `data/ef/expected-divergences-missalemeum.sexp` M16): a
+ known unimplemented office (the Seven Sorrows of Passion Friday). Proof of
+ teeth, reproduced twice: reversing `admit`'s *dignity*-based sort (the
+ historical defect shape) turns layer 4 red — an unexplained
+ `commemoration-identity-mismatch` day (colitur admitting "St. Thecla" where
+ both the rubric and missalemeum require "St. Linus"); separately, disabling
+ `band`'s own `Commemoration_only` guard (below) also turns it red, on the same
+ date this whole gap was originally found through — both reverted after
+ confirming.
- **The `admit` same-rank tie-break is RG 113, not an uncited convention** (same
- task): RG 113's own second sentence ("in admittendis et ordinandis aliis
- commemorationibus, servetur ordo tabellae praecedentiae"), previously quoted only
- in its first half, is the real rule — `admit` now orders/selects by
+ task, fix round 1): RG 113's own second sentence ("in admittendis et ordinandis
+ aliis commemorationibus, servetur ordo tabellae praecedentiae"), previously
+ quoted only in its first half, is the real rule — `admit` now orders/selects by
`Precedence_ef.band` (RG 91's own table), not RG 8's coarse four-class rank.
- Measured (not assumed): the fix's blast radius against the pre-change binary is
- **zero admitted-commemoration changes across the entire 1583–9999 domain** — a
- correctness-of-citation fix, not an answer-changing one, on this codebase's
- current data. A narrower residual remains open where two candidates land on the
- *identical* RG 91 table entry (RG 113 gives no further instruction there);
- `admit` still breaks that alphabetically, still uncited, register §6.1.
+ **`band` itself had a fidelity bug this exercise surfaced**: RG 91's table
+ enumerates only "dies liturgici" (real feasts), so a `Celebration.t.status =
+ Commemoration_only` candidate has NO row in it at all — `band` used to read
+ `rank` alone and silently lent such a candidate the same table entry as a
+ genuine `Feast` of its own rank, manufacturing ties RG 113 never created (the
+ original "Maurice vs Thomas of Villanova, both entry 24" example was this bug,
+ not a real RG 91 tie). Fixed at the source: `band` now returns `unclassified`
+ for any `Commemoration_only` candidate, checked first. Measured, independently,
+ twice (`compare_precedence`'s own ordering-criterion change, then `band`'s
+ fidelity fix): the ORDERING-CRITERION change alone is zero-blast-radius
+ (byte-identical across the whole 1583–9999 domain — a correctness-of-citation
+ fix, not an answer-changing one); the `band`-FIDELITY fix has a real, large,
+ fully-classified effect, **4 451 days across the whole domain, exactly 4
+ verified shapes, no surprises** (register §6.1). A genuine "two different
+ candidates on the identical REAL table entry" residual was searched for
+ exhaustively across the whole domain and found EMPTY — the tie-break `admit`
+ still breaks alphabetically is real but narrower than first thought: it is only
+ ever exercised between two `Commemoration_only` candidates, neither of which has
+ any RG 91 table position to compare in the first place.
## Current state (Plans 1–3 DONE — verify with `git log`)
@@ -214,8 +229,10 @@ dune exec colitur -- day 2026 | head
- **Plan 4 — OF rite module** (proves `RITE` generalizes) → the lectionary
bootstrap and citations → full output/rendering → hardening and a first tag.
- **Open Plan 4 with the two behaviour items below**, before OF: they are the
- places the five layers, taken together, currently sleep through a real error.
+ **Both behaviour items below are now RESOLVED** (RG 16(a) and commemoration
+ identity, closed on branch `ef-rg16a`) — kept here as the record of what the
+ five layers, taken together, used to sleep through, and as the shape a
+ future gap of the same kind would need to be caught by.
### Carried into Plan 4 (read before starting)
@@ -259,24 +276,27 @@ nowhere in git (`docs/` is gitignored).
— a feast/mystery of one Divine Person excludes a commemoration of
another of the SAME Person — entirely unimplemented, confirmed live on 7
real years).
-2. **Commemoration identity is unasserted** outside ~3 test rows (see the layer
- caveats above) — **the exact gap the RG 16(a) fix round exploited**: the
- lectio differential compares season/slug/rank/colour only, never
- commemorations, so of the 5 996 days RG 16(a) changes, the differential
- sees at most the handful where the OBSERVED slug itself also changes (the
- Purification's own 1 200, and only because it happens to change who is
- observed) — RG 16(a) proper moved ZERO differential rows either round.
- The missalemeum oracle window (2026–2027) contains zero live RG 16(a)
- instances at all. Every finding in the RG 16(a) fix round, both the
- original bug and the fix-round-1 review's own catch (Pope Sixtus II
- wrongly admitted, both golden pins promoted from output rather than
- independently derived), rested entirely on unit tests and two golden
- pins — this is precisely how a defect of this shape gets through
- undetected for as long as it did, and precisely how the review's own
- correction had to be caught by the same narrow means. Related:
- `compare_dignity` breaks same-rank ties
- **alphabetically**, with no RG warrant, and over 2005–2050 the admission cut
- falls between two same-rank candidates on **66 days**.
+2. **Commemoration identity — RESOLVED (Task B, branch `ef-rg16a`, 2026-08-12;
+ ONE FIX ROUND of review after the first pass — see register §6.1 for the
+ full, corrected account).** Was unasserted outside ~3 test rows — **the
+ exact gap the RG 16(a) fix round above had exploited**: the lectio
+ differential (layer 3) compares season/slug/rank/colour only, never
+ commemorations, BY DESIGN (lectio has no RG 111 admission logic of its
+ own) and still does not — that part of this item is unchanged and remains
+ the reason layer 4, not layer 3, had to close this gap. Layer 4
+ (missalemeum, 2026–2027) now compares commemoration IDENTITY, not only
+ presence/count (see the "know what each layer cannot see" section above
+ for the mapping and its limits). While building it, found and fixed a
+ SECOND, independent bug the exercise surfaced: `Precedence_ef.band` gave a
+ `Commemoration_only` candidate the same RG 91 table entry as a genuine
+ `Feast` of its own rank (RG 91's table has no row for a bare commemoration
+ at all) — **4 451 days wrong across the whole 1583–9999 domain**, exactly
+ 4 verified shapes, fixed at the source. The `admit` same-rank tie-break
+ itself is RG 113 (previously uncited), not the alphabetical convention
+ this item used to describe — reconciled against the Plan-3-era "66 days"
+ figure: 599 is the tie POPULATION, 65 (or 67) the real ADMITTED-SET
+ decisions within it, 149 order-only — all now independently reproduced
+ (register §6.1), not merely asserted.
**Structural items, cheapest to fix before OF exists:**
diff --git a/data/ef/expected-divergences-missalemeum.sexp b/data/ef/expected-divergences-missalemeum.sexp
index 2488cb8..53e93d4 100644
--- a/data/ef/expected-divergences-missalemeum.sexp
+++ b/data/ef/expected-divergences-missalemeum.sexp
@@ -16,17 +16,22 @@
; did not build. Those are honestly verdicted [missalemeum] -- colitur is
; short a feature or a row, not correct -- and each is cross-referenced
; into docs/research/rules-register.md §6 as an open item, not silently
-; absorbed as if colitur were right. THREE entries (M11, M13, and, since
-; Task B/branch ef-rg16a, M17) are [verdict open] -- corrected, final fix
-; wave, item 7: this note previously said "one entry (M13)", missing M11
-; (whose own verdict changed from [colitur] to [open] in fix round 1, see
-; M11's own entry below); M17 (the RG 113 same-band tie-break residual) is
-; a genuinely new open item from the commemoration-identity task, not a
-; stale-comment fix. ONE entry (M15) carries a fourth verdict,
-; [unresolvable] -- a LIMIT of this comparator (no English name exists on
-; colitur's side for a temporal-origin commemoration to compare), not a
-; rubric dispute or a data gap either engine is wrong about; see M15's own
-; note. All are adjudicated as UNRESOLVED/UNRESOLVABLE after real
+; absorbed as if colitur were right. TWO entries (M11 and M13) are
+; [verdict open] -- corrected, final fix wave, item 7: this note previously
+; said "one entry (M13)", missing M11 (whose own verdict changed from
+; [colitur] to [open] in fix round 1, see M11's own entry below). Task B
+; (branch ef-rg16a) briefly added a THIRD, M17, adjudicated [open] as a
+; "genuine same-band tie RG 113 does not resolve" -- CORRECTED, Task B fix
+; round 1: M17 was wrong. The tie was manufactured by Precedence_ef.band
+; lending a Commemoration_only candidate the same table entry as a genuine
+; Feast of its own rank (RG 91's table has no row for a bare commemoration
+; at all); fixed in band itself, and the divergence disappears entirely --
+; M17 was REMOVED, not re-adjudicated to a different verdict. ONE entry
+; (M15) carries a fourth verdict, [unresolvable] -- a LIMIT of this
+; comparator (no English name exists on colitur's side for a temporal-
+; origin commemoration to compare), not a rubric dispute or a data gap
+; either engine is wrong about; see M15's own note. All remaining OPEN
+; entries are adjudicated as UNRESOLVED/UNRESOLVABLE after real
; primary-source effort, not defaulted past -- see each entry's own note
; and the task report for the full search.
;
@@ -117,10 +122,5 @@
((id M16)
(citation "1962 calendarium's March table (same primary text as M13's own citation): \"Feria VI post dominicam I Passionis: Commemoratio septem Dolorum B. Mariae Virg.\" -- a fixed commemoration of Our Lady's Seven Sorrows for the Friday of Passion Week, every year, confirmed real")
(verdict missalemeum)
- (note "27 March 2026 (Friday of Passion Week): colitur admits \"St. John Damascene\" (a real Class3 universal feast that genuinely falls that day, data/ef/sanctoral.sexp's own john-damascene entry, 27 March); missalemeum shows \"For Our Lady of the Seven Sorrows\" instead. The SAME standing gap M13 already names: the Seven-Sorrows-of-Passion-Friday commemoration is confirmed real in the primary text but not implemented anywhere in temporal_ef.ml, so colitur has no candidate for it and falls back to whichever ordinary Class3 saint actually wins that Friday. Verdict missalemeum: colitur is missing a real, primary-cited office, not differently opinioned. Register §6 open item (the same one M13's own note already tracks). Only 2026 shows here as an IDENTITY mismatch -- 2027's Friday of Passion Week IS 19 March, M13's own date, where identity comparison cannot even be reached (M13's own rank/colour mismatch already excludes that day from count-matched identity comparison, folded into M13's own widened subset above).")
- (expected_rows 1))
- ((id M17)
- (citation "RG 113 (docs/research/rules-register.md §4's own RG 113 entry, and §6.1's \"RG 113 same-band residual\" record, Task B/ef-rg16a): \"in admittendis et ordinandis aliis commemorationibus, servetur ordo tabellae praecedentiae\" reaches only the TABLE order (Precedence_ef.band); it supplies no further instruction for two candidates landing on the identical band value")
- (verdict open)
- (note "22 September 2027 (September Ember Wednesday): colitur admits \"St. Maurice and Companions, Martyrs\" (Commemoration_only, Class3); missalemeum shows \"St. Thomas of Villanova\" (Feast, Class3) instead -- both land on Precedence_ef.band entry 24 (III-class universal feasts), a genuine tie RG 113 does not resolve. colitur's own residual tie-break (alphabetical by slug, register §6.1's own uncited-convention record) picks \"maurice...\" over \"thomas...\" purely because 'm' < 't' in ASCII -- no rubrical warrant either way. NOT adjudicated colitur or missalemeum: verdict OPEN, the same explicit permission the brief gives M11/M13 (\"say so as an open item rather than absorbing it\"). Only 2027 shows this exact collision in this window -- 2026's 22 September is an ordinary (non-Ember) Tuesday, where Thomas of Villanova simply wins the day outright on both sides (colitur: observed; missalemeum: title) with Maurice commemorated alongside him identically on both, no tie to observe that year.")
+ (note "27 March 2026 (Friday of Passion Week): colitur admits \"St. John Damascene\" (a real Class3 universal feast that genuinely falls that day, data/ef/sanctoral.sexp's own john-damascene entry, 27 March); missalemeum shows \"For Our Lady of the Seven Sorrows\" instead. The SAME standing gap M13 already names: the Seven-Sorrows-of-Passion-Friday commemoration is confirmed real in the primary text but not implemented anywhere in temporal_ef.ml, so colitur has no candidate for it and falls back to whichever ordinary Class3 saint actually wins that Friday. Verdict missalemeum: colitur is missing a real, primary-cited office, not differently opinioned. Register §6 open item (the same one M13's own note already tracks). Only 2026 shows here as an IDENTITY mismatch -- 2027's Friday of Passion Week IS 19 March, M13's own date, where identity comparison cannot even be reached (M13's own rank/colour mismatch already excludes that day from count-matched identity comparison, folded into M13's own widened subset above). NOTE for whoever builds the office (fix round 1, coordinator finding 7): 27 March 2026 is a III-class day, where RG 111(d) admits TWO commemorations -- yet missalemeum admits only the Seven Sorrows and DISPLACES John Damascene entirely, not merely drops him to second place. A naive implementation (one more ordinary III-class commemoration competing for the day's two slots) would give colitur two commemorations where missalemeum shows one; whatever privilege the Seven Sorrows carries must itself explain the exclusion, not just win admission.")
(expected_rows 1))
diff --git a/lib/kernel/validate.ml b/lib/kernel/validate.ml
index 433dc99..24adcd8 100644
--- a/lib/kernel/validate.ml
+++ b/lib/kernel/validate.ml
@@ -330,12 +330,41 @@ let run (rite : ('s, 'r) Rite.t) (layer : 'r Layer.t) ~year =
season = d.Liturgical_day.temporal.Temporal.season;
weekday = d.Liturgical_day.temporal.Temporal.weekday }
in
+ (* CORRECTED (Task B fix round 1, coordinator finding 5): [origin]
+ used to be reconstructed as [Sanctoral] UNCONDITIONALLY, which
+ was harmless while nothing here called [band] on the result
+ (the comment this replaces was correct about [PE.admit] itself:
+ it never reads [origin]) -- but this function ALSO now calls
+ [rite.rules.Precedence.band day_ctx cand] on every one of these
+ reconstructed candidates (the line just below), and [band] DOES
+ read [origin] (its own [is_temporal] test) to choose between its
+ temporal- and sanctoral-keyed branches. A genuinely
+ TEMPORAL-origin commemoration (a privileged Advent/Lent/
+ Passiontide feria, an Ember day, an impeded Sunday) mislabelled
+ [Sanctoral] would score the WRONG band entry (e.g. a Lent feria
+ scoring entry 23, "III-class feasts in particular calendars",
+ instead of its real entry 22) -- inert today only because
+ nothing currently asserts on the SCORE [band] returns here, only
+ on whether re-offering [admit] the same values round-trips
+ (which happens not to depend on getting [origin] right for any
+ case this codebase's data reaches -- unverified in general).
+ [Liturgical_day.t] itself still does not retain a
+ commemoration's original origin, so it is recovered the only
+ way available: a commemoration whose SLUG matches the day's own
+ temporal office is temporal-origin; every other commemoration
+ is sanctoral-origin. This is exact whenever slugs cannot
+ collide across the two streams (Task 12's own "observed" check
+ already assumes this for a different purpose), which is the
+ same assumption the rest of this codebase already leans on. *)
+ let temporal_office_slug = d.Liturgical_day.temporal.Temporal.office.Celebration.slug in
let as_candidates comms =
List.map
(fun (c, p) ->
- let cand : 'r Precedence.candidate =
- { Precedence.cel = c; origin = Precedence.Sanctoral }
+ let origin =
+ if Slug.equal c.Celebration.slug temporal_office_slug then Precedence.Temporal
+ else Precedence.Sanctoral
in
+ let cand : 'r Precedence.candidate = { Precedence.cel = c; origin } in
(cand, p, rite.Rite.rules.Precedence.band day_ctx cand))
comms
in
diff --git a/lib/kernel/validate.mli b/lib/kernel/validate.mli
index 5e55fc5..b46ca30 100644
--- a/lib/kernel/validate.mli
+++ b/lib/kernel/validate.mli
@@ -59,14 +59,24 @@ val failure_to_string : failure -> string
fixed point.
- ["admission"]: the rite's own [rules.admit] is a fixed point on what it
already admitted -- re-offering a day's [commemorations] back to
- [admit] (reconstructed with {!Precedence.Sanctoral} origin; the real EF
- admit reads only rank and slug, never origin, so this reconstruction is
- exact for it) must return exactly that same set. A cap-enforcing
- selector that is not idempotent on its own output has, by definition,
- admitted something its own rule would not admit if asked again -- the
- rite-agnostic form of "the admission limit was not exceeded" available
- without embedding a rite's specific numeric caps (RG 111's, for EF)
- into kernel code.
+ [admit] (with [observed]'s own origin reconstructed as
+ {!Precedence.Sanctoral}; the real EF admit reads only rank and slug
+ from [observed], never origin, so this reconstruction is exact for
+ it) must return exactly that same set. Each offered commemoration's
+ own origin -- CORRECTED, Task B fix round 1 -- is recovered by
+ comparing its slug against the day's own temporal office, not
+ reconstructed as [Sanctoral] uniformly: {!Precedence.rules.admit}
+ (since Task B) is handed each candidate's own {!Precedence.rules.band}
+ value, computed here exactly as {!Precedence.resolve} computes it, and
+ [band] DOES read a candidate's origin (temporal- vs sanctoral-keyed
+ branches) even though EF's own [admit] itself still does not -- a
+ mislabelled origin would silently score the wrong table entry for a
+ genuinely temporal-origin commemoration (e.g. a Lent feria) before
+ this fix. A cap-enforcing selector that is not idempotent on its own
+ output has, by definition, admitted something its own rule would not
+ admit if asked again -- the rite-agnostic form of "the admission limit
+ was not exceeded" available without embedding a rite's specific
+ numeric caps (RG 111's, for EF) into kernel code.
Total over the whole 1583..9999 domain, including [year] = 9999: the
liturgical year opening there continues into out-of-domain civil year
diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml
index 861bb1a..8452e69 100644
--- a/lib/rites/rite_ef/precedence_ef.ml
+++ b/lib/rites/rite_ef/precedence_ef.ml
@@ -110,6 +110,7 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc
int =
let cel = c.Precedence.cel in
let rank = cel.Celebration.rank in
+ let status = cel.Celebration.status in
let subject = cel.Celebration.subject in
let layer = cel.Celebration.layer in
let slug = Slug.to_string cel.Celebration.slug in
@@ -129,8 +130,41 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc
binding, not a second literal, so the two can never drift apart. *)
let entry_15_band = 15 in
let open Vocab_ef in
- (* 1: Nativity, Easter Sunday, Pentecost Sunday (I class w/ octave). *)
- if is_temporal && rank = Class1 && ((m = 12 && d = 25) || off = 0 || off = 49) then 1
+ (* CORRECTED (Task B fix round 1, ef-rg16a): a [Commemoration_only]
+ celebration has NO row in RG 91's table at all, checked FIRST, ahead of
+ every rank-keyed branch below -- RG 91's own text enumerates only "dies
+ liturgici" (entry 24's own wording, e.g., "Festa III classis, in
+ calendario Ecclesiae universae inscripta" -- FEASTS, inscribed in the
+ calendar), and the calendarium itself marks the difference in its own
+ notation: 22 September's row reads "S. Thomae de Villanova Ep. et
+ Conf., III classis. / Commemoratio Ss. Mauritii et Soc. Mm." -- Thomas
+ gets a class number (a "festum"); Maurice gets "Commemoratio" and NO
+ class number at all, because he never had a row in the table for that
+ occasion to begin with. Before this fix, [band] read [rank] alone, so
+ a [Commemoration_only] entry silently borrowed the SAME entry number
+ as a genuine [Feast] of its own rank (a Class3 Commemoration_only
+ entry banded to 24, indistinguishable from a real Class3 universal
+ feast) -- manufacturing a "tie" at {!compare_precedence}/RG 113's own
+ admission ordering that the primary text never creates: RG 113 does
+ not run out of instruction between two same-rank commemorations;
+ [band] ran out of fidelity, handing out a table row that does not
+ exist for one of them. [unclassified] (worse than every real entry) is
+ the same value already used for a candidate this table's 28 branches
+ otherwise fail to describe, which is exactly the right answer here
+ too: "not in this table" for a different reason, same table-position
+ consequence. Confirmed by the oracle: missalemeum's own commemoration
+ id embeds a rank that AGREES with the demoted status this represents
+ (e.g. sancti:09-22o:4:r for Maurice, rank 4, against Thomas's own
+ sancti:09-22:3:w, rank 3, on the day Thomas is observed -- test_oracle
+ .ml's own [test_identity_rank_corroboration]). Restricted to [status];
+ [rank] itself is untouched, since {!Celebration.status}'s own doc
+ comment is explicit that a demoted feast "retains a rank" for RG 111's
+ admission-COUNT purposes (I/II/III/IV class thresholds), which this
+ guard does not touch -- only ORDERING among candidates already offered
+ to {!admit} changes. *)
+ if status = Celebration.Commemoration_only then unclassified
+ (* 1: Nativity, Easter Sunday, Pentecost Sunday (I class w/ octave). *)
+ else if is_temporal && rank = Class1 && ((m = 12 && d = 25) || off = 0 || off = 49) then 1
(* 2: Sacred Triduum (Thu-Sat of Holy Week). *)
else if is_temporal && rank = Class1 && off >= -3 && off <= -1 then 2
(* 3: Epiphany, Ascension, Holy Trinity, Corpus Christi, Sacred Heart,
@@ -769,16 +803,44 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate)
This REPLACES a previous [compare_dignity], which sorted by RG 8's coarse
four-class [rank] ("dignity") and broke same-rank ties alphabetically by
- slug -- an engineering convention with no rubrical warrant, silently
- deciding 66 days over 2005-2050 (e.g. 22 Sept 2027, an Ember Wednesday:
- the alphabetical rule admitted "maurice-and-companions-martyrs" over
- "thomas-of-villanova", both Class3/{!band} entry 24 -- missalemeum shows
- Thomas). {!band} needs a [context] this function itself does not have
- (date/season/weekday) -- unlike [dignity], which read [rank] alone --
- so {!Precedence.resolve} now computes each candidate's own [band] value
- once, generically, and hands it to [admit] as the trailing [int] on each
- input triple (see {!Precedence.rules.admit}'s own doc). [comms] below is
- [(candidate * privilege * int) list], not the pair it used to be. *)
+ slug -- an engineering convention with no rubrical warrant. Measured,
+ fix round 1 (2005-2050, a temporary pre-fix [git worktree]): a
+ same-[rank] tie existed on 599 days, most never reaching a real
+ decision; reversing the slug tie-break alone changed the ADMITTED SET on
+ 65 of them and the printed ORDER of an already-admitted pair on a
+ further 149 (docs/research/rules-register.md §6.1's full account,
+ correcting this comment's own earlier, unreproduced "66 days" claim).
+
+ CORRECTED, fix round 1 (coordinator finding 1): checking every one of
+ those 65+149 real decisions against data/ef/sanctoral.sexp found the
+ SAME underlying shape in all of them, no exceptions -- one candidate is
+ always [Cel.Commemoration_only] (e.g. "maurice-and-companions-martyrs",
+ 22 September) and the other always a genuine [Cel.Feast] or a temporal
+ office of the SAME [rank] (e.g. "thomas-of-villanova", same day; or a
+ Lent feria, {!band} entry 22, against "paul", 22 February). That is
+ {!band}'s OWN fidelity bug, fixed separately at its source (see [band]'s
+ own top-of-branch guard, RG 91's table has no row for a bare
+ commemoration at all) -- NOT a case RG 113's table-order alone resolves,
+ since a [Commemoration_only] candidate never had a real table entry to
+ compare in the first place. With that fixed, none of these 599 days'
+ real decisions any longer depend on THIS function's own slug fallback:
+ {!band} alone (a real entry vs {!unclassified}) already decides every
+ one. What remains genuinely open -- two DIFFERENT candidates landing on
+ the IDENTICAL real table entry, e.g. two different Class3 universal
+ feasts both at entry 24 -- was checked and found EMPTY across
+ 2005-2050 (register §6.1): RG 113's own table-order, once {!band} is
+ accurate, already decides every real case this codebase's current data
+ produces; the slug fallback below is exercised only between two
+ [Commemoration_only] candidates tied at {!unclassified} (138 of the 599,
+ all order/count-invisible -- RG 91 has no table position for either of
+ them to compare, so there is nothing more specific RG 113 could supply
+ here either). {!band} needs a [context] this function itself does not
+ have (date/season/weekday) -- unlike [dignity], which read [rank] alone
+ -- so {!Precedence.resolve} now computes each candidate's own [band]
+ value once, generically, and hands it to [admit] as the trailing [int]
+ on each input triple (see {!Precedence.rules.admit}'s own doc). [comms]
+ below is [(candidate * privilege * int) list], not the pair it used to
+ be. *)
let compare_precedence (a, _, ba) (b, _, bb) =
if ba <> bb then Int.compare (ba : int) bb
else Slug.compare a.Precedence.cel.Celebration.slug b.Precedence.cel.Celebration.slug
diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli
index 49195f9..b283a17 100644
--- a/lib/rites/rite_ef/precedence_ef.mli
+++ b/lib/rites/rite_ef/precedence_ef.mli
@@ -68,10 +68,21 @@ val unclassified : int
text states an exception: entry 8 (All Souls) reads "yields to an
occurring Sunday", so on a Sunday this returns a value that
loses to entry 15 rather than the literal integer 8 (see the comment on
- entry 8 in precedence_ef.ml for the exact value and why). Total over
- every candidate {!Precedence.resolve} or {!Calendar} can construct,
- including shapes the 1962 table itself does not describe (see
- {!unclassified}). *)
+ entry 8 in precedence_ef.ml for the exact value and why). ALSO
+ {!unclassified} for any [Cel.Commemoration_only] candidate, checked
+ first, ahead of every rank-keyed branch -- CORRECTED, Task B fix round
+ 1 (coordinator finding 1): RG 91's own table enumerates only "dies
+ liturgici" (entry 24's own text, "Festa III classis..." -- FEASTS), so
+ a celebration reduced to a bare commemoration was never a row in the
+ table to begin with, confirmed by the calendarium's own notation (a
+ genuine feast carries a class number on its occurrence line; a bare
+ "Commemoratio" carries none). Before this fix, a [Commemoration_only]
+ candidate silently borrowed the SAME entry number as a genuine [Feast]
+ of its own [rank] -- manufacturing ties {!Precedence.compare_by} and
+ {!compare_precedence} both then had to break some other way, that RG 91
+ itself never created. Total over every candidate {!Precedence.resolve}
+ or {!Calendar} can construct, including shapes the 1962 table itself
+ does not describe (see {!unclassified}). *)
val band : Vocab_ef.season Precedence.context -> Vocab_ef.rank Precedence.candidate -> int
(** RG 33's marker: every Sunday slug this rite's temporal cycle produces
@@ -195,8 +206,16 @@ val september_ember_prefix : string
CORRECTED, Task B (branch ef-rg16a): this used to sort by
[Vocab_ef.rank] (RG 8's coarse four-class "dignity") and break same-rank
ties alphabetically by slug -- an engineering convention with no
- rubrical warrant, cited nowhere, silently deciding 66 days over
- 2005-2050. RG 113's own second sentence, previously uncited
+ rubrical warrant, cited nowhere. Measured, fix round 1 (2005-2050,
+ reproducible via a temporary [git worktree] at the pre-fix commit):
+ a same-[rank] tie existed in the candidate pool offered to [admit] on
+ 599 days, most never reaching a real decision (RG 111's own admission
+ count often has room for both, e.g. a III-class day admits two); of
+ those, reversing the slug tie-break ALONE actually changed the ADMITTED
+ SET on 65 days and only the printed ORDER of an already-admitted pair
+ on a further 149 (docs/research/rules-register.md §6.1's own full
+ account, superseding this comment's earlier, unreproduced "66 days"
+ claim). RG 113's own second sentence, previously uncited
(docs/research/rules-register.md §4's RG 113 entry), is the real rule:
*"in admittendis et ordinandis aliis commemorationibus, servetur ordo
tabellae praecedentiae"* -- admitting and ordering commemorations both
@@ -205,8 +224,17 @@ val september_ember_prefix : string
receive on its own, so {!Precedence.resolve} computes it once,
generically, and hands each candidate its own value as the trailing
[int] on [comms]'s triples (see {!Precedence.rules.admit}). Ties break
- on slug ONLY within one identical [band] value (RG 113 gives no further
- instruction there; still an uncited engineering convention, register §6
+ on slug ONLY within one identical [band] value -- CORRECTED, fix round
+ 1 (coordinator finding 1): [band] itself used to hand a
+ [Cel.Commemoration_only] candidate the same table entry as a genuine
+ [Cel.Feast] of its own rank (RG 91's table has no row for a bare
+ commemoration at all), manufacturing most of the "identical [band]
+ value" ties this comment used to describe as RG 113's own residual.
+ Fixed in [band] itself; checked afterward (2005-2050 and the full
+ 1583-9999 domain, register §6.1) and found EMPTY of any remaining
+ "two different real table entries tied" case -- the slug fallback below
+ is exercised only between two [Commemoration_only] candidates both at
+ {!unclassified}, still an uncited engineering convention (register §6
"RG 113 tie-break"), matching {!Precedence.compare_by}'s own fallback,
so the result never depends on the order [comms] arrives in.
diff --git a/test/test_oracle.ml b/test/test_oracle.ml
index 07a5634..2624664 100644
--- a/test/test_oracle.ml
+++ b/test/test_oracle.ml
@@ -107,18 +107,25 @@
entries; RG 110's inseparable-Peter/Paul commemoration is unimplemented
code, a real feature this task did not build) -- honestly verdicted
[missalemeum] (colitur is short a feature or a row, not right), never
- silently absorbed as if colitur were correct. THREE entries (M11, M13
- and, since Task B/ef-rg16a, M17) are [verdict open] -- CORRECTED, final
- fix wave, item 7: this comment previously said "one entry (M13)",
- missing M11, whose own verdict was changed from [colitur] to [open] in
- fix round 1 (see M11's own entry below for why) but this summary was
- never updated to match; M17 (the RG 113 same-band tie-break residual,
- register §6.1) is a genuinely NEW open item, not a stale-comment fix.
- ONE entry (M15) carries its own fourth verdict, [unresolvable] -- not a
- rubric dispute or a data gap either engine is wrong about, but a LIMIT
- of this comparator itself (see M15's own entry). All are adjudicated as
- unresolved/unresolvable, not resolved either way -- the brief's own
- explicit permission ("say so as an open item") used for real, not
+ silently absorbed as if colitur were correct. TWO entries (M11 and M13)
+ are [verdict open] -- CORRECTED, final fix wave, item 7: this comment
+ previously said "one entry (M13)", missing M11, whose own verdict was
+ changed from [colitur] to [open] in fix round 1 (see M11's own entry
+ below for why) but this summary was never updated to match. Task B
+ (branch ef-rg16a) briefly added a THIRD, M17 (the same-band tie-break
+ between Maurice and Thomas of Villanova, 22 September) -- CORRECTED,
+ fix round 1 of that same task: M17 was itself wrong. The "tie" was
+ manufactured by {!Precedence_ef.band} lending a [Commemoration_only]
+ candidate the same table entry as a genuine [Feast] of its own rank
+ (RG 91's table has no row for a bare commemoration at all -- fixed in
+ [band] itself, not here); once fixed, 22 September resolves cleanly on
+ both sides and M17 was deleted, not merely re-adjudicated. ONE entry
+ (M15) carries its own fourth verdict, [unresolvable] -- not a rubric
+ dispute or a data gap either engine is wrong about, but a LIMIT of this
+ comparator itself (see M15's own entry). All remaining OPEN entries are
+ adjudicated as unresolved/unresolvable, not resolved either way -- the
+ brief's own explicit permission ("say so as an open item") used for real,
+ not
defaulted past. See the task report for every entry's full
reasoning and primary-source citation. *)
@@ -655,30 +662,43 @@ let m15_dates =
already tracks). Only 2026 shows here -- 2027's Friday of Passion Week
IS 19 March, M13's own date, where the identity axis cannot even be
reached (M13's own rank/colour mismatch already excludes that day from
- count-matched identity comparison). *)
+ count-matched identity comparison).
+
+ NOTE for whoever builds the office (fix round 1, coordinator finding 7):
+ 27 March 2026 is a III-class day, where RG 111(d) admits TWO
+ commemorations, not one -- yet missalemeum admits only the Seven Sorrows
+ and DISPLACES John Damascene entirely (its own "displaced" list carries
+ his title that day), not merely drops him to second place. Implementing
+ the Seven Sorrows candidate naively (as one more ordinary III-class
+ commemoration competing for the day's two slots) will not reproduce
+ this: John Damascene would still win one of the two admitted slots by
+ dignity/band, giving colitur TWO commemorations where missalemeum shows
+ one. Whatever privilege or precedence the Seven Sorrows commemoration
+ carries must itself explain the exclusion, not just the admission --
+ register §6's own open item for this office should carry this caveat
+ forward. *)
let m16_dates = [ "2026-03-27" ]
-(* M17 -- Task B: the SAME-band residual tie-break (docs/research/rules-
- register.md §4's own "RG 113" entry, and §6.1's "RG 113 same-band
- residual" record) made visible for the first time by identity
- comparison. 22 September 2027 (September Ember Wednesday): colitur
- admits "St. Maurice and Companions, Martyrs" (Commemoration_only,
- Class3); missalemeum shows "St. Thomas of Villanova" (Feast, Class3)
- instead -- both land on {!Precedence_ef.band} entry 24 (III-class
- universal feasts), a genuine tie RG 113 gives no further instruction for
- (its own text only reaches "servetur ordo tabellae praecedentiae", the
- TABLE order; nothing in the primary text breaks a tie WITHIN one table
- entry). colitur's own residual tie-break (alphabetical by slug, §6.1's
- own uncited-convention record) picks "maurice..." over "thomas..." purely
- because 'm' < 't' -- no rubrical warrant either way, so this is NOT
- adjudicated colitur or missalemeum: verdict OPEN, the same explicit
- permission the brief gives M11/M13 ("say so as an open item rather than
- absorbing it"), register §6.1. Only 2027 falls in this window with this
- EXACT collision -- 2026's 22 September is an ordinary (non-Ember)
- Tuesday, so Thomas of Villanova simply wins the day outright on both
- sides (colitur: observed; missalemeum: title) with Maurice commemorated
- alongside him identically on both -- no tie to observe that year. *)
-let m17_dates = [ "2027-09-22" ]
+(* M17 was DELETED, fix round 1 (Task B): the "genuine tie" it adjudicated
+ as [open] was itself wrong. 22 September (any year the September Ember
+ Wednesday falls on the 22nd -- 2027 in this window): colitur used to
+ admit "St. Maurice and Companions, Martyrs" (Commemoration_only, Class3)
+ where missalemeum shows "St. Thomas of Villanova" (Feast, Class3) --
+ NOT because RG 113 runs out of instruction between two same-rank
+ candidates (the framing this entry used to carry), but because
+ {!Precedence_ef.band} used to lend a [Commemoration_only] candidate the
+ SAME table entry (24) as a genuine [Feast] of its own rank, manufacturing
+ a tie the primary text never creates: RG 91's own table enumerates only
+ "dies liturgici" (entry 24: "Festa III classis..." -- FEASTS), and the
+ calendarium's own 22 September row confirms it in its own notation --
+ "S. Thomae de Villanova Ep. et Conf., III classis. / Commemoratio Ss.
+ Mauritii et Soc. Mm." -- Thomas carries a class number, Maurice carries
+ none. Fixed at the source ([band] itself now returns [Precedence_ef
+ .unclassified] for any [Commemoration_only] candidate, docs/research/
+ rules-register.md §6.1's own corrected account) rather than here: 22
+ September now resolves identically on both sides with no allow-list
+ entry needed at all -- removed, not re-adjudicated to a different
+ verdict, since there is no longer a divergence to name. *)
let layer_m_reason (c : colitur_row) (o : oracle_row) diffs =
if diffs = [] then None
@@ -704,7 +724,6 @@ let layer_m_reason (c : colitur_row) (o : oracle_row) diffs =
Some "M13"
else if List.mem c.c_date m15_dates && diffs = [ Comm_identity_unresolved ] then Some "M15"
else if List.mem c.c_date m16_dates && diffs = [ Comm_identity_mismatch ] then Some "M16"
- else if List.mem c.c_date m17_dates && diffs = [ Comm_identity_mismatch ] then Some "M17"
else None
(* ---------------------------------------------------------------------- *)
@@ -862,23 +881,36 @@ let test_layer_m_counts_match_citations () =
combination would break it) that stays honest about what remains
unverified.
+ WHAT THIS DOES NOT PIN (coordinator finding 8, fix round 1, honestly
+ named rather than left implicit): the [oracle_rank = colitur_rank]
+ branch accepts ANY genuine agreement, including one this check cannot
+ independently verify is the CORRECT rank -- a regression that silently
+ flipped some entry's [rank] from [Class3] to [Class4] would land in the
+ agreement branch and pass cleanly if missalemeum's own id happened to
+ read 4 for that entry too (data drift on one side coinciding with data
+ drift on the other is not ruled out by this check, only coincidence
+ independent of any real cause is). This test pins "no third pattern
+ appears", not "every individual rank is correct" -- a narrower, still
+ genuinely useful claim (see the [> 50] population guard below, which
+ confirms the pinned shape is actually exercised at scale, not vacuously
+ true over an empty or trivial set), and this comment says so rather than
+ letting the assertion's own name imply more than it checks.
+
[Feast]-status candidates get the ORIGINAL, unrestricted check (any
- disagreement at all is unexpected) -- but this window's own data never
- actually offers a [Feast]-status LOSING candidate whose identity is
- independently clean (checked: 0 in the 2026-2027 fixture -- a [Feast]
- candidate here either wins its own day outright, in which case it is
- never a commemoration at all, or the one day where it does lose,
- 22 September 2027/Thomas of Villanova, is ITSELF the M17 tie-break
- mismatch and so is excluded by the [identity_diff = None] guard before
- ever reaching this check). Kept anyway, not deleted: a real disagreement
- would still be reported the moment one becomes reachable (a wider oracle
- window, or a data change), and this project's own vacuity catalogue
- flags "an assertion true by construction" as a defect shape to avoid,
- not "an assertion whose population happens to be empty in the one
- fixture available" -- the CHECK still does real work when its input is
- non-empty; it is the DATA, not the code, that is currently silent here. *)
+ disagreement at all is unexpected). CORRECTED (fix round 1, coordinator
+ finding 1): this comment previously claimed this branch was unreachable
+ in the 2026-2027 window (checked: 0) because its one candidate, Thomas
+ of Villanova on 22 September, was M17's own mismatch -- WRONG, once
+ traced further: M17 itself was wrong (see {!Precedence_ef.band}'s own
+ fidelity fix, register §6.1), and fixing it made 22 September resolve
+ cleanly on both sides, reachable here after all. Measured, not assumed:
+ 22 Feast-status commemorations are now examined by this branch (not
+ only Thomas of Villanova -- every OTHER genuinely clean Feast-status
+ match in the window reaches it too, which the earlier version of this
+ comment did not check for before asserting "0"). All 22 agree. *)
let test_identity_rank_corroboration () =
let oracle, colitur = compare_streams () in
+ let feast_checked = ref 0 in
let feast_mismatches = ref [] in
let commemoration_only_checked = ref 0 in
let commemoration_only_surprises = ref [] in
@@ -896,6 +928,7 @@ let test_identity_rank_corroboration () =
| Some oracle_rank -> (
match List.find_opt (fun (_, _, _, n) -> n = Some title) c.c_commemorations with
| Some (slug, colitur_rank, Cel.Feast, _) ->
+ incr feast_checked;
if colitur_rank <> oracle_rank then
feast_mismatches :=
Printf.sprintf "%s: %s oracle-id-rank=%d colitur-rank=%d" c.c_date slug
@@ -921,14 +954,21 @@ let test_identity_rank_corroboration () =
o.o_commemorations o.o_commemoration_ids)
oracle colitur;
Alcotest.(check (list string))
- "Feast-status matches: oracle id-rank agrees with colitur's own rank (none reachable in this window, \
- see this test's own comment; the check still runs)"
- [] (List.rev !feast_mismatches);
+ "Feast-status matches: oracle id-rank agrees with colitur's own rank" [] (List.rev !feast_mismatches);
Alcotest.(check (list string))
"Commemoration_only-status matches: every one fits the KNOWN oracle=4/colitur=3 convention gap -- any \
other combination would be a genuine, new surprise"
[] (List.rev !commemoration_only_surprises);
- (* Vacuity guard for the branch that IS populated in this window. *)
+ (* Vacuity guard: BOTH branches must actually run now. CORRECTED (fix
+ round 1, coordinator finding 1): the [Feast] branch used to be
+ unreachable in this window (checked: 0) because its one candidate,
+ Thomas of Villanova on 22 September, was M17's own mismatch (excluded
+ by the [identity_diff = None] guard above). Fixing {!Precedence_ef
+ .band}'s Commemoration_only fidelity (register §6.1) made that day
+ resolve cleanly, so it is reachable here too -- this guard now expects
+ at least 1, not merely documents the branch as dormant (measured: 22
+ Feast-status commemorations now examined, up from 0). *)
+ Alcotest.(check bool) "the Feast-status population actually examined is non-trivial" true (!feast_checked > 0);
Alcotest.(check bool) "the Commemoration_only population actually examined is non-trivial" true
(!commemoration_only_checked > 50)
diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml
index ca623d2..2935e40 100644
--- a/test/test_precedence_ef.ml
+++ b/test/test_precedence_ef.ml
@@ -267,7 +267,26 @@ let cases =
(* RG 91's own vigil list (§4, "Vigils / octaves / Rogations / Sunday classes") stops at III class --
there is no IV-class vigil for entry 28's ferial catch-all to absorb. *)
( "unclassified: IV-class candidate marked as a vigil", mk 2026 6 20,
- cand ~rank:V.Class4 "ef-second-mystery-vigil", PE.unclassified )
+ cand ~rank:V.Class4 "ef-second-mystery-vigil", PE.unclassified );
+ (* Task B fix round 1 (coordinator finding 1): RG 91's table enumerates
+ only "dies liturgici" -- a [Cel.Commemoration_only] candidate is
+ never a row in the table at all, regardless of what rank it would
+ otherwise band to. [Class1]/universal-layer is deliberate: absent
+ the status guard this would band to entry 11, the FIRST, highest-
+ priority real entry any sanctoral candidate can reach, so this
+ proves the guard is checked BEFORE and overrides every rank-keyed
+ branch, not only the low-priority ones an accidental placement
+ might happen to catch. Real-world witness: "maurice-and-companions-
+ martyrs" (data/ef/sanctoral.sexp, 22 September, Class3,
+ Commemoration_only) used to band to entry 24 alongside the genuine
+ Class3 feast "thomas-of-villanova" -- test_oracle.ml's own former
+ M17 allow-list entry, deleted once this fix made the divergence it
+ named disappear. *)
+ ( "unclassified: a Commemoration_only candidate has no row in RG91's \
+ table at all, even at Class1", mk 2026 5 20,
+ cand ~origin:P.Sanctoral ~status:Cel.Commemoration_only ~layer:PE.universal_layer
+ "ef-some-commemoration-only",
+ PE.unclassified )
]
(* Review finding 1's end-to-end proof: on a real Sunday landing on 2
diff --git a/test/test_rite_ef.ml b/test/test_rite_ef.ml
index 6fcb25d..2e5d2fd 100644
--- a/test/test_rite_ef.ml
+++ b/test/test_rite_ef.ml
@@ -298,6 +298,45 @@ let test_search_genuinely_enters_the_window () =
"the walked span is at least 8 days -- the octave alone (Easter..Easter+7), not a one-day hop"
true (List.length walked >= 8)
+(* Task B fix round 1 (coordinator finding 1), end to end with REAL data:
+ 22 September 2027 is a September Ember Wednesday -- the OBSERVED day is
+ the Ember office itself ([band] entry 18, RG 91, beats both sanctoral
+ candidates), and RG 111(c) ("other II class: one") admits exactly ONE
+ of the two competing losers, "maurice-and-companions-martyrs"
+ ([Commemoration_only], Class3) and "thomas-of-villanova" ([Feast],
+ Class3). Before the fix, [Precedence_ef.band] gave Maurice the SAME
+ table entry (24) as Thomas, so the residual slug tie-break picked
+ Maurice ('m' < 't') and OMITTED Thomas -- contrary to the calendarium's
+ own 22 September row ("S. Thomae de Villanova Ep. et Conf., III
+ classis. / Commemoratio Ss. Mauritii et Soc. Mm.": Thomas is the one
+ WITH a table entry) and to missalemeum (test/test_oracle.ml's own
+ former M17 allow-list entry -- oracle "commemorations": Thomas,
+ "displaced": Maurice -- deleted once this fix made the divergence
+ disappear). This is the SAME date the oracle harness itself checks,
+ pinned here too so a regression shows up at the OCaml level, not only
+ via the fixture comparison. *)
+let test_maurice_thomas_band_fidelity_end_to_end () =
+ let layer = real_layer () in
+ (* [Calendar.year]'s own liturgical-year convention (Advent-anchored, see
+ calendar.ml's [year_bounds]): the label [y] spans December of civil
+ year [y] through November of civil year [y+1], so 22 September 2027
+ is reached via [Cal.year ... 2026], not 2027 -- the same
+ [y-1]/straddling indexing test_oracle.ml's own header comment already
+ documents for exactly this reason. *)
+ let days = Cal.year Rite_ef.context layer 2026 in
+ let day = Array.to_list days |> List.find (fun d -> Date.compare d.LD.date (mk 2027 9 22) = 0) in
+ Alcotest.(check string) "the September Ember Wednesday office itself is observed (band entry 18)"
+ "ef-september-ember-wed" (slug_of day.LD.observed);
+ Alcotest.(check (list string))
+ "St Thomas of Villanova (band entry 24, a real table row) is the sole commemoration"
+ [ "thomas-of-villanova" ]
+ (List.map (fun (c, _) -> slug_of c) day.LD.commemorations);
+ Alcotest.(check (list string))
+ "St Maurice and Companions (Commemoration_only, unclassified -- no table row) is omitted, not \
+ commemorated"
+ [ "maurice-and-companions-martyrs" ]
+ (List.map (fun (c, _) -> slug_of c) day.LD.omitted)
+
let suite =
( "Rite_ef (real data: overlay-in-effect, domain-ceiling)",
[ Alcotest.test_case "the overlay suppression is observably in effect" `Quick
@@ -311,4 +350,7 @@ let suite =
Alcotest.test_case "transfer_target skips the whole Easter octave from inside Holy Week" `Quick
test_transfer_target_skips_the_whole_easter_octave;
Alcotest.test_case "the search genuinely enters the window (not vacuous)" `Quick
- test_search_genuinely_enters_the_window ] )
+ test_search_genuinely_enters_the_window;
+ Alcotest.test_case
+ "band fidelity end-to-end: Thomas of Villanova observed, Maurice commemorated (2027-09-22)" `Quick
+ test_maurice_thomas_band_fidelity_end_to_end ] )