(* RG 91's Table of Precedence (docs/research/rules-register.md §4). Each branch below is one of the table's 28 entries, checked in the table's own numeric order -- lower wins, and because occasional entries are true exceptions to a later, broader one (RG 91 entry 18's Ember days are an exception carved out of entry 22's Lent ferias; entry 21/26's vigils are an exception carved out of the generic Class2/Class3 sanctoral-feast entries that would otherwise also match), checking in table order and returning on the first match is what makes the exception actually win without a separate exclusion for every later entry it pre-empts. Two kinds of evidence decide an entry: - The temporal cycle's own office (Nativity, a Sunday, a feria, All Souls) is identified structurally, from the context's date/season/weekday and the day's Easter offset -- never from its slug, which is just a label. [origin = Temporal] gates every such entry so a sanctoral candidate that happens to share a date (Immaculate Conception can never coincide with the movable cycle, but nothing stops a future rite bug from producing one) cannot be mistaken for the office itself. All Souls (entry 8, the one non-temporal-origin member of this group) additionally reads the context's weekday for its own register-stated exception -- see entry 8 below. - A sanctoral feast's entry is decided by its [rank], and -- except at entry 14 (see its own comment below, where the register draws no such line) -- per the brief's structural insight, also by its {!Celebration.t}.layer: a celebration whose layer is not the universal base is an overlay, hence "proper" or "indult" rather than the universal entry (11-13 I class; 16/19/20 II class; 23/24 III class; see precedence_ef.mli). [origin = Sanctoral] gates these for the same reason: temporal-origin celebrations carry the literal layer id "temporal" (rite_ef/temporal_ef.ml's [build]), which is not [universal_layer] either, and would otherwise be misread as "proper" by the layer test alone. Vigils (21, 26) are the one shape neither of those two kinds fully describes on their own: a II/III-class vigil can be temporal-origin (the Ascension Vigil, produced by temporal_ef today) or sanctoral-origin (a saint's vigil, not yet loaded by any task), so its entry cannot be gated on [origin] at all. Nothing in the day's other fields marks "this is a vigil, not an ordinary office of the same rank" either, so this reads it off the temporal cycle's own slug convention (a "-vigil" suffix -- see [named] in temporal_ef.ml) rather than guessing a new one. *) open Colitur_kernel (* Not an RG citation -- RG 91 ranks proper and indult feasts, it does not encode how a computer tells them apart. See precedence_ef.mli. *) let universal_layer = "ef-universal" let indult_prefix = "indult:" let unclassified = max_int (* CORRECTED, fix round 1 (coordinator finding 1) -- the ORIGINAL version of this constant was [-14], justified only by "cannot collide with a real table position (1..28) or [unclassified]". That is a COLLISION argument, not an ORDERING one, and [band]'s own return value is compared by {!Precedence.compare_by}/{!compare_precedence} as a plain [<] -- a negative value is not merely "distinct from 1..28", it is LOWER than every one of them, so a movable Class2 Lord feast (Holy Family) would have beaten every I-class day (entries 1-13) outright, not merely the fixed Baptism it was built to beat. Not live against the shipped UNIVERSAL sanctoral data (no I-class universal feast falls 7-13 January), which is why the original 1583-9999 blast radius (§6.2) showed only the one intended shape -- but entries 12/13 admit PROPER and INDULT I-class feasts too (patron/titular/dedication days, {!is_universal}/{!is_indult} below), which arrive via `data/ef/adjustments.sexp`-style overlays, this architecture's own advertised extension point -- a diocesan I-class patronal feast landing 7-13 January would have made the inversion live immediately, transferring the I-class feast as though a II-class Sunday had impeded it. Demonstrated directly (fix round 1, reproduced then reverted): a synthetic Class1 sanctoral candidate on 11 January was OBSERVED as Holy Family (band -14) and the synthetic candidate itself TRANSFERRED to the next day -- backwards on every count. FIXED: every {!band} branch now returns the real RG 91 entry number TIMES TEN (entry 1 -> 10, entry 28 -> 280, {!unclassified} untouched at [max_int]) instead of the bare 1..28 -- not an arbitrary rescale, a deliberate one: RG 91's own text records a "primum mobilia, deinde fixa" (movable-then-fixed) split at FIVE rows, not only this one -- entry 13 ("Festa indulta I classis, primum mobilia, deinde fixa", its own numbered row on both scans), entry 14 (this one), entry 20, entry 23's third sub-item ("deinde festa indulta, primum mobilia, deinde fixa"), and entry 24 ("Festa III classis, in calendario Ecclesiae universae inscripta, primum mobilia, deinde fixa") -- and only entry 14 has a live witness in this codebase's data today. NOTE, corrected by the fix-round re-review: this comment previously said THREE rows and attributed the first to "entry 12's own ninth sub-item". Both scans, and the register's own §4 table, put that clause at entry 13; entry 12 has eight sub-items and no such clause. Entry 24's split is the notable omission -- it is the most-evaluated band in the whole domain and is unsplit here, which is exactly the kind of sub-rank this headroom exists for. The ×10 scale reserves nine integers of genuine headroom immediately BEFORE each real entry's own value for exactly this kind of sub-rank, without requiring a second rescale if entry 12's or entry 20's own movable half ever needs one too -- a value DERIVABLE from RG 91's own table structure, not an arithmetic convenience chosen to dodge one collision. [band]'s own entry-14 comments (below) restate this scale at each branch it touches. *) let entry_14_fixed_band = 140 (* RG 91 entry 14's own text (scan-verified, docs/research/rules-register.md §4): "14. Festa Domini II classis, PRIMUM MOBILIA, DEINDE FIXA" -- feasts of the Lord, II class, MOVABLE FIRST, then fixed. Both halves are the SAME numbered table row, but the primary text states a real priority between them, not merely two disjoint categories -- [band]'s own entry-14 branches below must not literally TIE at {!entry_14_fixed_band}, because a tie would fall to {!Precedence.resolve}'s own kernel-level fallback ([compare_by], Slug.compare), which is deliberately NOT rubric-authoritative (the exact anti-pattern RG 113's own fix already corrected once for commemoration ordering, this file's own header) and would in fact pick the WRONG side here: "commemoration-of-the-baptism-of-the-lord" sorts before "ef-time-after-epiphany-sunday-1" alphabetically, backwards from "primum mobilia". Derived from {!entry_14_fixed_band}, one better (lower) than it, using the one integer of headroom the ×10 scale reserves immediately before every real entry -- not a citation to some entry "13.5" that does not exist in the primary text, just the ordering fact RG 91's own two half-rows require, expressed the same way the whole table now is. The one real witness today: Holy Family (RG 17(b), temporal_ef.ml's own [holy_family_sunday]) against the fixed Commemoration of the Baptism of the Lord (13 January) -- the only date they can ever coincide, since Holy Family only ever falls 7-13 January and no OTHER fixed Class2-Lord sanctoral entry shares that window (register §6.0's own subject audit: Transfiguration 6 Aug, Exaltation of the Cross 14 Sep, Dedication of the Archbasilica 9 Nov, the Purification 2 Feb, the Baptism 13 Jan -- the Baptism alone falls in Holy Family's own window). *) let entry_14_movable_band = entry_14_fixed_band - 1 let is_indult layer = String.starts_with ~prefix:indult_prefix layer let is_universal layer = String.equal layer universal_layer (* Not an RG citation either -- see [universal_layer] above. Nothing in {!Celebration.t} otherwise marks "this is a vigil, not an ordinary office of the same rank" (see the file's top comment), so entries 21/26 read it off the temporal cycle's own slug suffix (rite_ef/temporal_ef.ml's [named], e.g. "ef-ascension-vigil"). *) let vigil_suffix = "-vigil" (* Not an RG citation -- see [universal_layer]. Task 10's sanctoral bootstrap turned out to name its four real vigils with lectio's OWN convention, a "vigil-of-X" PREFIX (data/ef/sanctoral.sexp: vigil-of-st-lawrence, vigil-of-sts-peter-paul, vigil-of-the-assumption, vigil-of-the-nativity- of-st-john-the-baptist), not [vigil_suffix] -- exactly the mismatch Task 7's review predicted when it asked for [vigil_suffix] to be exposed. [is_vigil] below checks both conventions, so a celebration is a "vigil" for RG 91/33's purposes regardless of which layer (temporal or sanctoral) produced it. *) let vigil_prefix = "vigil-of-" let is_vigil slug = String.ends_with ~suffix:vigil_suffix slug || String.starts_with ~prefix:vigil_prefix slug (* Not an RG citation -- see [universal_layer]. Entry 18's Ember days are identified by the temporal cycle's own slug convention (rite_ef/ temporal_ef.ml's [ember]: "ef--ember-"), not re-derived here: the September anchor in particular is one of the more contested dates in the 1962 calendar (temporal_ef.ml's own comment on [third_sunday_of_september]), and re-deriving it a second time would only create a second place for that same uncertainty to drift. Only the Advent, Lent and September sets are listed: RG 91 entry 18 names exactly those three; the Whitsun (Pentecost) set is I class and falls inside the Pentecost octave, entry 10, matched below before this is ever reached. Exposed for the same reason as [vigil_suffix]: a rename of temporal_ef's format has somewhere to be caught other than a silently-wrong entry 18. [september_ember_prefix] is broken out as its own name (rather than an anonymous list literal) because Task 9's [privilege_of] needs to test the September set alone: RG 109(e)'s three named seasons (Advent, Lent, Passiontide, §4 "Commemorations") never include September, which sits entirely in time after Pentecost under any reading -- so September Ember days need their own separate privilege category, (d), regardless of how (e) itself is read. CORRECTED (fix round 1, F1/F2): this comment previously justified the split the other way round, claiming RG 109(e) privileges September specifically "while leaving the Advent and Lent sets ordinary" -- WRONG; see [privilege_of]'s own (e) comment below for the full argument. The Advent and Lent Ember sets ARE privileged under (e), the same as any other Advent/Lent feria; building [ember_prefixes] from this constant rather than duplicating the literal keeps the two from silently drifting apart. *) let advent_ember_prefix = "ef-advent-ember-" let lent_ember_prefix = "ef-lent-ember-" let september_ember_prefix = "ef-september-ember-" let ember_prefixes = [ advent_ember_prefix; lent_ember_prefix; september_ember_prefix ] let is_ember_18 slug = List.exists (fun prefix -> String.starts_with ~prefix slug) ember_prefixes let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedence.candidate) : 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 let is_temporal = c.Precedence.origin = Precedence.Temporal in let is_vigil = is_vigil slug in let date = ctx.Precedence.date in let season = ctx.Precedence.season in let weekday = ctx.Precedence.weekday in let is_sunday = weekday = Date.Sun in let m = Date.month date and d = Date.day date in (* Easter offset, the same convention as temporal_ef.ml's [days_between easter d]: 0 is Easter itself, negative before, positive after. *) let off = Date.to_rata date - Date.to_rata (Computus.gregorian_easter (Date.year date)) in (* Named so entry 8's Sunday exception below can read "one worse than the Sunday it must yield to" rather than a bare integer that happens to equal entry 15's own value; entry 15's own branch returns this same binding, not a second literal, so the two can never drift apart. CORRECTED, fix round 1 (coordinator finding 1): every branch below now returns the real RG 91 entry number TIMES TEN, not the bare 1..28 -- see {!entry_14_fixed_band}'s own comment for why (a genuine ordering bug the bare scale could not express, not a cosmetic rename). *) let entry_15_band = 150 in let open Vocab_ef in (* 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, and stays load-bearing for a demoted feast: RG 111(b)'s "scilicet de festo II classis" floor ({!admit} below) reads [rank] directly, and it is exactly that floor which excludes these Class3 entries from a II-class Sunday's single slot. Only ORDERING among candidates already offered to {!admit} changes here. NOTE, corrected: this comment previously cited {!Celebration.status}'s own doc comment as saying a demoted feast retains its rank for RG 111's admission-COUNT purposes. That misquotes it -- celebration.mli says RG 111 "orders admitted commemorations by dignity", i.e. it names the ORDERING use this guard removes, not a counting one. celebration.mli's line is itself now stale: ordering moved from dignity to [band] in ea22ad2. The guard is right; its former justification was not. *) 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 10 (* 2: Sacred Triduum (Thu-Sat of Holy Week). *) else if is_temporal && rank = Class1 && off >= -3 && off <= -1 then 20 (* 3: Epiphany, Ascension, Holy Trinity, Corpus Christi, Sacred Heart, Christ the King. *) else if is_temporal && rank = Class1 && ((m = 1 && d = 6) (* Epiphany *) || off = 39 (* Ascension *) || off = 56 (* Trinity *) || off = 60 (* Corpus Christi *) || off = 68 (* Sacred Heart *) || Date.compare date (Temporal_ef.christ_the_king (Date.year date)) = 0) then 30 (* 4: Immaculate Conception, Assumption BVM. *) else if (not is_temporal) && rank = Class1 && ((m = 12 && d = 8) || (m = 8 && d = 15)) then 40 (* 5: Vigil & Octave day of the Nativity. *) else if is_temporal && rank = Class1 && ((m = 12 && d = 24) || (m = 1 && d = 1)) then 50 (* 6: Sundays of Advent, Lent, Passiontide, and Low Sunday. *) else if is_temporal && rank = Class1 && is_sunday && (season = Advent || season = Lent || season = Passiontide || off = 7) then 60 (* 7: I-class ferias not above -- Ash Wednesday; Mon/Tue/Wed of Holy Week. Thu-Sat of Holy Week are the Triduum, entry 2 above, not this entry. *) else if is_temporal && rank = Class1 && (off = -46 || (off >= -6 && off <= -4)) then 70 (* 8: All Souls -- RG 91 entry 8's own text (§4) carries a qualifier this transcription must honour: "yields to an occurring Sunday". 2 November is always Time_after_pentecost (well clear of Advent/Lent/Passiontide and of every other entry's own Easter-relative or fixed date), so a Sunday landing on it is always an ordinary entry-15 II-class Sunday -- the one and only rival this exception ever has to lose to. On such a Sunday this returns [entry_15_band + 1]: strictly worse than 150 (an exact tie would fall to Precedence.resolve's slug tie-break, which for "ef-all-souls" against a "ef-time-after-pentecost-sunday-*" slug would make All Souls WIN -- the precise bug this guards against), but otherwise not a citation to any other RG 91 row -- nothing else can ever occur on 2 November to be confused with it. Entry 8's own [rank] is untouched by this, so Task 8's disposition (RG 95: only I-class feasts transfer) still sees the true I-class candidate it needs to move to 3 November. *) else if (not is_temporal) && rank = Class1 && m = 11 && d = 2 then if is_sunday then entry_15_band + 1 else 80 (* 9: Vigil of Pentecost. *) else if is_temporal && rank = Class1 && off = 48 then 90 (* 10: Days within the Octaves of Easter and Pentecost. *) else if is_temporal && rank = Class1 && ((off >= 1 && off <= 6) || (off >= 50 && off <= 55)) then 100 (* 11: I-class feasts of the universal Church not above. *) else if (not is_temporal) && (not is_vigil) && rank = Class1 && is_universal layer then 110 (* 12: Proper I-class feasts. *) else if (not is_temporal) && (not is_vigil) && rank = Class1 && not (is_indult layer) then 120 (* 13: Indult I-class feasts. By elimination once 11 and 12 have failed: not the universal layer (11), and marked as an indult overlay (12's "not indult" test having just failed). *) else if (not is_temporal) && (not is_vigil) && rank = Class1 then 130 (* 14, MOVABLE half -- RG 91 entry 14's own "primum mobilia, deinde fixa" ({!entry_14_movable_band}'s own comment has the full citation and reasoning). Checked before the FIXED half immediately below, matching the primary text's own order, though [is_temporal] already makes the two branches structurally disjoint on any one candidate regardless of which is checked first -- the ordering here is for a reader following RG 91's own prose, not for correctness. The only witness this codebase currently constructs is Holy Family (temporal_ef.ml's own [holy_family_sunday] branch, the one place [subject] is ever [Lord] on a temporal-origin candidate); a hypothetical movable Holy-Name-of-Jesus office (RG 17(a), still unbuilt as its own named day, register §6) would reach this same branch too, once built, since nothing here is keyed to Holy Family's own slug. *) else if is_temporal && rank = Class2 && subject = Subject.Lord then entry_14_movable_band (* 14, FIXED half -- RG 91 entry 14, deliberately UNQUALIFIED (contrast entry 16, which explicitly says "not of the Lord"; RG 37c (§4, "Sundays") speaks of "II-class feasts of the Lord" replacing an occurring II-class Sunday with no universal qualifier either). No layer test here, unlike 11/12/13 and 16/19/20: the register does not split this entry into universal/proper/indult, so a proper or indult feast of the Lord still bands {!entry_14_fixed_band}, not 19/20. *) else if (not is_temporal) && (not is_vigil) && rank = Class2 && subject = Subject.Lord then entry_14_fixed_band (* 15: Sundays, II class (every Sunday not already named at 6). *) else if is_temporal && rank = Class2 && is_sunday then entry_15_band (* 16: II-class feasts of the universal Church, not of the Lord. *) else if (not is_temporal) && (not is_vigil) && rank = Class2 && is_universal layer then 160 (* 17: Days within the Octave of the Nativity (26-28 Dec are Stephen, John, the Innocents -- sanctoral, not this entry). *) else if is_temporal && rank = Class2 && m = 12 && (d = 29 || d = 30 || d = 31) then 170 (* 18: II-class ferias -- Advent 17-23 Dec; Ember days of Advent, Lent, September. *) else if is_temporal && rank = Class2 && ((season = Advent && m = 12 && d >= 17 && d <= 23) || is_ember_18 slug) then 180 (* 19: Proper II-class feasts. *) else if (not is_temporal) && (not is_vigil) && rank = Class2 && not (is_indult layer) then 190 (* 20: Indult II-class feasts. By elimination, as at 13. *) else if (not is_temporal) && (not is_vigil) && rank = Class2 then 200 (* 21: II-class vigils (Ascension, Assumption, John Baptist, Peter & Paul -- can be temporal- or sanctoral-origin, see the file comment above). *) else if rank = Class2 && is_vigil then 210 (* 22: Ferias of Lent and Passiontide (Thursday after Ash Wednesday to the Saturday before Palm Sunday), except the Ember days (18 above). *) else if is_temporal && rank = Class3 && (season = Lent || season = Passiontide) then 220 (* 23: III-class feasts in particular calendars. Unlike 11/12 and 14/16 above, the universal entry (24) is the HIGHER number here -- RG 91's own table ranks a particular-calendar III-class feast ahead of a universal one, the reverse of the I/II-class ordering. Transcribed as the register states it, not "corrected" into the other classes' pattern. RG 91 has no indult sub-rank at III class, so every non-base layer lands here, not split further. *) else if (not is_temporal) && (not is_vigil) && rank = Class3 && not (is_universal layer) then 230 (* 24: III-class feasts in the universal calendar. *) else if (not is_temporal) && (not is_vigil) && rank = Class3 then 240 (* 25: Ferias of Advent to 16 Dec, except the Ember days (18 above). *) else if is_temporal && rank = Class3 && season = Advent then 250 (* 26: III-class vigils (St Lawrence). *) else if rank = Class3 && is_vigil then 260 (* 27: Office of the BVM on Saturday -- RG 78 (Caput IX, "De sancta Maria in sabbato"), both photographic scans and the electronic transcription, word for word (docs/research/rules-register.md §4/§6.4; Rite_ef.Temporal_ef's own [bvm_saturday_names] citation has the full argument): "78. In sabbatis, in quibus occurrit Officium de feria IV classis, fit de sancta Maria in sabbato" -- on Saturdays on which the Office of a IV-class feria occurs, [the Office] is made of Holy Mary on Saturday instead. Every otherwise-unoccupied IV-class Saturday reaches this branch; ordinary Mass propers still make Rogation Mon/Tue/Wed proper without changing the Office (RG 88, see temporal_ef.ml's [temporal]), so those never carry this entry unless they happen to fall on the Saturday itself. Excludes vigils for the same reason 11-13/14/16/19/20/23/24 do: RG 91 has no IV-class vigil at all (RG 91's own vigil list, §4 "Vigils", stops at III class), so one would be an anomaly, not this entry. *) else if is_temporal && (not is_vigil) && rank = Class4 && weekday = Date.Sat then 270 (* 28: IV-class ferias -- the unqualified catch-all (temporal_ef.ml's own comment on [ferial_rank] cites the same primary text, "Feriae IV classis"). Excludes vigils for the same reason as 27 above: a IV-class "feria" that is also a vigil is not a feria RG 91 describes. *) else if (not is_vigil) && rank = Class4 then 280 else unclassified (* RG 112(d) (Caput XVI, "De Commemorationibus"), the BVM half, fix round 1 of the ef-bvm-saturday task (coordinator finding F1) -- both photographic scans, word for word (docs/research/rules-register.md §4's own RG 112 entry already carries this sub-clause, unbuilt until this fix): *"item, Officium, Missa aut commemoratio de B. Maria Virg. aut de aliquo Sancto vel Beato excludit aliam commemorationem aut orationem in qua eiusdem B. Mariae Virg., vel Sancti aut Beati intercessio imploretur: quod tamen non valet de oratione dominicae vel feriae, in qua fit invocatio eiusdem Sancti."* -- the Office, Mass or commemoration OF the Blessed Virgin Mary (or of some Saint or Blessed) excludes ANOTHER commemoration or oration in which the intercession OF THE SAME BVM (or Saint or Blessed) is invoked -- which however does not hold of the oration of a Sunday or feria, in which invocation of the same Saint occurs. Found live by the fix-round review: the BVM Saturday Office (RG 91 entry 27, [temporal_ef.ml]'s own [bvm_saturday_names]) is itself "de B. Maria Virg." -- so when it is observed, RG 112(d) excludes any OTHER admitted candidate whose own oration invokes the SAME BVM's intercession, not merely commemorates her in passing. Real live witness: 16 July, "our-lady-of-mt-carmel" (data/ef/sanctoral.sexp, Class3, [Commemoration_only]) falls on a Saturday 8 times in the 2005-2050 fixture alone; its own collect, both photographic scans, word for word (Caput "Die 16 iulii, Beatae Mariae Virginis de Monte Carmelo, Commemoratio"): *"...concede propitius; ut, cuius hodie Commemorationem solemni celebramus officio, EIUS muniti praesidiis, ad gaudia sempiterna pervenire mereamur"* -- "her" ("eius") -- grant, we beseech Thee, that we, fortified by HER patronage, whose Commemoration we solemnly celebrate today, may be permitted to arrive at everlasting joys -- an explicit invocation of the SAME BVM's own intercession/patronage, precisely RG 112(d)'s own trigger. The exception clause ("non valet de oratione dominicae vel feriae") does NOT rescue this: RG 91 lists entry 27 as its own table row, separate from entry 28's "feriae IV classis" -- this Office is neither a Sunday nor, once RG 78 has substituted it in, a plain "feria" in Caput IV's own sense (RG 21: "Nomine feriae intelleguntur singuli dies hebdomadae" -- an ordinary weekday's OWN office, which RG 78 replaces, not merely supplements). ALSO found on the very same page, both photographic scans, word for word, confirming the collision is real and rubric-anticipated (not merely this codebase's own inference): *"Si Commemoratio B. Mariae Virg. de Monte Carmelo venerit in sabbato, Missa dici potest aut de sancta Maria in sabbato, aut propria de Commemoratione B. Mariae Virg. de Monte Carmelo."* -- if the Commemoration of the BVM of Mount Carmel falls on a Saturday, the Mass MAY be said EITHER of Holy Mary on Saturday OR properly of the Commemoration of the BVM of Mount Carmel -- an explicit EITHER/OR between two MASS TEXTS, not an instruction to commemorate one in the other's Office. Read together with RG 112(d): the OFFICE question (is Mt Carmel commemorated at all) and the MASS question (which of the two propers is said that day) are two different questions -- RG 112(d) answers the first (excluded); this rubric answers the second, and is a Mass-propers selection detail of exactly the same kind as the I-V numbered cycle (RG 309(a)) [bvm_saturday_names]'s own citation already puts out of scope for the SAME reason (colitur computes no citations/readings at all yet, Plan 4) -- not modelled here either. [marian_slugs] is a CLOSED, HAND-VERIFIED list, not a claim to have read every one of these entries' own Latin orations on the scan (only Mt Carmel's, quoted above, was actually verified against the primary text for this fix) -- built from data/ef/sanctoral.sexp's own [names.en] field, restricted to entries that are themselves a feast/commemoration OF the Blessed Virgin Mary in her own right (never merely a feast of someone else that happens to mention her -- "St. Anne, Mother of the Blessed Virgin", "St. Joseph, Spouse of the Bl. Virgin Mary", "St. Anthony Mary Claret" and similarly-named entries are deliberately EXCLUDED, checked one by one). This is the SAME modelling simplification {!disposition}'s own pre-existing RG 112(a) branch already makes for "eiusdem Divinae Personae" (read as simply "both [subject = Lord]", that branch's own comment: "safe at today's data's own granularity") -- here read as "titled a feast of the BVM", not verified oration-by- oration, and said so explicitly rather than overclaimed. THREE entries found and considered individually rather than merely omitted -- the third, "vigil-of-the-assumption" (14 August, Class2, Feast), was MISSED by the original enumeration and is recorded here by the fix-round re-review: it is unambiguously de B. Maria Virg. and its collect implores her protection ("sua nos defensione munitos"), so it BELONGS on the list on the same reasoning as the rest. It is behaviourally inert either way -- measured domain-wide, it wins 7196 times, always with the non-Marian [eusebius-confessor] as its only commemoration, and never loses, since no Class1 Marian falls on 14 August -- but a closed list's whole value is its enumeration claim, and that claim was false as first written. The other two were genuinely excluded after consideration: "dedication-of-the-basilica-of-st-mary-major" (5 August, Class3, Feast) -- a DEDICATION feast (of the building, "In Dedicatione S. Mariae ad Nives"), whose own oration could not be found in either scan under this exact heading to confirm it invokes her intercession the same way an ordinary Marian commemoration does, so left out per this project's "a wrong citation is worse than a missing one" rule; and "purification-of-the-blessed-virgin-mary" (2 February, Class2), already deliberately tagged [subject = Lord] by an earlier task's own ruling (register §6.0, following the oracle's Sunday- displacing treatment) for an unrelated reason -- its own RG 112(a) Lord-vs-Lord exclusion already covers it against another Lord-subject winner, and at Class2 it can never lose to a Class4 candidate in the first place (this or any other), so including or excluding it here has no live consequence either way; excluded for consistency with the earlier ruling rather than silently overriding it. What this rule COVERS: any admitted candidate on {!marian_slugs}' own closed list, or tagged [subject = Bvm], losing to a WINNER that is itself either {!marian_slugs}-listed or [subject = Bvm] -- currently live only for the BVM Saturday Office (subject-tagged, its own slug deliberately NOT on this list, {!bvm_saturday_names}'s own "Slug" citation in temporal_ef.ml) as winner against Mt Carmel as loser, plus (checked, not live: 24 September, "our-lady-of-ransom", the ONLY other {!Celebration.status.Commemoration_only} Marian entry in the data) -- PROVEN structurally unreachable: 24 September falling on a Saturday forces 1 September to be a Thursday (23 days = 3 weeks 2 days earlier), which by {!third_sunday_of_september}'s own construction makes 24 September the September Ember Saturday EVERY time, RG 91 entry 18 (Class2), always outranking entry 27 -- not a sampled coincidence, an exact day-of-week identity, checked against several sample years above before being generalised. What this rule does NOT cover, stated plainly: a hypothetical non-Marian-titled saint whose own oration happens to invoke the BVM's intercession in passing (RG 112(d)'s own text does not restrict itself to Marian-TITLED commemorations) -- no oration text is stored anywhere in this codebase to detect that, and none is guessed at here. Also does not cover RG 112(d)'s OTHER half (two commemorations of the SAME non-BVM saint) -- unbuilt, unaffected by this fix, register §4/§6 already tracks it as open. Checked for what this fix does NOT change: {!Precedence.resolve} holds [Commemoration_only] candidates out of the WINNER contest entirely, so Mt Carmel could never have been [observed] either before or after this fix -- only its own admission AS a commemoration changes. Verified live against real 2033/2005 data (both years 16 July is a Saturday): before this fix, [comms] included "our-lady-of-mt-carmel:ordinary"; after, [comms] is empty and the omitted list carries it with reason "omitted: yielded to a higher day", the same generic reason every other [Omit] disposition in this function produces. *) let marian_slugs = [ "annunciation-of-the-blessed-virgin-mary"; "assumption-of-the-blessed-virgin-mary"; "immaculate-conception-of-the-blessed-virgin-mary"; "immaculate-heart-of-mary"; "maternity-of-the-blessed-virgin-mary"; "most-holy-name-of-mary"; "nativity-of-the-blessed-virgin-mary"; "our-lady-of-lourdes"; (* Added by the fix-round re-review: missed by the original enumeration, behaviourally inert (it never loses; no Class1 Marian falls 14 Aug), but it is de B. Maria Virg. and its collect implores her protection, so it belongs on the list by the same test as every other member. *) "vigil-of-the-assumption"; "our-lady-of-mt-carmel"; "our-lady-of-ransom"; "our-lady-of-the-rosary"; "presentation-of-the-blessed-virgin-mary"; "queenship-of-the-blessed-virgin-mary"; "seven-sorrows-of-the-blessed-virgin-mary"; "visitation-of-the-blessed-virgin-mary" ] let is_bvm_office (c : Vocab_ef.rank Precedence.candidate) = c.Precedence.cel.Celebration.subject = Subject.Bvm || List.mem (Slug.to_string c.Precedence.cel.Celebration.slug) marian_slugs (* Task 8: what happens to the day's LOSING candidate (docs/research/ rules-register.md §4, "Occurrence" RG 92-95 and "Vigils" RG 33, plus RG 94; also §6.0/Caput III "De Dominicis" RG 16(a), below). [band] above decides who wins; this decides the loser's fate, which turns on the LOSER's own rank and status (RG 95), except RG 33's vigil omission and RG 16(a)'s Sunday-suppression, which also have to read the winner. Nothing here ever returns [Precedence.Repose]: that disposition denotes RG 100-102's *repositio* (perpetual impediment from a proper/diocesan calendar), out of this plan's scope -- see calendar.mli's own note that nothing in the EF ruleset currently emits it. *) (* RG 33 -- CORRECTED 2026-08-12 (Task 16, primary-source-verified against docs/research/1962-06-23,_SS_Ioannes_XXIII,_Missale_Romanum,_LT.pdf, the General Rubrics' own Chapter XI "De Vigiliis"). The register previously transcribed this as "a I/II-class vigil is entirely omitted"; the PRIMARY TEXT reads the other way round: "33. Vigilia II aut III classis penitus omittitur, si occurrat in dominica quavis, aut in festo I classis, vel si festum cui præmittitur in alium diem transferri aut ad commemorationem reduci contingat." -- "A vigil of the II OR III class is entirely omitted, if it occurs on ANY Sunday whatsoever, or on a feast of the I class, or if the feast it precedes happens to be transferred to another day or reduced to a commemoration." I-class vigils (Nativity, Pentecost, RG 30) are outside this rule entirely -- RG 30's own text says they "festis quibuslibet præferunt, et nullam admittunt commemorationem" (are preferred to ANY feast whatsoever, and admit no commemoration at all), i.e. they can never lose in the first place: {!band} entries 1/5/9 already rank Nativity Eve and the Pentecost Vigil above every Sunday and every other I-class row that could coincide with their fixed/Easter-relative dates (verified: no date collision is even representable), so no I-class vigil can ever reach this function as a [loser] -- the branch below never needs to test for [Class1] and, before this fix, its stray inclusion of [Class1] here was simply dead code, not a second bug (see the task report for the argument). The bug was the OTHER half: [Class3] (the sole III-class vigil, St Lawrence, RG 32) was MISSING from this branch, so it fell through to the generic "commemorated or omitted" branch below instead of RG 33's mandatory omission -- confirmed wrong for real data: 9 August 2026 is a Sunday, and before this fix "vigil-of-st-lawrence" competed for (and could in principle win) that Sunday's single commemoration slot, when RG 33 says it must not even be a candidate. The oracle comparison (missalemeum, Task 16) independently confirms: 9 Aug 2026 shows no trace of the vigil surviving as a commemoration. The third omission trigger in RG 33's own text -- "or if the feast it precedes is transferred to another day or reduced to a commemoration" -- is NOT implemented: no II/III-class vigil's own feast (Ascension, Assumption, John Baptist, Sts Peter & Paul, Lawrence) is ever transferred or reduced to a commemoration anywhere in this codebase's current data (all fixed I-class, none coincide with anything of equal or higher rank within any year this project has sampled), so no witness exists to build or test this clause against; flagged in the register (§6) rather than guessed. *) let is_omissible_vigil (rank : Vocab_ef.rank) = rank = Vocab_ef.Class2 || rank = Vocab_ef.Class3 (* Every Sunday slug this rite's temporal cycle produces -- named (temporal_ef.ml's [named], e.g. "ef-easter-sunday") or the generic "ef--sunday-" fallback ([sunday_slug]) -- contains this marker; nothing else [band] classifies does. Not an RG citation itself -- see [universal_layer]'s note on this file's own naming conventions -- exposed for the same reason as {!vigil_suffix}: a future rename of temporal_ef's Sunday-slug format has somewhere to be caught other than a silently-wrong RG 33 disposition. *) let sunday_marker = "-sunday" let contains_substring s ~needle = let ls = String.length s and ln = String.length needle in let rec at i = i + ln <= ls && (String.sub s i ln = needle || at (i + 1)) in ln = 0 || at 0 let is_sunday_slug slug = contains_substring slug ~needle:sunday_marker (* RG 33's own two conditions, taken directly from its text ("any Sunday or a I-class feast") -- NOT derived from anything about which RG 91 entries can numerically outrank a vigil. [rank = Class1] is the "I-class feast" half. [is_sunday_slug] is the "any Sunday" half, and it is not redundant with the rank check: RG 91 entries 14 and 16-20 (Feasts of the Lord II class, universal/proper/indult II-class feasts, days within the Nativity octave) are all [Class2], all outrank a II-class vigil (entry 21), and none of them is a Sunday -- a winner of that shape satisfies neither condition here, so [impedes_vigil] correctly returns [false] and such a vigil falls through to RG 95's ordinary commemorate-or-omit branch instead of RG 33's omission, exactly as the rubric requires. *) let impedes_vigil (winner : Vocab_ef.rank Precedence.candidate) = let cel = winner.Precedence.cel in cel.Celebration.rank = Vocab_ef.Class1 || is_sunday_slug (Slug.to_string cel.Celebration.slug) (* RG 91 entry 17's own slug convention (rite_ef/temporal_ef.ml's [named]: "ef-nativity-octave-day-%d" for 29-31 Dec -- 26-28 Dec are Stephen, John, the Innocents, sanctoral, and never carry this prefix, see [band]'s entry 17 comment). Not an RG citation itself -- see [universal_layer] -- reused below by [privilege_of] for RG 109(c). *) let nativity_octave_prefix = "ef-nativity-octave-day-" (* RG 109's own three named seasons for (e), "of ferias of Advent, Lent and Passiontide" (§4, "Commemorations") -- temporal_ef.ml's generic -- ferial fallback slugs, whose season word is [season_slug_word]'s output for exactly these three (vocab_ef.ml: Advent and Passiontide are unmodified [season_to_string]; Lent likewise). Also matches the Lent "after Ashes" sub-case ("ef-lent-after-ashes-", temporal_ef.ml's own [christmastide_feria_slug]-adjacent branch), which is still a Lent feria under this same prefix. Not an RG citation -- see [universal_layer] -- private: nothing outside [privilege_of] needs it. *) let alp_feria_prefixes = [ "ef-advent-"; "ef-lent-"; "ef-passiontide-" ] (* RG 80 (Caput X, "De Litaniis maioribus et minoribus", §A "De Litaniis maioribus"; both photographic scans and the electronic transcription, word for word, no scan-vs-transcription conflict to adjudicate -- docs/research/rules-register.md's own citation, this task): "80. Litaniae maiores assignatae sunt diei 25 aprilis; si vero eo die occurrit dominica Paschatis vel feria II post Pascha, transferuntur in sequentem feriam III." -- the Major Litanies are assigned to 25 April; but if Easter Sunday or Easter Monday falls on that day, they are transferred to the FOLLOWING TUESDAY. Both trigger shapes land on the SAME target, Easter+2 (worked out from the text, not independently stated by it): 25 April = Easter Sunday means the "following Tuesday" is Easter+2; 25 April = Easter Monday means Easter itself is 24 April, and the following Tuesday is again Easter+2. {!transfer_target}'s own Litanies branch, below, computes that directly. RG 81, same division, immediately following: "81. De Litaniis maioribus nihil fit in Officio, sed tantum in Missa. Earum autem commemoratio non est habenda commemoratio 'de Tempore'." -- nothing is done in the Office, only in the Mass, and its commemoration is not to be reckoned a "de Tempore" one. This is why [major_litanies_slug]'s own data/ef/adjustments.sexp entry is [Commemoration_only]: RG 91's table enumerates "dies liturgici" (Office days), and this is explicitly denied any Office standing at all -- the same reasoning {!band}'s own top-of-function guard already gives every [Commemoration_only] candidate (never a table row, never [observed]), which is exactly the behaviour RG 81 requires here independently. [major_litanies_slug] is this codebase's OWN invented English slug: RG 80/81/109(f) name the observance, never a computer key for it, and lectio does not compute the Major Litanies at all (this task's own measurement) -- nothing to adopt verbatim, unlike every other slug this file cross-checks against temporal_ef.ml's own conventions. data/ef/adjustments.sexp's own [Add] directive is the ONE other place this exact string is written; a rename here with no matching rename there silently stops every branch below from ever seeing a live candidate again -- flagged the same way [rg110_companion_slug] already flags its own three hand-maintained pairs. *) let major_litanies_slug = "major-litanies" let is_major_litanies (c : Vocab_ef.rank Precedence.candidate) = String.equal (Slug.to_string c.Precedence.cel.Celebration.slug) major_litanies_slug (* RG 109 (docs/research/rules-register.md §4, "Commemorations"): the closed list of privileged commemorations, checked in the register's own lettered order. A candidate matching none of (a)-(f) is ordinary, per the register's own closing sentence, "All others are ordinary." Read entirely off the candidate's own fields (rank, slug, origin) -- no [context] (date/season/weekday) is available or needed: every category names a property of the commemorated OFFICE ITSELF ("a commemoration OF a Sunday", "OF a I-class day", ...), not of the day it happens to fall on, and each of (a)-(e) already has a candidate-only marker this file's own conventions establish ([sunday_marker], rank, [nativity_octave_prefix], [september_ember_prefix]/[alp_feria_prefixes]) -- see the task report for the full reasoning. [disposition] below is this function's only caller, at both of its [Commemorate] sites -- replacing Task 8's [interim_privilege] placeholder, which always returned [Ordinary] regardless of the loser's real shape. [admit] (RG 108-111's admission counts, below) trusts the privilege value [disposition] has already attached rather than recomputing it here a second time. *) let privilege_of (c : Vocab_ef.rank Precedence.candidate) : Precedence.privilege = let cel = c.Precedence.cel in let rank = cel.Celebration.rank in let slug = Slug.to_string cel.Celebration.slug in let is_temporal = c.Precedence.origin = Precedence.Temporal in let open Vocab_ef in (* (a) RG 109(a) (§4): "of a Sunday" -- the same slug marker RG 33's [impedes_vigil] already reads to answer "is this candidate a Sunday". FRAGILITY, named here because this is the decision point (fix-round re-review, F5): [is_sunday_slug] is a SUBSTRING match, so the Holy Name of Jesus obtains this privilege on its Sunday shape only because its slug happens to read "ef-holy-name-sunday". That is a naming accident, not a cited rule. The real warrant is RG 17's own closing paragraph (quoted in full at temporal_ef.ml's [holy_name_sunday]): "Haec festa locum tenent dominicae occurrentis cum omnibus iuribus et privilegiis" -- these feasts hold the occurring Sunday's place WITH ALL ITS RIGHTS AND PRIVILEGES, of which RG 109(a)'s privilege is one. A slug rename would silently drop it while every test stayed green. *) if is_sunday_slug slug then Precedence.Privileged (* (b) RG 109(b) (§4): "of a I-class day" -- the candidate's own rank. In this codebase's current disposition rules the ONLY way a [Class1] candidate ever reaches [Commemorate] at all is via [Celebration.status = Commemoration_only] (a plain [Feast]-status [Class1] loser always [Transfer]s instead, RG 95, below) -- so this branch is real but its only reachable witness today is that shape; see the task report. *) else if rank = Class1 then Precedence.Privileged (* (c) RG 109(c) (§4): "of days within the Octave of the Nativity". *) else if is_temporal && String.starts_with ~prefix:nativity_octave_prefix slug then Precedence.Privileged (* (d) RG 109(d) (§4): "of September Ember days" -- named on its own because September falls entirely outside (e)'s three seasons (Advent/Lent/Passiontide) under ANY reading, not because it needs excluding FROM (e) the way review round 1's F1/F2 finding corrected the Advent/Lent Ember sets below to no longer need. *) else if is_temporal && String.starts_with ~prefix:september_ember_prefix slug then Precedence.Privileged (* (e) RG 109(e) (§4): "of ferias of Advent, Lent and Passiontide" -- CORRECTED, fix round 1 (F1/F2): this branch previously excluded the Advent and Lent Ember sets via [not (is_ember_18 slug)], reading RG 109(e)'s bare "feriis Adventus, Quadragesimae" as tacitly narrower than the ordinary ferias of those seasons, on the theory that (d)'s separate September carve-out implied Ember days needed excluding from (e) too. That reading does not survive comparing (e)'s text against RG 91's own TABLE entries for the same seasons (register §4, "Ferias of Lent and Passiontide... EXCEPTIS feriis Quatuor Temporum" at entry 22; "Ferias of Advent... EXCEPTIS feriis Quatuor Temporum" at entry 25): the table needs an explicit "exceptis" to keep Ember days from being double- listed at both their own entry 18 AND entries 22/25 -- and an explicit exception is only necessary because, ABSENT one, "feriae Adventus"/ "feriae Quadragesimae" already DO include their Ember sub-days by default (an unnecessary exception is not how a rubrical text is drafted). RG 109(e) carries no such "exceptis" clause, so its bare "feriis Adventus, Quadragesimae" is read at that same default, INCLUSIVE scope: the Advent and Lent Ember ferias ARE "ferias of Advent"/"of Lent" in RG 109(e)'s sense, hence privileged, not merely ordinary. (d)'s own separate existence is unaffected by this reading either way -- September Ember days sit in "time after Pentecost", never within Advent/Lent/Passiontide under any reading, so (d) remains necessary regardless; it is not evidence for excluding Advent/Lent Ember from (e), only for including September at all.) Consequently [is_ember_18] is no longer tested here -- an Advent/Lent Ember slug matches this branch exactly like an ordinary Advent/Lent feria slug does, via the same [alp_feria_prefixes] prefix test; only a September Ember slug is structurally excluded, because "ef-september-ember-*" never starts with any of [alp_feria_prefixes] ("ef-advent-"/"ef-lent-"/ "ef-passiontide-") in the first place -- (d) above already privileges it under its own name. *) else if is_temporal && List.exists (fun p -> String.starts_with ~prefix:p slug) alp_feria_prefixes then Precedence.Privileged (* (f) RG 109(f) (Caput XVI, "De Commemorationibus", §4): "de Litaniis maioribus, in Missa" -- of the Major Litanies, IN THE MASS (RG 81's own restriction: never in the Office). NOW LIVE (this task, ef-major-litanies): this branch used to be dead code -- "no candidate this engine can currently construct represents one" -- because nothing built the Major Litanies as a candidate at all. data/ef/adjustments.sexp's own [major_litanies_slug] entry ({!major_litanies_slug}'s own citation above has the full RG 80/81 text) is exactly that candidate now. Read by slug alone, the same convention every other one-off entry in this file uses ([nativity_octave_prefix], [rg110_companion_slug], [annunciation_slug] below) -- RG 109(f) names one specific, closed-list observance, not a structural property [is_temporal]/[rank] could derive the way (c)/(d)/ (e) above do for whole classes of temporal ferias. Deliberately NOT matched by anything above it: the Minor Litanies/Rogations ("ef-rogation-monday"/"-tuesday", RG 87) temporal_ef.ml DOES compute are a different observance RG 109(f) does not name (RG 88: the Minor Rogations change nothing in the Office at all, and [is_sunday_slug]/ [rank = Class1]/[nativity_octave_prefix]/the Ember prefixes/ [alp_feria_prefixes] never match their slugs either), so they correctly fall through to "ordinary" below, not this category -- {!privilege_cases}'s own negative row in test_precedence_ef.ml pins this boundary. *) else if is_major_litanies c then Precedence.Privileged else Precedence.Ordinary let disposition ~(winner : Vocab_ef.rank Precedence.candidate) ~(loser : Vocab_ef.rank Precedence.candidate) : Precedence.disposition = let open Vocab_ef in let cel = loser.Precedence.cel in let is_temporal = loser.Precedence.origin = Precedence.Temporal in if is_bvm_office winner && is_bvm_office loser then (* RG 112(d) -- see [marian_slugs]'s own citation above for the full argument. ON THIS BRANCH'S POSITION (fix-round re-review, finding 5): it sits ahead of RG 33's vigil branch and RG 95's Class1 Transfer branch as well as ahead of [Commemoration_only], and only the last of those three is a position it NEEDS. RG 112(d) governs commemorations, not translations, so if a Class1 Marian celebration ever lost to a Marian winner it would be silently [Omit]ed here instead of reaching RG 95 and transferring. That is unreachable on shipped data -- the re-review swept the whole domain and found ZERO Marian-winner / Marian-loser pairs other than the 16 July case this branch exists for, which is [Commemoration_only] and so could never transfer anyway -- and it is left in place rather than moved, because reordering a disposition branch is a behavioural change that deserves its own measurement and review rather than a late edit. Recorded here and in the register so the next task that touches this chain knows the constraint: if a Class1 Marian entry is ever added, this branch must move BELOW the Transfer branch. Checked FIRST, then, ahead of the [Commemoration_only] branch immediately below, because the one live witness (Mt Carmel, 16 July) IS [Commemoration_only] -- that branch's own "always Commemorate, nothing overrides it" previously had no exception for two commemorations invoking the identical BVM, the exact gap this fix closes. *) Precedence.Omit else if is_major_litanies loser && (let wslug = Slug.to_string winner.Precedence.cel.Celebration.slug in String.equal wslug "ef-easter-sunday" || String.equal wslug "ef-easter-1-monday") then (* RG 80 -- {!major_litanies_slug}'s own citation above has the full text and the "both trigger shapes land on Easter+2" derivation. data/ef/adjustments.sexp's own [major_litanies_slug] entry is UNCONDITIONALLY Fixed at 25 April, so it is offered as a candidate every year regardless of what 25 April turns out to be -- this is where the two RETRACTED blockers this task's own brief names (docs/research/rules-register.md's "the recorded blocker was wrong") actually close: [disposition] already receives [~winner], so a rite-local test on the WINNER's own slug alone (no kernel signature change, no [context]/date needed here at all) is enough to tell the 194 trigger years apart from the other 8,223 -- exactly the "zero kernel surface" the retraction predicted. "ef-easter- sunday" and "ef-easter-1-monday" are [Temporal_ef.named]'s/[temporal]'s own slugs for Easter Sunday and Easter Monday respectively (this file's own [entry_15_band]-adjacent branches above already trust the same convention); Easter Monday's own reliability as a marker (measured, register: 8,417 occurrences in 8,417 years, once per year, never displaced, being a I-class octave day) is what makes reading it off a bare slug string safe here, the same argument that retracted the second blocker. Checked ahead of the [Commemoration_only] branch immediately below: that branch's own "always Commemorate, nothing overrides it" would otherwise fire first, and this candidate would wrongly commemorate 25 April itself in exactly the 194 years RG 80 forbids it from doing so. No earlier branch in this function can pre-empt it either -- [is_bvm_office] above is keyed on Marian/BVM subjects this candidate never carries ({!major_litanies_slug}'s own entry is [subject = Saint], and its slug is not on {!marian_slugs}). [Transfer], not [Omit]: RG 80 does not say the Litanies simply vanish in a trigger year, it says WHERE they move to -- and {!Precedence.disposition}'s own [Transfer] constructor, together with the placement machinery {!Calendar} already has for RG 96 (calendar.ml's [place_transfers]/[resolve_with_injected]), is exactly "move a losing candidate to another day and inject it there" -- reused here rather than invented a second time, because RG 80's own transfer is structurally the same operation, just with its OWN named target instead of RG 96's generic forward search ({!transfer_target}'s own Litanies branch, below, computes that target directly rather than searching for it). A [Commemoration_only] candidate transferring is a new shape for this codebase, checked rather than assumed safe: {!Precedence.resolve}'s own fold matches [Transfer | Repose] uniformly, with no [status] test anywhere in it, and once re-offered at the target date this candidate is still [Commemoration_only], so it is still held out of the band contest there too (the same [forced_comm] partition, {!Precedence.resolve}'s own top comment) -- it can never accidentally become [observed] at its transfer target either, matching RG 81's "nihil fit in Officio" for the same reason it cannot become [observed] on 25 April itself. *) Precedence.Transfer else if cel.Celebration.status = Celebration.Commemoration_only then (* Always -- checked before RG 33's omission and RG 95's transfer so neither can override it: a Commemoration_only entry can never win (Precedence.resolve holds it out of the band contest entirely, see that module's [resolve]) and, per the brief, can never transfer either. Its privilege is [privilege_of loser] like every other [Commemorate] below -- Commemoration_only carries a real [rank] for exactly this purpose (Celebration.mli: "RG 111 orders admitted commemorations by dignity"), so RG 109(b) applies to it precisely as it would to any other candidate. *) Precedence.Commemorate (privilege_of loser) else if is_omissible_vigil cel.Celebration.rank && is_vigil (Slug.to_string cel.Celebration.slug) && impedes_vigil winner then (* RG 33, corrected (see {!is_omissible_vigil}'s own comment): II- or III-class vigils only -- a real I-class vigil can never reach this function as a loser at all (see that comment), so this branch would never have fired for [Class1] even before the fix; what changed is that [Class3] (St Lawrence) now correctly reaches RG 33's omission instead of falling through to the generic "commemorated or omitted" branch below. *) Precedence.Omit else if cel.Celebration.rank = Class1 && not (is_sunday_slug (Slug.to_string cel.Celebration.slug)) then (* RG 95 (§4, "Occurrence" and "Transfer/translation"): only I-class FEASTS have the right of translation -- RG 91's own table lists Sundays as a separate row (entry 6) from feasts (entries 11-13), so a Sunday is never a "feast" in RG 95's sense, and [is_sunday_slug] (the same marker RG 33's [impedes_vigil] and RG 109(a)'s [privilege_of] already use) excludes it here. This is the branch that completes Task 7's All Souls fix (RG 91 entry 8): All Souls is I class, not a vigil, and not a Sunday slug, so once it loses to an occurring Sunday it still reaches here and transfers -- to 3 November, now DIRECTLY authorised by RG 96 Attamen (b) (primary-source-verified 2026-08-12): "Commemoratio omnium Fidelium defunctorum, quando occurrit cum dominica, transfertur, tamquam in sedem propriam, in feriam II sequentem" -- when it coincides with a Sunday, transferred, as to its own proper seat, to the following Monday. Previously this rested only on entry 8's own parenthetical plus the general RG 96 walk, which happened to produce the right date; WHERE it lands either way is Rite.transfer_target's job, not this function's -- disposition only says THAT it moves. *) Precedence.Transfer else if is_temporal && (not (is_vigil (Slug.to_string cel.Celebration.slug))) && cel.Celebration.rank = Class4 then (* CORRECTED, fix round 1 (F1/F2 -- both real, the second the direct cause of the first): the branch this replaces gated on [privilege_of loser = Ordinary], justified by treating RG 109 as an "exhaustive, closed list of the only temporal-origin circumstances that ever generate a commemoration". That justification does not survive reading RG 109 itself: it is headed "Commemorationes PRIVILEGIATAE sunt commemorationes" and closes "Omnes aliae commemorationes sunt commemorationes ORDINARIAE" -- it sorts commemorations that ALREADY exist into two HONOUR classes (privileged vs ordinary, RG 108's differing liturgical hours), and says nothing about which offices have the RIGHT to be commemorated in the first place. Testing [privilege_of = Ordinary] as an ELIGIBILITY gate therefore happened to reach the right answer for IV-class ferias (they are never commemorated, but for a reason RG 109 does not state) and the WRONG answer for II- and III-class ferias impeded during a season RG 109(e) does not privilege by name (Advent 17-23 Dec's own ordinary-non-Ember ferias were fine, already matching (e)'s slug prefix; the Advent and Lent EMBER ferias were not, since the pre-fix (e) excluded them -- see [privilege_of]'s own fix-round-1 comment above, which independently corrects THAT half too). Confirmed wrong by direct reproduction (fix-round-1 review): 1900-12-21 (an Advent Ember Friday, RG 91 entry 18, II class) lost its own commemoration entirely under the pre-fix code, while an ORDINARY (non-Ember, lower-solemnity) Advent feria the same week kept its commemoration -- backwards on any reading. The actual rule is Caput IV, "De feriis" (RG 21-27), which the original Task 16 pass never opened -- a FERIAL-CLASS-keyed rule, entirely separate from RG 109's HONOUR-class one: - RG 23 (I-class ferias -- Ash Wednesday, Holy Week): "nullam admittunt commemorationem, nisi unam privilegiatam" -- admit no commemoration except one privileged one. Never actually reaches this function as a loser (these ferias structurally always outrank anything that could coincide with their dates -- {!band} entries 2/7, see that function's own file comment and the Task 11 Easter-window invariant), so this clause has no live witness, the same as before. - RG 24 (II-class ferias -- Advent 17-23 Dec, the Advent/Lent/ September Ember ferias, RG 91 entry 18): "si vero impediuntur, COMMEMORARI DEBENT" -- if indeed impeded, they MUST be commemorated. Not optional, not conditioned on RG 109's list. - RG 25 (III-class ferias -- ordinary Lent/Passiontide ferias, RG 91 entry 22; ordinary Advent ferias to 16 Dec, entry 25): "Hae feriae, si impediuntur, commemorari debent" -- same mandate. - RG 26: "Omnes feriae, numeris 23-25 non nominatae, sunt feriae IV classis; quae NUNQUAM COMMEMORANTUR" -- every feria not named in 23-25 is IV class, and IV-class ferias are NEVER commemorated. This is [ferial_rank]'s own unqualified IV-class catch-all (temporal_ef.ml), covering the ordinary green-season ferias of Time after Epiphany/Pentecost, Septuagesima, Paschaltide outside its privileged octave, and the Minor Rogation days (RG 87/88 -- they change nothing in the Office, so they take their season's plain ferial class, which for Paschaltide-adjacent dates is IV, not a special one). So this branch is now gated directly on RG 26's own condition ([rank = Class4]), which is the ONLY ferial class RG 21-27 excludes from commemoration -- Class1 is structurally unreachable here (RG 23, above); Class2 and Class3 both fall through to the final [Commemorate] branch below (RG 24/25's mandate), tagged with whatever HONOUR class [privilege_of] separately computes for them under RG 109 -- a question this branch no longer conflates with eligibility. SANCTORAL losers are entirely unaffected (the [is_temporal] guard): Caput IV governs FERIAE, RG 21's own opening definition ("Nomine feriae intelleguntur singuli dies hebdomadae, praeter dominicam"), never a saint's day; RG 111(c)/(d) admit an "ordinary" commemoration of a losing SAINT freely, with no such class-keyed gate. Empirically confirmed against the missalemeum oracle (Task 16, 2026-2027, both years): every one of ~190 days where a saint's feast impedes an ordinary (IV-class, non-privileged) temporal feria shows ZERO commemorations in the oracle (e.g. "St. Marcellus I" impeding the plain "Friday after Epiphany"), and the SAME rank-4 gate, independently, correctly still omits the Minor Rogation days (RG 87) losing to a saint -- both consequences of RG 26 alone now, not of a reading of RG 109 that RG 109's own text does not support. [is_vigil] is EXCLUDED from this branch for the same reason as before, restated under the corrected citation: a II/III-class vigil is temporal-origin too (the Ascension/Pentecost-adjacent case {!of_temporal} produces) and typically Class2, so it would already fall through this branch's [rank = Class4] test harmlessly on its own -- RG 91 has no IV-class vigil at all (this file's own entry-27/ 28 comments), so [is_vigil && rank = Class4] should never occur on real data. Kept as an explicit, defensive guard (not load-bearing for real data, but total over every candidate {!Precedence.resolve} or {!Calendar} can construct, including shapes RG 91's table itself does not describe) rather than relying on that absence silently: a vigil, per RG 31 (II class, "si impediuntur, commemorantur") / RG 32 (III class, "si impeditur, commemoratur"), is ALWAYS commemorated once RG 33 does not omit it outright, regardless of ferial class -- a rule Caput IV does not speak to at all (vigils are Caput V, RG 28-34, not "feriae"). RG 32's own full sentence, primary-source- verified (final fix wave): "Vigilia III classis est vigilia S. Laurentii. Haec vigilia praefertur diebus liturgicis IV classis; et, si impeditur, commemoratur, iuxta rubricas" -- confirmed word for word against the scan, not constructed by analogy with RG 31 (the register's own §4 "Vigils" entry states RG 32 only as "same pattern [as RG 31]", not verbatim -- now closed here). *) Precedence.Omit else if is_temporal && cel.Celebration.rank = Class2 && is_sunday_slug (Slug.to_string cel.Celebration.slug) && (let wcel = winner.Precedence.cel in (wcel.Celebration.rank = Class1 || wcel.Celebration.rank = Class2) && wcel.Celebration.subject = Subject.Lord) then (* RG 16(a) (docs/research/rules-register.md §6.0, Caput III "De Dominicis", primary text): "Dominica II classis, in occurrentia, festis II classis praefertur. Attamen: a) festum Domini I aut II classis, in dominica II classis occurrens, locum tenet ipsius dominicae cum omnibus iuribus et privilegiis: de dominica, proinde, NULLA FIT COMMEMORATIO" -- a Feast of the Lord, I or II class, occurring on a II-class Sunday, takes the Sunday's own place with all its rights and privileges: OF THE SUNDAY, THEREFORE, NO COMMEMORATION IS MADE. {!band} entry 14 already ranks such a feast above the Sunday (RG 91 entry 14 < entry 15), so the feast is correctly [observed]; this is [disposition]'s own answer for what becomes of the SUNDAY once it has lost -- [Omit], not the [Commemorate] every other impeded II-class Sunday gets via RG 109(a)/RG 111(b) below. This is the ONE place in this function that needs the WINNER's [subject], not only the loser's own fields -- like RG 33's vigil omission above, not a new kind of signature: [Precedence.rules.disposition] already takes [~winner], this is simply its first other reader. Both winner-side conjuncts are load-bearing, proved by [test_precedence_ef.ml]'s own paired rows (the brief's "one without the other proves nothing"): - [rank = Class1 || rank = Class2]: {!band} entries 11-13 admit ANY I-class feast -- Lord or Saint alike, no subject test at all (entry 14's own comment: unlike entry 16, entries 11-13 draw no such line) -- ahead of a II-class Sunday. REACHABLE at Class1 on real data: e.g. the Nativity of St John the Baptist (24 June, I class, Saint) landing on a Time-after-Pentecost Sunday already wins the day under {!band} alone; without this conjunct RG 16(a) would wrongly fire for it too. (At Class2 this conjunct adds no further cases beyond what the [subject = Lord] conjunct below already requires -- entry 16's ordinary II-class feasts never outrank a Sunday in {!band} in the first place, 16 > 15 -- so it is only independently reachable at Class1.) - [subject = Lord]: at Class2 this is what {!band} entry 14 already requires of its own winners, so it adds no further restriction there; at Class1 it is independently reachable and necessary, per the St John Baptist example above -- dropping it would fire RG 16(a) for any winning I-class feast at all, exactly the over-wide branch the task brief warns against. The SUNDAY-side conjuncts: [rank = Class2] excludes a I-class Sunday (Advent/Lent/Passiontide, Low Sunday, {!band} entry 6) -- RG 16(a)'s own text says "Dominica II classis", not "any Sunday", and a I-class Sunday can never actually reach here as a [loser] against a Class1-or-2 Lord winner in the first place, for TWO DIFFERENT reasons depending on the winner's shape (CORRECTED, fix round 1: the previous version of this comment claimed "entry 6's own band value (6) is lower than every entry [3, 11-14]" as a single numeric argument -- WRONG on its face, 6 is not lower than 3, and the claim would in any case prove the opposite of what it was cited for: if a band-3 candidate really did contest a Class1 Sunday, the LOWER number (3) would win, i.e. the Sunday would LOSE, not "always win outright" as claimed): - Against entries 11-14 (SANCTORAL-origin Lord feasts, e.g. the Transfiguration): the numeric argument genuinely holds here -- entry 6 (6) IS lower than 11-14, so a I-class Sunday always wins outright against these. - Against entry 3 (TEMPORAL-origin Lord feasts -- Epiphany, Ascension, Trinity, Corpus Christi, Sacred Heart, Christ the King): not a numeric argument at all, but a STRUCTURAL one -- every band-3 celebration is [is_temporal], and {!Precedence.resolve} takes exactly ONE temporal candidate per day ([~temporal], not a list), so a band-3 Lord feast IS that date's own single temporal candidate, never a SEPARATE candidate contesting an independently-produced Sunday on the same date. There is no collision to resolve by band comparison in the first place. Either way, no I-class Sunday can reach here as a loser against a Class1-or-2 Lord winner -- this is exactly why the rubric restricts itself to II class. [is_sunday_slug] (the same marker RG 33's [impedes_vigil] and RG 109(a)'s [privilege_of] already use, with no [is_temporal] guard there either -- no sanctoral slug this codebase's data produces contains {!sunday_marker}) is the "Dominica" half; [is_temporal] is kept alongside it anyway, the same explicit-but-not-load-bearing defence the RG26 branch above gives its own [not is_vigil] guard, rather than relying on that absence silently. *) Precedence.Omit else if winner.Precedence.cel.Celebration.subject = Subject.Lord && cel.Celebration.subject = Subject.Lord then (* PRIMARY AUTHORITY -- CORRECTED, fix round 1 (coordinator finding 7): RG 95's own SECOND paragraph (Caput XIII, "De dierum liturgicorum occurrentia accidentali", immediately after the "only I-class feasts translate" sentence {!disposition}'s own Transfer branch above already cites), present verbatim in ALL THREE documents including the electronic transcription -- an OCCURRENCE-level rule, closer to this exact question than RG 112(a) below: "Si vero duo festa eiusdem Divinae Personae aut duo festa eiusdem Sancti vel Beati simul occurrunt, fit de festo, quod in tabella praecedentiae superiorem obtinet locum et aliud omittitur" -- but if two feasts of the SAME DIVINE PERSON, or two feasts of the same Saint or Blessed, occur TOGETHER, [the Office] is made of the feast which holds the HIGHER PLACE in the table of precedence, and the OTHER IS OMITTED. This is the direct authority: two feasts of the same Divine Person (RG 112(a)'s own vocabulary) occurring together, the higher-table one kept, the other omitted -- exactly {!band}'s own [entry_14_movable_band]/[entry_14_fixed_band] ordering plus this branch's own [Omit], not [Commemorate]. CORROBORATION 1, RG 112(a) (docs/research/rules-register.md §4/§6.0, primary text, verified against all three documents -- CORRECTED, fix round 1 (coordinator finding 2): a previous version of this comment claimed the electronic transcription drops this paragraph's own worked example and the Mass-proper rubric below, "the transcription's documented defect" -- WRONG on both counts, struck. RG 112 has NO worked example in any of the three documents; there was nothing to drop. The Mass-proper rubric's absence from the transcription is not an instance of that document's documented defect either (dropped CALENDARIUM commemoration lines) -- checked directly: that specific transcription (1962-06-23,_SS_Ioannes_XXIII,_Missale_Romanum, _LT.pdf, a 2006 web capture) contains almost no Mass-propers text of any kind (5 "Introitus"/"Antiphona ad Introitum" occurrences in 26,322 lines, against 61 and 402 in the two photographic scans); its own "Proprium de Tempore" page is a TABLE OF CONTENTS linking to separate PDF files the capture never pulled in. A coverage gap in a partial web capture, not a silent drop from content it otherwise has): "112. Ad commemorationes et orationes quod attinet, haec insuper serventur: a) Officium, Missa aut commemoratio de aliquo festo vel mysterio UNIUS DIVINAE PERSONAE excludit commemorationem aut orationem de alio festo vel mysterio EIUSDEM DIVINAE PERSONAE" -- the Office, Mass, or commemoration of some feast or MYSTERY of ONE Divine Person excludes a commemoration or oration of ANOTHER feast or mystery of the SAME Divine Person. {!Subject.t} has no finer split within [Lord] than "a mystery touching the Divine Person of the Son" -- every real [subject = Lord] entry this codebase's own data carries concerns Christ specifically (register §6.0's subject audit: the Precious Blood, the Transfiguration, the Exaltation of the Cross, the Dedication of the Archbasilica, the Purification, the Baptism, and now Holy Family), so reading "same Divine Person" as simply "both [Lord]" is safe at today's data's own granularity -- a rite that ever needed to distinguish, say, a Trinity-specific mystery from a Son-specific one by Person would need a finer [Subject.t] first, not a special case added here. CORROBORATION 2 -- the Holy Family Mass propers' own note, immediately following the Postcommunion, word for word on BOTH photographic scans (real, and settles 13 January on its own even apart from RG 95/112(a) -- the quotation itself is not in dispute, only its earlier mis-attribution above): "Si festum S. Familiae occurrerit die 13 ianuarii, Missa dicitur de festo S. Familiae, SINE COMMEMORATIONE BAPTISMATIS D.N.I.C., et sine commemoratione dominicae" -- if the feast of the Holy Family occurs on 13 January, the Mass is said of the Holy Family, WITHOUT commemoration of the Baptism of Our Lord Jesus Christ, and without commemoration of the Sunday (RG 17(b)'s own general rule, restated for this specific date). Independently confirmed against missalemeum (register §6.0): title "The Holy Family: Jesus, Mary & Joseph", commemorations [], the Baptism listed only under "displaced", never commemorated. Checked after RG 16(a) above (so a genuine RG 16(a) Sunday- suppression is never re-explained under this citation instead) and before the final ordinary-commemoration catch-all below, since without it a [Feast]-status loser of this shape would otherwise reach RG 95's ordinary "commemorated or omitted" branch and be admitted as an ordinary Class2 commemoration under RG 111(b) -- confirmed wrong against all three primary sources above. *) Precedence.Omit else (* RG 95's other branch: "aut commemorantur aut penitus omittuntur" -- commemorated or wholly omitted. Reached by every SANCTORAL loser below I class (RG 111(c)/(d)'s "ordinary" commemoration, no closed list the way the temporal branch above has), AND by an impeded I-class Sunday (excluded from the [Transfer] branch above, and from the temporal Class4 [Omit] branch above because a Sunday is never IV class -- RG 11-12/91 entry 6/15 make every Sunday I or II class, never a "feria" at all in Caput IV's own sense, RG 21): RG 109(a) (§4) lists "of a Sunday" as a privileged commemoration category, which presupposes an impeded Sunday stays put rather than moving to another day the way a feast does -- [privilege_of] tags it [Privileged] via the same [is_sunday_slug] marker, with no further code needed here. Which of commemorate/omit survives is RG 108-111's admission count ([admit], below), not this function's decision; this only opens the commemoration, tagged with its real RG 109 privilege via [privilege_of]. RG 94 (a fixed-day commemoration is not carried along with a transferred feast) needs no code here: [Precedence.resolve] calls this function once per loser, always against the day's actual [observed] winner -- never against a fellow loser that itself transferred away -- so no mechanism exists by which a commemoration could ride along with a departing feast in the first place; there is nothing to suppress. *) Precedence.Commemorate (privilege_of loser) (* Task 9: how many of the day's commemorations RG 111 admits, and which (docs/research/rules-register.md §4, "Commemorations", RG 111). [band] decides who wins the day; [disposition] decides who is even eligible to be commemorated, and tags each with its RG 109 privilege via [privilege_of]; this decides how many of THOSE survive. RG 111 keys its four admission rules off the CLASS OF THE DAY ("diebus I classis", "dominicis II classis", "aliis diebus II classis", "diebus III et IV classis") -- read here off [observed]'s own [rank] and, for the Sunday/non-Sunday II-class split, the same slug marker [privilege_of] and RG 33's [impedes_vigil] already use ([is_sunday_slug]). CORRECTED (fix round 1, RG16(a) task): the previous version of this comment claimed "[observed] IS the day's own celebration, so its rank and slug already carry everything RG 111's own four categories test" -- WRONG once RG 16(a) exists. RG 16(a)'s own text says the winning Feast of the Lord holds the Sunday's place "cum omnibus iuribus et privilegiis" (with ALL its rights and privileges) -- the day remains a "dominica II classis" for RG 111(b)'s own purposes even though [observed] is now the FEAST, not the Sunday, so [observed]'s own slug is no longer a reliable signal of "is this a Sunday" once something can legitimately observe in the Sunday's place. Confirmed wrong for real data by the oracle: 6 August falling on a Sunday shows the Transfiguration observed and NO commemoration (missalemeum: `commemorations: []`, Pope Sixtus II et al. `displaced`); the pre-fix code, reading Sunday-ness off [observed]'s own slug ("transfiguration-of-our-lord", no Sunday marker), wrongly took the [Class2, false] "other II class: one" branch below and admitted Sixtus (Class3) regardless of RG 111(b)'s own "de festo II classis" rank floor. Control, also oracle-confirmed: 6 August on an ordinary WEEKDAY (no Sunday collision) correctly admits Sixtus -- being a Sunday is exactly what excludes him, and [observed]'s own identity cannot tell the two cases apart on its own. Fixed by reading Sunday-ness off [temporal] instead -- {!Precedence.rules.admit}'s own [~temporal] parameter, {!Precedence.resolve}'s [~temporal] argument passed straight through, unaffected by whether it won the day. [context] (date/season/weekday) is still not needed: [temporal]'s own slug already carries everything this split needs, the same way [observed]'s used to before a competing office could occupy the Sunday's place. *) (* RG 113 -- CORRECTED, Task B (branch ef-rg16a): docs/research/rules- register.md §4's RG 113 entry previously quoted only its FIRST sentence ("commemoratio de Tempore fit primo loco"); its SECOND, load-bearing sentence, primary-source-verified against two independent scans, is the real rule for this function: *"In admittendis et ordinandis aliis commemorationibus, servetur ordo tabellae praecedentiae"* -- in ADMITTING and ORDERING the other commemorations, the order of the table of precedence (RG 91, {!band}'s own 28-entry table) is to be kept. 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. 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, RG 91 having no table position for either of them to compare). CORRECTED 2026-08-18: this used to add "all order/count-invisible", which was true of 2005-2050 -- the range it was measured over -- and is NOT true generally. 2038-06-02 is a live counter-example, found when the 2035/2038 oracle windows began comparing commemorations: the Ascension Vigil is II class, RG 111(c) admits exactly ONE commemoration, and the two candidates are `rogation-wednesday` and `sts-marcellinus-peter-erasmus` -- both genuinely [Commemoration_only] (2 June reads "Commemoratio ... Comm." in the calendarium itself, LT.txt:5148 and scan1.txt:2828, so the data is right, not defective). The slug fallback therefore decides which commemoration the day KEEPS, not merely how two kept ones are ordered. missalemeum keeps the saints; this keeps the Rogation, on nothing but "rogation-" sorting before "sts-". AND THE RUBRIC GENUINELY DOES NOT SETTLE IT, checked rather than assumed. RG 113's second sentence needs a table position and neither has one. Its FIRST sentence, "Commemoratio de Tempore fit primo loco", looked like the answer -- a Rogation being plausibly *de Tempore* -- but RG 81 forecloses it in terms: "De Litaniis maioribus ... Earum autem commemoratio NON EST HABENDA commemoratio «de Tempore»" (LT.txt:1399-1401), and RG 89 extends RG 81 to the minor Litanies. So a Rogation commemoration is explicitly not *de Tempore*, and tagging the entry [Subject.Temporal] to reach that sentence would have been WRONG -- the [Subject.Saint] it carries, inherited from the Major Litanies precedent, stands. RG 114 says only that what exceeds the number is omitted, never which. What remains is therefore a genuine LACUNA, not an unfound rule: the slug order below is a documented CONVENTION for a case the Rubricae Generales leave open, deliberately kept rather than changed to match the oracle -- which would be adopting an answer with no more warrant than this one, from a source already adjudicated wrong six times over. See docs/research/rules-register.md §6.16. {!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 let rec take n = function | [] -> [] | x :: xs -> if n <= 0 then [] else x :: take (n - 1) xs (* [comms]'s own candidate/privilege pair, its [band] value dropped once a selection has been made -- {!Precedence.rules.admit}'s return type is still the pair, not the triple; only the INPUT carries [band]. *) let drop_band (c, p, (_ : int)) = (c, p) (* RG 110 (docs/research/rules-register.md §4, Caput XIV, "De dierum liturgicorum occurrentia perpetua"), full text, both photographic scans, word for word: "110. In Officio et Missa S. Petri semper fit commemoratio S. Pauli, et vicissim. Haec commemoratio dicitur inseparabilis; et duae orationes adeo in unam coalescere censentur ut, in numero orationum computando, pro unica habeantur. Proinde: a) in Officio S. Petri aut S. Pauli, oratio alterius Apostoli additur, ad Laudes et ad Vesperas, sub unica conclusione, orationi diei, absque antiphona et versu; b) in Missa S. Petri aut S. Pauli, oratio alterius Apostoli additur, sub unica conclusione, orationi diei; c) quoties vero oratio unius Apostoli addenda est ad modum commemorationis, huic orationi additur altera immediate, ante omnes alias commemorationes." -- in the Office and Mass of St Peter, a commemoration of St Paul is ALWAYS made, and vice versa. This commemoration is called INSEPARABLE, and the two orations are held to coalesce into one so much that, in counting the number of orations [RG 111's own admission cap], they are counted AS ONE. Accordingly: (a)/(b) in the Office/Mass OF Peter or Paul [i.e. when one of them is the day's own observed office], the other Apostle's oration is added, under a single conclusion, to the oration of the day; (c) whenever the oration of ONE Apostle must be added AS A COMMEMORATION [i.e. when one of them is not the day's own office but is itself only being admitted as an ordinary/privileged commemoration elsewhere], the OTHER is added to it immediately, BEFORE ALL OTHER COMMEMORATIONS. Three real pairs in this codebase's data, all three confirmed on both photographic scans (register §4): 25 January's [conversion-of-st-paul] + [peter]; 22 February's [chair-of-st-peter] + [paul]; 30 June's [in-commemoratione-sancti-pauli-apostoli] + [commemoration-of-st-peter] (data/ef/adjustments.sexp's own `Add` directive -- lectio has no equivalent entry at all, a genuine upstream data gap, not merely a colitur bootstrap miss). 29 June ([sts-peter-paul]) needs none: it is already the JOINT feast, not one Apostle's office alone. [peter]/[paul] already existed as ordinary [Commemoration_only] sanctoral candidates before this task (Task 10's bootstrap) -- part of RG 110's own machinery, just wired into the ORDINARY, CAPPED RG 111 admission contest instead of this rule's own UNCAPPED one, which is the defect this closes: measured (1583-9999, `tools/`'s own throwaway scan, reproduced and discarded per this task's own report) at 3,533 years where [chair-of-st-peter] is observed but [paul] loses the day's single non-Sunday-II-class slot to a competing privileged Advent/Lent/Ember feria (RG 109(e)/(d)), and a FURTHER 593 years of shape (c) below, where [chair-of-st-peter] itself loses outright to an ordinary (ordinary, i.e. non-I-class) II-class Sunday and is admitted only AS a commemoration -- [paul] was entirely absent in every one of those 593 years too, before this fix. 25 January/30 June never show shape (c) at all: both trigger candidates are [Class3], below RG 111(b)'s "de festo II classis" floor, so neither can ever be admitted as a mere ordinary commemoration on a II-class Sunday in the first place -- confirmed by the same scan, 0 instances either way, not assumed absent. [rg110_companion_slug] reads ONLY the candidate's own slug -- the same convention every other not-an-RG-citation lookup in this file uses ({!universal_layer}, {!vigil_suffix}, {!nativity_octave_prefix}) -- not [subject]/[rank]/anything else, because "which slug pairs with which" is RG 110's own closed, three-pair list, not a general property [band]/ [disposition] could derive structurally the way (say) [is_sunday_slug] derives Sunday-ness from a naming convention. A rename of any of the six slugs involved (either side of a pair) has nowhere else to be caught but here -- flagged the same way {!annunciation_slug} already is. *) let rg110_companion_slug (slug : string) : string option = if slug = "chair-of-st-peter" then Some "paul" else if slug = "conversion-of-st-paul" then Some "peter" else if slug = "in-commemoratione-sancti-pauli-apostoli" then Some "commemoration-of-st-peter" else None (* Looks [companion_slug] up directly in [comms] -- the SAME full candidate pool {!admit} itself sorts from, before any cap is applied, so this finds the companion regardless of whether the NORMAL admission process would have kept it (it never would: {!band} always ranks a [Commemoration_only] candidate at {!unclassified}, worse than literally any real table entry, so a companion competing on its own merits never survives a contested slot -- RG 110's entire point is that it should not have to). Returns the ORIGINAL triple's candidate/privilege pair, unrebuilt, for the same physical-equality reason {!drop_band} and this function's own top comment already document -- reusing the [privilege] {!disposition}/{!privilege_of} already computed for it rather than inventing a new tag here. *) let rg110_find_companion comms companion_slug = List.find_map (fun (c, p, (_ : int)) -> if Slug.to_string c.Precedence.cel.Celebration.slug = companion_slug then Some (c, p) else None) comms (* RG 110's own uncapped addition, applied to [normal] -- the day's ALREADY- COMPUTED, ORDINARILY-CAPPED admission result (every branch of {!admit} below, unchanged otherwise). Two triggers, covering RG 110(a)/(b) and RG 110(c) respectively, in this function's own comment above -- and, fix round 1, TWO DIFFERENT ORDERINGS, not one, because the two shapes' own primary texts say different things about where the companion goes: - [observed] itself is one of the three trigger slugs (shape (a)/(b): Peter's or Paul's own Office/Mass IS today's day) -- the companion is looked up directly and PREPENDED to [normal]. (a)/(b)'s own text, "oratio alterius Apostoli additur ... orationi diei" (the OTHER Apostle's oration is added ... TO THE DAY'S OWN oration), has nothing in [normal] to order the companion AGAINST in the first place: the day's own oration is [observed], not a member of this list, so leading the list is simply where an item with nothing to be "before" or "after" inside it goes -- not itself a citation for a POSITION the primary text does not describe a list position for at all. - one of [normal]'s OWN members is a trigger slug (shape (c): Peter's or Paul's own office lost the day outright but is itself being admitted as an ordinary/privileged commemoration of whatever else won) -- the companion is looked up the same way, but SPLICED IN IMMEDIATELY AFTER its own trigger, not prepended to the whole list. RG 110(c)'s own text, word for word: "quoties vero oratio unius Apostoli addenda est ad modum commemorationis, HUIC ORATIONI ADDITUR ALTERA immediate, ante omnes alias commemorationes" -- "huic orationi" (dative, "to THIS oration") refers back to "oratio unius Apostoli" (the trigger's OWN oration, the one just named as "addenda... ad modum commemorationis") -- so "additur altera" (the OTHER is added) means the companion is added TO the trigger's own oration, i.e. FOLLOWS it, not precedes it; the pair as a BLOCK then precedes "omnes ALIAS commemorationes" ("alias" = OTHER, unrelated ones -- cannot include the pair's own first member). CORRECTED, fix round 1: the original version of this function prepended in BOTH shapes uniformly, citing (c)'s own "ante omnes alias" for shape (a)/(b) too by analogy -- which is fine for WHERE the pair sits relative to unrelated commemorations, but wrongly also reordered the PAIR'S OWN internal order in shape (c), producing `paul, chair-of-st-peter` where the primary text requires `chair-of-st-peter, paul` (verified against BOTH photographic scans, word for word, no divergence to adjudicate). Confirmed live: every one of the 593 shape-(c) days measured in this task's own report emitted the companion FIRST before this fix; RG 111's own admission branches never build a second, unrelated commemoration alongside a shape-(c) trigger in today's data (RG 111(b) admits exactly one candidate on a II-class Sunday, the only day shape (c) ever occurs), so this splice-after-trigger behaviour is Total the same way {!take}/ {!drop_band} are, but has no OTHER live witness to also prove the "ante omnes alias" half of (c) against. Both shapes can never fire together on one call ({!Precedence.resolve} only ever calls {!Precedence.rules.admit} once per date, and all three trigger slugs are distinct FIXED calendar dates -- 25 January, 22 February, 30 June -- so at most one of [observed]/[normal]'s own members is ever a trigger for a given call). A companion already present in [normal] on its own merits (structurally unreachable today -- {!unclassified}'s own comment above -- but not provably so for every future rite/data shape) is not added a second time: [List.exists] guards both branches. NEW BLIND SPOT, fix round 1: no validation layer in this codebase compares commemoration ORDER at all -- the lectio differential does not compare commemorations (its own "limit 1"); the oracle's own [identity_diff] sorts both sides into a multiset before comparing; and [describe] (test_golden.ml) sorts its own comms field too, for the same reason presence/identity checks should not be order-sensitive by accident. (CORRECTED, fix-round re-review: this previously also named {!Colitur_kernel.Record}. Record.t has NO commemorations field at all -- see record.mli -- so it neither sorts nor carries them.) This function's own [admit_cases] unit test (test_precedence_ef.ml) is therefore the ONLY place in this codebase's test suite where commemoration order is asserted at all -- flagged here, and in CLAUDE.md's own "know what each layer cannot see" section, as a genuine, permanent limit, not merely this fix's own gap. *) let rg110_additions (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege * int) list) ~(observed : Vocab_ef.rank Precedence.candidate) (normal : (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list) : (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list = let already_has slug = List.exists (fun (c, _) -> Slug.to_string c.Precedence.cel.Celebration.slug = slug) normal in match rg110_companion_slug (Slug.to_string observed.Precedence.cel.Celebration.slug) with | Some companion_slug when not (already_has companion_slug) -> ( (* Shape (a)/(b): prepend -- see this function's own comment above. *) match rg110_find_companion comms companion_slug with | Some companion -> companion :: normal | None -> normal) | _ -> (* Shape (c): splice each trigger's companion in immediately after it -- {!List.concat_map} rather than a fold, so a trigger with no companion found (defensive; {!rg110_find_companion} returning [None]) simply passes through unchanged, and a member that is not a trigger at all ([rg110_companion_slug] returns [None]) is never touched. *) List.concat_map (fun ((c, _) as pair) -> match rg110_companion_slug (Slug.to_string c.Precedence.cel.Celebration.slug) with | Some companion_slug when not (already_has companion_slug) -> ( match rg110_find_companion comms companion_slug with | Some companion -> [ pair; companion ] | None -> [ pair ]) | _ -> [ pair ]) normal let admit ~(observed : Vocab_ef.rank Precedence.candidate) ~(temporal : Vocab_ef.rank Precedence.candidate) (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege * int) list) : (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list = (* Sorted once, by {!band} then slug (see [compare_precedence]); every branch below either takes a prefix of this list or filters it, so the RESULT is always built from [comms]'s own elements, untouched -- never rebuilt -- which matters beyond determinism: {!Precedence.resolve}'s own [dropped] computation tells an admitted candidate from a dropped one by physical equality (==) on the candidate value (Task 2's own deferred note: "assuming admit returns the same candidate values rather than rebuilt ones; undocumented for rite authors" -- documented here, now that this is the function that note was about). Building a fresh [{ c with ... }] record anywhere below would silently defeat that accounting: the original would then match nothing in [admitted], so the celebration would surface TWICE in the same day -- once in [commemorations] (the rebuilt copy) and once in [omitted] (the original, which nothing admitted matches). One admission, double-reported, and no crash to announce it, which is exactly why this comment exists. [drop_band] only unwraps the pair back out of the triple -- it does not rebuild [c] or [p] themselves, so this obligation still holds. *) let sorted = List.stable_sort compare_precedence comms in let is_privileged (_, p, _) = p = Precedence.Privileged in let observed_rank = observed.Precedence.cel.Celebration.rank in (* CORRECTED (fix round 1, RG16(a) task): read off [temporal], not [observed] -- see this function's own doc comment above for the full argument and the oracle evidence. [temporal] is the day's own temporal-cycle candidate regardless of who is [observed]; RG 16(a) makes that distinction live for the first time (a Feast of the Lord can now be [observed] on a day whose [temporal] candidate is a Sunday). *) let day_is_sunday = is_sunday_slug (Slug.to_string temporal.Precedence.cel.Celebration.slug) in let open Vocab_ef in (* RG 110's own uncapped additions are layered on AFTER this match -- {!rg110_additions}'s own top comment -- never inside any one branch: the three trigger slugs can each reach [observed] via any rank/Sunday combination the table admits (a Class2 fixed feast, here, but nothing in {!rg110_additions} itself assumes that), so computing it once, uniformly, over whatever this match already decided is both simpler and safer than duplicating the same lookup into every branch. *) let normal = match (observed_rank, day_is_sunday) with | Class1, _ -> (* RG 111: "I class: none save one privileged." Ordinary commemorations never get a slot at all on a I-class day, no matter how many are due; at most one privileged one does, the highest-precedence one (RG 113: {!band}'s own table order) if several are. *) (match List.filter is_privileged sorted with [] -> [] | best :: _ -> [ drop_band best ]) | Class2, true -> (* RG 111(b), primary text, RE-VERIFIED word for word against the scan (final fix wave; this sentence is the sole textual basis for the shipped rank-floor fix below, and the register's own §4 "RG 111" entry previously carried only the fragment "de festo II classis", not the full sentence -- now added there too): "in dominicis II classis, una tantum admittitur commemoratio, SCILICET DE FESTO II CLASSIS, quæ tamen omittitur si commemoratio privilegiata facienda sit" -- "on Sundays of the II class, only ONE commemoration is admitted, NAMELY OF A FEAST OF THE II CLASS, which however is dropped if a privileged commemoration is due." Two clauses, not one: (i) a privileged commemoration, whenever due, categorically takes the day's one slot -- not by comparing its precedence against the ordinary contender's, so an ordinary commemoration that would otherwise win on raw table order is still dropped once any privileged one is also due (the asymmetric clause the brief and task report flag as deliberate, not present at "other II class" below); (ii) failing that, the slot is reserved SPECIFICALLY for a [Class2] candidate -- "de festo II classis" is a RANK restriction, not merely "whichever ordinary candidate has the best table position": a III- or IV-class ordinary loser (a plain commemoration-only saint with no privilege of its own) has NO standing for this slot at all and must be entirely omitted, even when it is the only candidate present. Fix, Task 16 (primary-source-verified + missalemeum-confirmed): previously this fell back to "the best of [sorted], whatever its rank" once no privileged candidate was due, silently admitting a III/IV-class ordinary saint that RG 111(b)'s own wording excludes. Confirmed wrong for real data by the oracle comparison: e.g. 11 Jan 2026 (Holy Family, a II-class Sunday) has St Hyginus (Class3, commemoration-only) as its only competing candidate -- missalemeum shows him "displaced" (omitted), never commemorated; the pre-fix code admitted him regardless. CORROBORATED (ef-major-litanies task, fix round 1, F5): RG 111 sits in Caput XVI, "De Commemorationibus", governing "tam pro Missa quam pro Officio" per RG 106 -- read alone it could be mistaken for Office-shaped, incidentally reused for the Mass. Rubricae Generales Missalis Romani n. 434(b) (Part "VIII - De diversis Missae partibus", "D) De orationibus", "I - De orationibus in genere"), verified word for word, all three documents: "in dominicis II classis, nulla alia admittitur oratio, praeter commemorationem festi II classis, quae tamen omittitur si commemoratio privilegiata facienda sit" -- IDENTICAL IN ITS OPERATIVE CLAUSE to the one above -- not word for word throughout: RG 111(b) opens "una tantum admittitur commemoratio, SCILICET DE FESTO II CLASSIS", n. 434(b) recasts that into the orations register as "NULLA ALIA ADMITTITUR ORATIO, PRAETER COMMEMORATIONEM festi II classis", and only the trailing "quae tamen omittitur si commemoratio privilegiata facienda sit" is verbatim in both. That trailing clause is the one this adjudication turns on. And n. 434 is not merely a different part of the same document: the running heads show RG 111 under "Rubricae generales" and n. 434 under "Rubricae generales Missalis Romani" -- two distinct rubrical corpora bound in one volume, which is the whole force of the corroboration. Explicitly scoped "post orationem Missae" -- an independent, Mass-structure-rubric confirmation of the exact same privilege-overrides-ordinary rule this branch already implements, from a different part of the same document. First load-bearing live witness for this branch's "privileged wins even against a WORSE table position" half: the Major Litanies (RG 109(f), major_litanies_slug above), whose own [band] value is {!unclassified} (worse than every real table entry, per that constant's own comment) yet still displaces St Mark's real entry-16 table position on the four Sunday-conflict years the Litanies' own data/ef/expected-divergences-missalemeum .sexp M20 entry adjudicates -- every earlier witness for this branch (Holy Family/Hyginus above; the RG16(a) admit_cases table) happened to also be the day's own privileged Sunday/feast winning OUTRIGHT (band already decided it before admit ran), not two independently-offered LOSING candidates settled by this clause alone. *) (match List.filter is_privileged sorted with | best :: _ -> [ drop_band best ] | [] -> ( match List.filter (fun (c, _, _) -> c.Precedence.cel.Celebration.rank = Class2) sorted with | [] -> [] | best :: _ -> [ drop_band best ])) | Class2, false -> (* RG 111: "other II class: one" -- no privilege-override clause here, unlike the Sunday case immediately above, so the day's one slot goes to whichever candidate outranks the rest by RG 113's own table-of-precedence order ({!band}), privileged or not. *) (match sorted with [] -> [] | best :: _ -> [ drop_band best ]) | (Class3 | Class4), _ -> (* RG 111: "III-IV class: at most two" -- by RG 113's table order, same as the non-Sunday II-class case, just with room for two. *) List.map drop_band (take 2 sorted) in rg110_additions comms ~observed normal (* Task 11: RG 96 -- where an impeded I-class feast lands (docs/research/ rules-register.md §4, "Transfer/translation"). [band] decides who is impeded; [disposition] decides that an impeded I-class FEAST (not a Sunday, not omitted by RG 33) is [Transfer]-disposed; this is the third and final question RG 96 poses -- WHERE the translation lands -- and is {!Rite.t.transfer_target} itself, called by {!Calendar}'s placement pass once per deferred candidate, never re-run once a target is accepted (calendar.ml's own comment on [~start ~stop]). RG 96's own text, register-transcribed: "the next following day that is not I or II class." [is_blocking] reads that off [Vocab_ef.rank] -- RG 96 speaks of the day's CLASS (RG 8's four-way "dignity"), not [band]'s finer 28-entry occurrence-table row -- unlike {!admit} above, which (RG 113, Task B/ef-rg16a) now DOES use [band] itself for its own selection order; RG 96's own text has no such finer-table reading, so [is_blocking] stays on [Vocab_ef.rank] alone. *) let is_blocking (rank : Vocab_ef.rank) = rank = Vocab_ef.Class1 || rank = Vocab_ef.Class2 (* RG 96's own named exception (docs/research/rules-register.md §4, "Transfer/translation", RG 96 Attamen (a) -- primary-source-verified 2026-08-12, corrected from an earlier unconditional transcription; see the register's own correction note). Verbatim: "festum Annuntiationis B. Mariae Virg., quando est transferendum post Pascha, transfertur, tamquam in sedem propriam, in feriam II post dominicam in albis" -- when [the feast] is to be transferred PAST EASTER, [it] is transferred, as to its own proper seat, to the Monday after Low Sunday. The exception is CONDITIONAL on that "past Easter" clause -- {!transfer_target} tests it by comparing the GENERAL RG 96 target against Easter itself, not by testing the date here. Identified by slug -- the same convention this file already uses to pick out one specific celebration from a rank/ status shape shared by many others ({!nativity_octave_prefix}, [is_ember_18]'s date anchors) -- not an RG citation itself: RG 96 does not encode how a computer recognises "the Annunciation", only what happens to it once recognised. data/ef/sanctoral.sexp's own bootstrapped slug (Task 10), reused verbatim rather than guessed. *) let annunciation_slug = "annunciation-of-the-blessed-virgin-mary" (* Not an RG citation -- a defensive engineering ceiling, the same role Calendar's own [max_transfer_rounds] plays for the OUTER round loop (calendar.ml). That guard bounds how many ROUNDS the whole-year placement pass takes; it does nothing for the walk a single call to this function makes internally, which is this module's own responsibility (rite.mli documents the obligation this constant exists to satisfy). Comfortably longer than the longest real run of consecutive I/II-class days the 1962 calendar produces -- 24 Dec to 1 Jan (the Nativity vigil through the Circumcision, both I class, with the intervening octave days II class) is 9 days; Easter through Low Sunday (the Easter octave, I class, entry 10) is 8 -- RG 91 entry 28's own unqualified IV-class catch-all guarantees a non-blocking feria follows any such run in real data. Not tuned to that bound any more than 64 is tuned to RG 97-98's real collision count: a ceiling nothing in the 1962 calendar comes close to, so a rite/data shape this module has not anticipated fails FINITELY (see [search_from]) rather than hanging the CLI. *) let max_search_days = 400 (* The domain's own ceiling ({!Date.make}'s documented 1583..9999 bound, also duplicated by calendar.ml's own [domain_max_date] for the same reason: neither module exposes it to the other, and this is a three-line constant, not worth a new signature just to share it). [search_from] below must never call [occupant] on a date past this: [occupant] chains through the rite's own [temporal] (calendar.ml's [resolve_with_injected]), which for the real EF rite calls [Computus.gregorian_easter], which is NOT total outside 1583..9999 -- it constructs a [Date.t] via [Date.make] and [failwith]s on [Error]. [Date.add_days] itself has no such limit (it is documented "unbounded total arithmetic"), so [search_from] CAN walk [d] past 31 December 9999 without raising by itself -- the raise would only happen on the NEXT [occupant d] call, which is exactly the bug this guards against: an I-class feast impeded late enough in civil year 9999 that every remaining day of the year is also I or II class (reachable through the project's own overlay mechanism, confirmed by review: an Add-ed I-class feast on 25 December leaves only Class2 Nativity-octave days for the rest of 9999, so the unguarded walk reached 1 January 10000 and crashed there). *) let domain_max_date = match Date.make ~year:9999 ~month:12 ~day:31 with Ok d -> d | Error e -> failwith e (* Walks forward from [d], returning the first date [occupant] reports as NOT [is_blocking]. [steps] is a strictly increasing structural bound on the recursion, capped at [max_search_days]: the function decreases [max_search_days - steps] by exactly one on every call and returns as soon as that reaches zero (whether or not an admissible day was ever found), so THIS loop terminates by construction, regardless of what [occupant] reports -- it does not rely on the real EF calendar's own structure to guarantee termination the way the comment above explains why the bound is never actually reached in practice. Also stops, without calling [occupant] again, once [d] passes {!domain_max_date} -- see that constant's own comment for why probing [occupant] beyond it can raise. Either way the last date visited is returned WITHOUT a further [occupant] probe -- one more finite (not necessarily admissible) date, not a further search -- because the value the caller ([transfer_target]) is still owed is "a date", never an exception; {!Calendar}'s own [~start ~stop] bound (calendar.ml's [place_transfers]) is what turns an implausible non-terminating real search into a recorded [omitted], not this function pretending to have found something admissible. *) let rec search_from (occupant : Date.t -> Vocab_ef.rank Celebration.t) (steps : int) (d : Date.t) : Date.t = if steps >= max_search_days || Date.compare d domain_max_date > 0 then d else if is_blocking (occupant d).Celebration.rank then search_from occupant (steps + 1) (Date.add_days d 1) else d (* [transfer_target]'s contract (rite.mli): total, terminating, and its result is always strictly after [origin]. Terminating: [search_from]'s own structural bound, above. Strictly after [origin]: the general branch is exactly [search_from]'s own result starting at [Date.add_days origin 1], which only ever advances forward from there, so it is always >= origin + 1. The Annunciation branch, when it fires, instead searches from the Monday after Low Sunday for [origin]'s own civil year -- NOT provably later than [origin] by the code alone, but true of every representable year: the Annunciation's [origin] is always 25 March (Date_spec.Fixed in data/ef/sanctoral.sexp), Easter always falls within that SAME civil year in [22 March, 25 April] (Computus's own documented range, register §0), so Low Sunday (Easter + 7) falls in [29 March, 2 May] and the Monday after it in [30 March, 3 May] -- always after 25 March. RG 96 Attamen (a) (see {!annunciation_slug}'s own comment) makes the Annunciation exception CONDITIONAL on the general walk carrying the feast past Easter -- so the general target is always computed FIRST, for every candidate, and only overridden for the Annunciation when that target itself falls after Easter Sunday. A version of this function that tested the DATE of [origin] instead (e.g. "is 25 March within some fixed window of Easter") would be re-deriving the register's own "quando est transferendum post Pascha" condition from first principles, exactly the kind of guess this project's "a wrong citation is worse than a missing one" rule warns against; comparing the general target against Easter directly tests the rubric's own words. *) let transfer_target (c : Vocab_ef.rank Precedence.candidate) (origin : Date.t) (occupant : Date.t -> Vocab_ef.rank Celebration.t) : Date.t = let easter = Computus.gregorian_easter (Date.year origin) in if is_major_litanies c then (* RG 80 -- {!major_litanies_slug}'s own citation (precedence_ef.ml, above [privilege_of]) has the full text and the "both trigger shapes land on Easter+2" derivation. Checked BEFORE computing [general_target] at all (unlike the Annunciation branch below, which computes the general RG 96 walk first and only overrides it conditionally) -- this candidate's target is never the general walk's own result, so running {!search_from} for it would be wasted work at best. "The following Tuesday", unconditionally: [search_from]'s ordinary forward walk exists because a DISPLACED FEAST needs an unoccupied (non-I/II-class) day to be fully celebrated as itself (RG 96's own "next day that is not I or II class"). The Major Litanies are never a feast -- RG 81: "nihil fit in Officio, sed tantum in Missa" -- only ever a commemoration once they arrive, and a commemoration needs no unoccupied day at all (RG 108-111 admit commemorations on I-class days routinely, see [privilege_of]'s own (b) branch and [admit]'s [Class1] case above). Running {!search_from} here would therefore be actively WRONG, not merely unnecessary: Easter+2 is itself I-class (within the Easter octave, {!band}'s own entry-10 branch, {!is_blocking}'s own [Class1] test), so a search starting there would walk PAST the exact date RG 80 names, looking for a day the rubric never asked for. Total and terminating trivially (no search performed at all). Strictly after [origin], {!Rite.t.transfer_target}'s own contract (rite.mli), on both trigger shapes (this file's header, worked examples): 25 April = Easter Sunday (origin = Easter itself = Easter+0, target = Easter+2 = origin+2) or 25 April = Easter Monday (Easter itself = 24 April = origin-1, target = Easter+2 = origin+1) -- either way strictly forward. Not conditioned on [occupant] at all, unlike every other branch in this function -- deliberately: nothing about RG 80's own text makes the target depend on what else is observed that year, only on Easter's own date. *) Date.add_days easter 2 else let general_target = search_from occupant 0 (Date.add_days origin 1) in let is_annunciation = Slug.to_string c.Precedence.cel.Celebration.slug = annunciation_slug in if is_annunciation && Date.compare general_target easter > 0 then (* Low Sunday = Easter + 7 (register §0, temporal_ef.ml's [off 7]); the Monday after it = Easter + 8. Searched onward from there exactly like the general case searches from [origin + 1] -- "only if that day is itself blocked" (rite.mli) is [search_from]'s ordinary behaviour, not a second mechanism. *) search_from occupant 0 (Date.add_days easter 8) else general_target