diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/kernel/celebration.mli | 13 | ||||
| -rw-r--r-- | lib/kernel/precedence.ml | 22 | ||||
| -rw-r--r-- | lib/kernel/precedence.mli | 31 | ||||
| -rw-r--r-- | lib/kernel/validate.ml | 90 | ||||
| -rw-r--r-- | lib/kernel/validate.mli | 26 | ||||
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.ml | 333 | ||||
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.mli | 121 |
7 files changed, 516 insertions, 120 deletions
diff --git a/lib/kernel/celebration.mli b/lib/kernel/celebration.mli index a3c2960..85f7af4 100644 --- a/lib/kernel/celebration.mli +++ b/lib/kernel/celebration.mli @@ -1,7 +1,14 @@ (** Whether this celebration can be the observed day at all. The 1960 reform - reduced many feasts to a bare commemoration; they retain a rank (RG 111 - orders admitted commemorations by dignity) but can never be observed. NOT - a fifth rank: RG 8 fixes the classes at four. *) + reduced many feasts to a bare commemoration; they retain a rank but can + never be observed. NOT a fifth rank: RG 8 fixes the classes at four. + + What the retained rank is FOR (corrected 2026-08-12): this line formerly + read "RG 111 orders admitted commemorations by dignity". Ordering is no + longer done by rank at all -- it moved to the rite's own table of + precedence (RG 113) in commit ea22ad2, and a bare commemoration has no + row in that table. The rank remains load-bearing for ADMISSION: RG 111(b) + reserves a II-class Sunday's single slot "de festo II classis", a floor + read off this rank directly. *) type status = Feast | Commemoration_only [@@deriving sexp] (** A celebration. Parameterised by the rite's rank type only. *) diff --git a/lib/kernel/precedence.ml b/lib/kernel/precedence.ml index 05ad69f..2c55817 100644 --- a/lib/kernel/precedence.ml +++ b/lib/kernel/precedence.ml @@ -17,8 +17,14 @@ type ('s, 'r) rules = { disposition : winner:'r candidate -> loser:'r candidate -> disposition; admit : observed:'r candidate -> - ('r candidate * privilege) list -> + temporal:'r candidate -> + ('r candidate * privilege * int) list -> ('r candidate * privilege) list; + (** the trailing [int] on each input triple is that candidate's own + [band] value, computed once by {!resolve} below (a rite's [admit] + has no [context] of its own to compute it with) -- see [resolve]'s + own comment for why this is a KERNEL-level policy, not a rite- + specific rule threaded in as data. *) } type 'r resolution = { @@ -55,7 +61,19 @@ let resolve rules ctx ~temporal ~sanctoral = ([], [], []) losers in let comms = List.rev comms and deferred = List.rev deferred in - let admitted = rules.admit ~observed comms in + (* RG 113 (EF; docs/research/rules-register.md §4 "Commemorations"): "in + admittendis et ordinandis aliis commemorationibus, servetur ordo + tabellae praecedentiae" -- admitting AND ordering commemorations both + run on the rite's own table of precedence, the same [band] already + used above to pick [observed]. Computed here, once, generically (a + rite's own [admit] has no [ctx] of its own to call [band] with) rather + than inside every rite's [admit] separately -- a kernel-level POLICY + ("commemorations are ordered by the rite's own band"), not a + rite-specific RULE baked into the kernel: the actual [band] function, + and whether a rite's [admit] even uses the value it is handed, both + stay entirely rite-supplied. *) + let comms_by_precedence = List.map (fun (c, p) -> (c, p, rules.band ctx c)) comms in + let admitted = rules.admit ~observed ~temporal comms_by_precedence in let dropped = List.filter (fun c -> not (List.exists (fun a -> fst a == fst c) admitted)) comms in diff --git a/lib/kernel/precedence.mli b/lib/kernel/precedence.mli index ae054dd..09376d7 100644 --- a/lib/kernel/precedence.mli +++ b/lib/kernel/precedence.mli @@ -34,12 +34,41 @@ type ('s, 'r) rules = { (** RG 92-95: the loser's fate, which depends on the loser's own rank. *) admit : observed:'r candidate -> - ('r candidate * privilege) list -> + temporal:'r candidate -> + ('r candidate * privilege * int) list -> ('r candidate * privilege) list; (** RG 108-111: how many commemorations are admitted, and in what order; anything filtered out here is recorded in {!resolution.omitted}, not dropped. + Each input triple's trailing [int] is that candidate's own {!band} + value, computed once by {!resolve} (RG 113: "in admittendis et + ordinandis aliis commemorationibus, servetur ordo tabellae + praecedentiae" -- ADMITTING and ORDERING commemorations is governed + by the same table-of-precedence order {!band} already supplies for + picking the day's own winner; docs/research/rules-register.md §4 + "Commemorations"). Supplied rather than left for [admit] to compute + itself because [admit] has no [context] (date/season/weekday) of + its own -- {!resolve} already holds one and calls {!band} with it + for every candidate regardless. A rite's [admit] is free to ignore + the value entirely (e.g. fall back to [Vocab.rank] alone), the same + as it may ignore [temporal] below. + + [temporal] is {!resolve}'s own [~temporal] argument, passed through + unchanged -- the day's temporal-cycle candidate, regardless of + whether it won. Fix round 1 (RG16(a) task): before this, a rite's + [admit] could only infer properties of the CIVIL DAY (chiefly "is + this a Sunday", RG 111(b)'s own two-tier admission rule) from + [observed]'s own fields -- a proxy that breaks the moment something + OTHER than the day's own temporal candidate can be [observed], the + exact shape RG 16(a) introduces (a Feast of the Lord standing in + the impeded Sunday's place "cum omnibus iuribus et privilegiis", + RG 91 entry 14). This is NOT a kernel definition of "Sunday" -- + the kernel does not gain any rite-specific knowledge by this + parameter, it only threads through a value {!resolve} already + holds; a rite's own [admit] is free to ignore it entirely, the + same as [observed]. + OBLIGATION ON THE IMPLEMENTATION, not enforced by this type: every candidate this function returns must be a value taken UNCHANGED from its input list, never rebuilt (e.g. via a [{ c with ... }] diff --git a/lib/kernel/validate.ml b/lib/kernel/validate.ml index cc8bdce..ffeb89e 100644 --- a/lib/kernel/validate.ml +++ b/lib/kernel/validate.ml @@ -293,26 +293,96 @@ let run (rite : ('s, 'r) Rite.t) (layer : 'r Layer.t) ~year = "transfer placement did not reach a fixed point within the round guard (RG 96-98)"; (* "admission": re-offer this day's own admitted commemorations back to [rite.rules.admit] and require the exact same set back. - [origin] is reconstructed as [Sanctoral] uniformly: - {!Liturgical_day.t} does not retain a commemoration's original - origin, and the real EF [admit] (precedence_ef.ml) reads only - rank and slug from a candidate, never [origin], so this - reconstruction is exact for it; documented in validate.mli as - the one place a rite whose [admit] DOES consult [origin] could - see a false negative from this check. *) + [origin] is RECOVERED, not fabricated -- see the fuller note + below on [as_candidates]. It was formerly reconstructed as + [Sanctoral] uniformly, justified by the claim that the real EF + [admit] reads only rank and slug and never [origin]. That claim + is now FALSE: since ea22ad2 the EF [admit] orders by [band] + (RG 113), and [band] does read [origin] via [is_temporal], so a + temporal-origin commemoration relabelled [Sanctoral] would be + scored on the wrong table entry. The recovery below is exact, + not a heuristic: [resolve] builds exactly one temporal + candidate per day, so a slug match against the day's own + temporal office identifies it unambiguously. *) let observed_candidate : 'r Precedence.candidate = { Precedence.cel = d.Liturgical_day.observed; origin = Precedence.Sanctoral } in + (* [~temporal] (fix round 1, RG16(a) task): {!Precedence.rules.admit} + now also takes the day's own temporal-cycle candidate, reused + here from {!Liturgical_day.t}'s own embedded [Temporal.t] -- + the exact same value {!Calendar} passed to {!Precedence.resolve} + in the first place, so re-offering is against the SAME inputs, + not a reconstruction that could itself introduce a false + negative. [origin] is [Precedence.Temporal], genuinely (this IS + the temporal candidate, not a reconstructed sanctoral one). *) + let temporal_candidate : 'r Precedence.candidate = + { Precedence.cel = d.Liturgical_day.temporal.Temporal.office; + origin = Precedence.Temporal } + in + (* [band]'s own [context] (RG 113, docs/research/rules-register.md + §4 "Commemorations" -- {!Precedence.rules.admit}'s own new + [int] parameter, added alongside RG 113's fix: [admit] now + orders/selects by the rite's table-of-precedence value + {!resolve} attaches to each candidate, not by [Vocab.rank] + alone). Reconstructed from {!Liturgical_day.t}'s own embedded + [Temporal.t], the same source [temporal_candidate] above + already draws its [cel] from, so this is the exact [ctx] + {!Calendar} passed to {!Precedence.resolve} for this date in + the first place, not a re-derivation that could itself drift. *) + let day_ctx : 's Precedence.context = + { Precedence.date; + season = d.Liturgical_day.temporal.Temporal.season; + weekday = d.Liturgical_day.temporal.Temporal.weekday } + in + (* CORRECTED (Task B fix round 1, coordinator finding 5): [origin] + used to be reconstructed as [Sanctoral] UNCONDITIONALLY, which + was harmless while nothing here called [band] on the result + (the comment this replaces was correct about [PE.admit] itself: + it never reads [origin]) -- but this function ALSO now calls + [rite.rules.Precedence.band day_ctx cand] on every one of these + reconstructed candidates (the line just below), and [band] DOES + read [origin] (its own [is_temporal] test) to choose between its + temporal- and sanctoral-keyed branches. A genuinely + TEMPORAL-origin commemoration (a privileged Advent/Lent/ + Passiontide feria, an Ember day, an impeded Sunday) mislabelled + [Sanctoral] would score the WRONG band entry (e.g. a Lent feria + scoring entry 23, "III-class feasts in particular calendars", + instead of its real entry 22) -- inert today only because + nothing currently asserts on the SCORE [band] returns here, only + on whether re-offering [admit] the same values round-trips + (which happens not to depend on getting [origin] right for any + case this codebase's data reaches -- unverified in general). + [Liturgical_day.t] itself still does not retain a + commemoration's original origin, so it is recovered the only + way available: a commemoration whose SLUG matches the day's own + temporal office is temporal-origin; every other commemoration + is sanctoral-origin. This is exact whenever slugs cannot + collide across the two streams (Task 12's own "observed" check + already assumes this for a different purpose), which is the + same assumption the rest of this codebase already leans on. *) + let temporal_office_slug = d.Liturgical_day.temporal.Temporal.office.Celebration.slug in let as_candidates comms = - List.map (fun (c, p) -> ({ Precedence.cel = c; origin = Precedence.Sanctoral }, p)) comms + List.map + (fun (c, p) -> + let origin = + if Slug.equal c.Celebration.slug temporal_office_slug then Precedence.Temporal + else Precedence.Sanctoral + in + let cand : 'r Precedence.candidate = { Precedence.cel = c; origin } in + (cand, p, rite.Rite.rules.Precedence.band day_ctx cand)) + comms in let offered = as_candidates d.Liturgical_day.commemorations in - let readmitted = rite.Rite.rules.Precedence.admit ~observed:observed_candidate offered in + let readmitted = + rite.Rite.rules.Precedence.admit ~observed:observed_candidate + ~temporal:temporal_candidate offered + in let norm l = List.map (fun (c, p) -> (Slug.to_string c.Precedence.cel.Celebration.slug, p)) l |> List.sort compare in - if norm readmitted <> norm offered then + let offered_pairs = List.map (fun (c, p, _) -> (c, p)) offered in + if norm readmitted <> norm offered_pairs then fail date "admission" (Printf.sprintf "admit is not a fixed point on this day's own commemorations: re-offering %d \ diff --git a/lib/kernel/validate.mli b/lib/kernel/validate.mli index 5e55fc5..b46ca30 100644 --- a/lib/kernel/validate.mli +++ b/lib/kernel/validate.mli @@ -59,14 +59,24 @@ val failure_to_string : failure -> string fixed point. - ["admission"]: the rite's own [rules.admit] is a fixed point on what it already admitted -- re-offering a day's [commemorations] back to - [admit] (reconstructed with {!Precedence.Sanctoral} origin; the real EF - admit reads only rank and slug, never origin, so this reconstruction is - exact for it) must return exactly that same set. A cap-enforcing - selector that is not idempotent on its own output has, by definition, - admitted something its own rule would not admit if asked again -- the - rite-agnostic form of "the admission limit was not exceeded" available - without embedding a rite's specific numeric caps (RG 111's, for EF) - into kernel code. + [admit] (with [observed]'s own origin reconstructed as + {!Precedence.Sanctoral}; the real EF admit reads only rank and slug + from [observed], never origin, so this reconstruction is exact for + it) must return exactly that same set. Each offered commemoration's + own origin -- CORRECTED, Task B fix round 1 -- is recovered by + comparing its slug against the day's own temporal office, not + reconstructed as [Sanctoral] uniformly: {!Precedence.rules.admit} + (since Task B) is handed each candidate's own {!Precedence.rules.band} + value, computed here exactly as {!Precedence.resolve} computes it, and + [band] DOES read a candidate's origin (temporal- vs sanctoral-keyed + branches) even though EF's own [admit] itself still does not -- a + mislabelled origin would silently score the wrong table entry for a + genuinely temporal-origin commemoration (e.g. a Lent feria) before + this fix. A cap-enforcing selector that is not idempotent on its own + output has, by definition, admitted something its own rule would not + admit if asked again -- the rite-agnostic form of "the admission limit + was not exceeded" available without embedding a rite's specific + numeric caps (RG 111's, for EF) into kernel code. Total over the whole 1583..9999 domain, including [year] = 9999: the liturgical year opening there continues into out-of-domain civil year diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index db7e708..750da8b 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -110,6 +110,7 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc int = let cel = c.Precedence.cel in let rank = cel.Celebration.rank in + let status = cel.Celebration.status in let subject = cel.Celebration.subject in let layer = cel.Celebration.layer in let slug = Slug.to_string cel.Celebration.slug in @@ -129,8 +130,48 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc binding, not a second literal, so the two can never drift apart. *) let entry_15_band = 15 in let open Vocab_ef in - (* 1: Nativity, Easter Sunday, Pentecost Sunday (I class w/ octave). *) - if is_temporal && rank = Class1 && ((m = 12 && d = 25) || off = 0 || off = 49) then 1 + (* CORRECTED (Task B fix round 1, ef-rg16a): a [Commemoration_only] + celebration has NO row in RG 91's table at all, checked FIRST, ahead of + every rank-keyed branch below -- RG 91's own text enumerates only "dies + liturgici" (entry 24's own wording, e.g., "Festa III classis, in + calendario Ecclesiae universae inscripta" -- FEASTS, inscribed in the + calendar), and the calendarium itself marks the difference in its own + notation: 22 September's row reads "S. Thomae de Villanova Ep. et + Conf., III classis. / Commemoratio Ss. Mauritii et Soc. Mm." -- Thomas + gets a class number (a "festum"); Maurice gets "Commemoratio" and NO + class number at all, because he never had a row in the table for that + occasion to begin with. Before this fix, [band] read [rank] alone, so + a [Commemoration_only] entry silently borrowed the SAME entry number + as a genuine [Feast] of its own rank (a Class3 Commemoration_only + entry banded to 24, indistinguishable from a real Class3 universal + feast) -- manufacturing a "tie" at {!compare_precedence}/RG 113's own + admission ordering that the primary text never creates: RG 113 does + not run out of instruction between two same-rank commemorations; + [band] ran out of fidelity, handing out a table row that does not + exist for one of them. [unclassified] (worse than every real entry) is + the same value already used for a candidate this table's 28 branches + otherwise fail to describe, which is exactly the right answer here + too: "not in this table" for a different reason, same table-position + consequence. Confirmed by the oracle: missalemeum's own commemoration + id embeds a rank that AGREES with the demoted status this represents + (e.g. sancti:09-22o:4:r for Maurice, rank 4, against Thomas's own + sancti:09-22:3:w, rank 3, on the day Thomas is observed -- test_oracle + .ml's own [test_identity_rank_corroboration]). Restricted to [status]; + [rank] itself is untouched, 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 1 (* 2: Sacred Triduum (Thu-Sat of Holy Week). *) else if is_temporal && rank = Class1 && off >= -3 && off <= -1 then 2 (* 3: Epiphany, Ascension, Holy Trinity, Corpus Christi, Sacred Heart, @@ -244,13 +285,14 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc (* 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). [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, which also has 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. *) + 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 @@ -605,6 +647,91 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate) 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 (* RG 95's other branch: "aut commemorantur aut penitus omittuntur" -- commemorated or wholly omitted. Reached by every SANCTORAL loser @@ -642,42 +769,105 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate) 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]). No [context] - (date/season/weekday) is available to [admit] (see precedence.mli's - [rules.admit]) or needed: [observed] IS the day's own celebration, so its - rank and slug already carry everything RG 111's own four categories test. *) + 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 8's four-class dignity order, Class1 highest. Deliberately NOT [band] - (RG 91's much finer 28-entry table): [band] needs a [context] [admit] - does not have (see above), and Celebration.mli's own comment on [status] - -- "RG 111 orders admitted commemorations by dignity" -- names [rank] - itself as that dignity, not the finer occurrence-table entry. *) -let dignity = function - | Vocab_ef.Class1 -> 1 - | Vocab_ef.Class2 -> 2 - | Vocab_ef.Class3 -> 3 - | Vocab_ef.Class4 -> 4 +(* 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. -(* Deterministic selection order for RG 111: dignity first, then slug -- - the same tie-break {!Precedence.compare_by} uses for [band] itself (the - brief: "break ties on slug"), so which candidate wins a shared rank never - depends on the order [comms] arrives in. *) -let compare_dignity (a, _) (b, _) = - let da = dignity a.Precedence.cel.Celebration.rank - and db = dignity b.Precedence.cel.Celebration.rank in - if da <> db then Int.compare da db + 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, + all order/count-invisible -- RG 91 has no table position for either of + them to compare, so there is nothing more specific RG 113 could supply + here either). {!band} needs a [context] this function itself does not + have (date/season/weekday) -- unlike [dignity], which read [rank] alone + -- so {!Precedence.resolve} now computes each candidate's own [band] + value once, generically, and hands it to [admit] as the trailing [int] + on each input triple (see {!Precedence.rules.admit}'s own doc). [comms] + below is [(candidate * privilege * int) list], not the pair it used to + be. *) +let compare_precedence (a, _, ba) (b, _, bb) = + if ba <> bb then Int.compare (ba : int) bb else Slug.compare a.Precedence.cel.Celebration.slug b.Precedence.cel.Celebration.slug 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) + let admit ~(observed : Vocab_ef.rank Precedence.candidate) - (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list) : + ~(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 dignity then slug (see [compare_dignity]); every branch - below either takes a prefix of this list or filters it, so the RESULT - is always a sub-list of [comms] with its elements untouched -- never + (* 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 @@ -689,21 +879,29 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) 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. *) - let sorted = List.stable_sort compare_dignity comms in - let is_privileged (_, p) = p = Precedence.Privileged in + 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 - let observed_is_sunday = - is_sunday_slug (Slug.to_string observed.Precedence.cel.Celebration.slug) + (* 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 - match (observed_rank, observed_is_sunday) with + 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-dignity one if - several are. *) - (match List.filter is_privileged sorted with [] -> [] | best :: _ -> [ best ]) + 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 @@ -717,17 +915,18 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) 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 dignity against the ordinary contender's, + -- not by comparing its precedence against the ordinary contender's, so an ordinary commemoration that would otherwise win on raw - dignity 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 dignity": 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. + 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 @@ -739,21 +938,21 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) shows him "displaced" (omitted), never commemorated; the pre-fix code admitted him regardless. *) (match List.filter is_privileged sorted with - | best :: _ -> [ best ] + | best :: _ -> [ drop_band best ] | [] -> ( - match List.filter (fun (c, _) -> c.Precedence.cel.Celebration.rank = Class2) sorted with + match List.filter (fun (c, _, _) -> c.Precedence.cel.Celebration.rank = Class2) sorted with | [] -> [] - | best :: _ -> [ best ])) + | 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 dignity alone, - privileged or not. *) - (match sorted with [] -> [] | best :: _ -> [ best ]) + 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 dignity, same as the - non-Sunday II-class case, just with room for two. *) - take 2 sorted + (* 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) (* Task 11: RG 96 -- where an impeded I-class feast lands (docs/research/ rules-register.md §4, "Transfer/translation"). [band] decides who is @@ -766,9 +965,11 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) 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, the same distinction {!admit} above - already draws for RG 111 ({!dignity}, not [band]). *) + 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, diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli index 55f947e..b283a17 100644 --- a/lib/rites/rite_ef/precedence_ef.mli +++ b/lib/rites/rite_ef/precedence_ef.mli @@ -68,10 +68,21 @@ val unclassified : int text states an exception: entry 8 (All Souls) reads "yields to an occurring Sunday", so on a Sunday this returns a value that loses to entry 15 rather than the literal integer 8 (see the comment on - entry 8 in precedence_ef.ml for the exact value and why). Total over - every candidate {!Precedence.resolve} or {!Calendar} can construct, - including shapes the 1962 table itself does not describe (see - {!unclassified}). *) + entry 8 in precedence_ef.ml for the exact value and why). ALSO + {!unclassified} for any [Cel.Commemoration_only] candidate, checked + first, ahead of every rank-keyed branch -- CORRECTED, Task B fix round + 1 (coordinator finding 1): RG 91's own table enumerates only "dies + liturgici" (entry 24's own text, "Festa III classis..." -- FEASTS), so + a celebration reduced to a bare commemoration was never a row in the + table to begin with, confirmed by the calendarium's own notation (a + genuine feast carries a class number on its occurrence line; a bare + "Commemoratio" carries none). Before this fix, a [Commemoration_only] + candidate silently borrowed the SAME entry number as a genuine [Feast] + of its own [rank] -- manufacturing ties {!Precedence.compare_by} and + {!compare_precedence} both then had to break some other way, that RG 91 + itself never created. Total over every candidate {!Precedence.resolve} + or {!Calendar} can construct, including shapes the 1962 table itself + does not describe (see {!unclassified}). *) val band : Vocab_ef.season Precedence.context -> Vocab_ef.rank Precedence.candidate -> int (** RG 33's marker: every Sunday slug this rite's temporal cycle produces @@ -164,33 +175,82 @@ val nativity_octave_prefix : string so the two cannot silently drift apart. *) val september_ember_prefix : string -(** [admit ~observed comms]: RG 108-111 (docs/research/rules-register.md §4, - "Commemorations"). How many of [comms] -- each already tagged with its - real RG 109 privilege by {!disposition} -- RG 111 admits, and which, - given the day actually observed: +(** [admit ~observed ~temporal comms]: RG 108-111 (docs/research/rules-register.md + §4, "Commemorations"). How many of [comms] -- each already tagged with + its real RG 109 privilege by {!disposition}, and its own {!band} value + (RG 91's table-of-precedence entry, computed once by + {!Precedence.resolve} -- see {!Precedence.rules.admit}'s own doc) -- RG + 111 admits, and which, given the day actually observed: - [observed] a [Class1] day: none, except at most one privileged - commemoration (the highest-dignity one, if several are due) -- an - ordinary one is never admitted here, no matter how many are due; - - [observed] a [Class2] Sunday (its slug carries {!sunday_marker}): one, - subject to TWO conditions, not one -- (i) a privileged commemoration, - whenever due, categorically takes the day's one slot over any - ordinary one, not by comparing dignity, so an ordinary commemoration - that would otherwise win on dignity is still dropped; (ii) failing - that, the slot is reserved for a [Class2] candidate SPECIFICALLY - ("de festo II classis", RG 111(b)'s own wording -- a RANK FLOOR, not - "whichever ordinary candidate has the best dignity"): a III- or - IV-class ordinary loser has no standing for this slot at all and is - admitted nothing, even when it is the only candidate due; - - [observed] any other [Class2] day: one, by dignity alone -- no - privilege override and no rank floor, unlike the Sunday case - immediately above; - - [observed] a [Class3] or [Class4] day: at most two, by dignity alone. + commemoration (the highest-{!band}-precedence one, if several are + due) -- an ordinary one is never admitted here, no matter how many + are due; + - the CIVIL DAY is a [Class2] Sunday ([temporal]'s slug carries + {!sunday_marker} -- CORRECTED, fix round 1, RG16(a) task: read off + [temporal], not [observed]; see below): one, subject to TWO + conditions, not one -- (i) a privileged commemoration, whenever due, + categorically takes the day's one slot over any ordinary one, not by + comparing table position, so an ordinary commemoration that would + otherwise win on table order is still dropped; (ii) failing that, the + slot is reserved for a [Class2] candidate SPECIFICALLY ("de festo II + classis", RG 111(b)'s own wording -- a RANK FLOOR, not "whichever + ordinary candidate has the best table position"): a III- or IV-class + ordinary loser has no standing for this slot at all and is admitted + nothing, even when it is the only candidate due; + - the civil day is any other [Class2] day: one, by {!band}'s table order + alone -- no privilege override and no rank floor, unlike the Sunday + case immediately above; + - [observed] a [Class3] or [Class4] day: at most two, by {!band}'s table + order alone. - "Dignity" here is [Vocab_ef.rank] (RG 8's four classes), NOT {!band}'s - finer RG 91 entry number -- {!band} needs a [context] (date/season/ - weekday) this function does not receive (see {!Precedence.rules.admit}). - Ties break on slug, matching {!Precedence.compare_by}, so the result - never depends on the order [comms] arrives in. + CORRECTED, Task B (branch ef-rg16a): this used to sort by + [Vocab_ef.rank] (RG 8's coarse four-class "dignity") and break same-rank + ties alphabetically by slug -- an engineering convention with no + rubrical warrant, cited nowhere. Measured, fix round 1 (2005-2050, + reproducible via a temporary [git worktree] at the pre-fix commit): + a same-[rank] tie existed in the candidate pool offered to [admit] on + 599 days, most never reaching a real decision (RG 111's own admission + count often has room for both, e.g. a III-class day admits two); of + those, reversing the slug tie-break ALONE actually changed the ADMITTED + SET on 65 days and only the printed ORDER of an already-admitted pair + on a further 149 (docs/research/rules-register.md §6.1's own full + account, superseding this comment's earlier, unreproduced "66 days" + claim). RG 113's own second sentence, previously uncited + (docs/research/rules-register.md §4's RG 113 entry), is the real rule: + *"in admittendis et ordinandis aliis commemorationibus, servetur ordo + tabellae praecedentiae"* -- admitting and ordering commemorations both + run on {!band}'s own finer RG 91 table, not RG 8's four classes. + {!band} needs a [context] (date/season/weekday) this function does not + receive on its own, so {!Precedence.resolve} computes it once, + generically, and hands each candidate its own value as the trailing + [int] on [comms]'s triples (see {!Precedence.rules.admit}). Ties break + on slug ONLY within one identical [band] value -- CORRECTED, fix round + 1 (coordinator finding 1): [band] itself used to hand a + [Cel.Commemoration_only] candidate the same table entry as a genuine + [Cel.Feast] of its own rank (RG 91's table has no row for a bare + commemoration at all), manufacturing most of the "identical [band] + value" ties this comment used to describe as RG 113's own residual. + Fixed in [band] itself; checked afterward (2005-2050 and the full + 1583-9999 domain, register §6.1) and found EMPTY of any remaining + "two different real table entries tied" case -- the slug fallback below + is exercised only between two [Commemoration_only] candidates both at + {!unclassified}, still an uncited engineering convention (register §6 + "RG 113 tie-break"), matching {!Precedence.compare_by}'s own fallback, + so the result never depends on the order [comms] arrives in. + + [temporal] -- CORRECTED, fix round 1 (RG16(a) task): this function used + to read the Sunday/non-Sunday split off [observed]'s own slug, which + silently assumed [observed] IS the day's temporal-cycle office. RG 16(a) + breaks that assumption on purpose: a Feast of the Lord holds an impeded + II-class Sunday's own place "cum omnibus iuribus et privilegiis" + (RG 16(a)'s own text), so the day still counts as a "dominica II + classis" for RG 111(b) even though [observed] is now the FEAST. + [temporal] -- {!Precedence.rules.admit}'s own parameter, the day's + temporal-cycle candidate regardless of who won -- is the reliable + signal; oracle-confirmed (6 August: Sunday excludes a Class3 saint + Sixtus II from the freed slot, an ordinary weekday admits him). Every + row of the previous behaviour where [observed] and [temporal] coincide + (no RG 16(a) displacement occurred) is unaffected. Every candidate this returns is a value taken unchanged from [comms], never rebuilt: {!Precedence.resolve}'s own [dropped]/[omitted] @@ -202,7 +262,8 @@ val september_ember_prefix : string itself total over an empty or arbitrarily long [comms]. *) val admit : observed:Vocab_ef.rank Precedence.candidate -> - (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list -> + temporal:Vocab_ef.rank Precedence.candidate -> + (Vocab_ef.rank Precedence.candidate * Precedence.privilege * int) list -> (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list (** The Annunciation's own bootstrapped slug (data/ef/sanctoral.sexp, Task |
