aboutsummaryrefslogtreecommitdiff
path: root/lib/kernel
diff options
context:
space:
mode:
Diffstat (limited to 'lib/kernel')
-rw-r--r--lib/kernel/celebration.mli13
-rw-r--r--lib/kernel/precedence.ml22
-rw-r--r--lib/kernel/precedence.mli31
-rw-r--r--lib/kernel/validate.ml90
-rw-r--r--lib/kernel/validate.mli26
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