diff options
Diffstat (limited to 'lib/kernel')
| -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 |
5 files changed, 158 insertions, 24 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 |
