summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-12 16:45:58 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-12 16:45:58 +0200
commit5d5454208464892e96310efd99ec9db09171f982 (patch)
treeeb6940f3087bf7cb2fd7c92063e867c95f3b0402
parent76298db9b0fb5409bcfd4276b642f69f4da530e9 (diff)
downloadcolitur-5d5454208464892e96310efd99ec9db09171f982.tar.gz
colitur-5d5454208464892e96310efd99ec9db09171f982.zip
fix(rite-ef): band has no table entry for a bare commemoration, ever
Fix round 1 on the commemoration-identity/tie-break work: the "genuine same-band tie" M17 adjudicated as open was not a tie at all. RG 91's table enumerates only "dies liturgici" (real feasts) -- entry 24's own text is "Festa III classis, in calendario Ecclesiae universae inscripta" -- so a Celebration.t.status = Commemoration_only candidate (a saint reduced to a bare commemoration) has no row in the table to begin with. Precedence_ef.band used to read rank alone, so it silently lent such a candidate the same table entry as a genuine Feast of its own rank; the calendarium's own 22 September row confirms the distinction in its own notation ("S. Thomae de Villanova ... III classis. / Commemoratio Ss. Mauritii et Soc. Mm." -- Thomas carries a class number, Maurice carries none). Fixed at the source, not by adding a status tier to compare_precedence: band now checks status first, ahead of every rank-keyed branch, and returns unclassified for any Commemoration_only candidate. M17 deleted (not re-adjudicated) -- 22 September now resolves identically on both sides, with no divergence left to name. Blast radius measured independently (a temporary git worktree at the pre-fix commit): 4451 days change across the whole 1583-9999 domain, exactly 4 verified shapes -- 3576 x maurice-and-companions-martyrs -> thomas-of-villanova and 282 x eleutherius -> philip-neri (admitted set changes), 502 x appollonia/cyril-of-alexandria and 91 x dorothy/titus (order only, independently confirmed against the calendarium's own listing convention). Zero days change who is observed. Further swept the whole domain for a genuine "two different candidates on the identical real table entry" residual -- the shape RG 113 truly gives no instruction for -- and found it empty: of 109201 same-rank ties domain-wide, 83950 now resolve by band alone and the remaining 25251 are exclusively two Commemoration_only candidates tied at unclassified, never two real feasts. The residual admit still breaks alphabetically is real, but narrower than previously documented. The "66 days" figure is reconciled, not merely disputed: 599 is the tie population, 65 (whole-comparator: 67) is the real admitted-set decision count within it (independently reproduced against the pre-Task-B binary), 149 is order-only. Every one of the 65 real decisions traces to the same Commemoration_only/Feast root cause as Maurice/Thomas. Also: validate.ml's admission fixed-point check recovers a commemoration's real origin (by matching its slug against the day's own temporal office) instead of reconstructing every candidate as Sanctoral, since band -- unlike EF's own admit -- does read origin; precedence_ef.ml/.mli's motivating example and blast-radius claims corrected to no longer rest on the fixed Maurice/Thomas case; test_identity_rank_corroboration's own population count corrected now that 22 September (and 21 other Feast-status matches) became reachable; its rank-agreement pin documents exactly what it does and does not prove. Register (§4, §6.1, §6 checklist) and CLAUDE.md corrected in place, including the RG 113 first-sentence gap this exercise surfaced but did not close, recorded as a new open item. 275 -> 276 tests (with the exhaustive sweep), all green.
-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 ] )