aboutsummaryrefslogtreecommitdiff
path: root/lib/rites/rite_ef/precedence_ef.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-12 15:16:00 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-12 15:16:00 +0200
commitea22ad2bde211998e6719fd5fb76233571a48383 (patch)
tree43c5a7532d910bf8d8f51fa4965c745bc7d8e300 /lib/rites/rite_ef/precedence_ef.ml
parent81bb608ad60877df977af16f69f1ee0c99aa68f4 (diff)
downloadcolitur-ea22ad2bde211998e6719fd5fb76233571a48383.tar.gz
colitur-ea22ad2bde211998e6719fd5fb76233571a48383.zip
fix(kernel,rite-ef): admit orders commemorations by RG 113's table of precedence, not slug
Precedence_ef.admit broke a same-rank tie among commemoration candidates alphabetically by slug -- a deterministic engineering convention with no rubrical citation. RG 113's own second sentence, primary-source-verified against two independent scans and previously uncited in the register (only "commemoratio de Tempore fit primo loco" was quoted before), gives the real rule: "in admittendis et ordinandis aliis commemorationibus, servetur ordo tabellae praecedentiae" -- admitting and ordering commemorations both run on the rite's own table of precedence (band's 28-entry table), not RG 8's coarse four-class rank. Precedence.resolve now computes each commemoration candidate's own band value once, generically, and hands it to rules.admit as a third tuple element (Precedence.rules.admit's signature changed accordingly, ditto Precedence_ef.admit; every rule-record stub in the test suite updated to match). Precedence_ef.admit's own compare_dignity is replaced by compare_precedence, ordering by band then slug; a residual tie within one identical band value still falls back to slug, since RG 113 gives no further instruction there -- documented as a still-uncited engineering convention, not dressed up as a rubric. RG 98 ("in paritate autem Officium prius impeditum praecedit") was considered as a candidate authority for that residual and rejected: it governs the transfer queue order among several simultaneously-impeded I-class feasts (Caput XIII), a different operation in a different chapter from RG 113's commemoration admission (Caput XVI); nothing in the primary text connects the two. Blast radius measured against the pre-change binary across the entire 1583-9999 domain (not only 2005-2050): the admitted-commemoration-slug set is byte-identical, day for day, before and after this change. The fix corrects the citation and mechanism, not the answer, on this codebase's current data -- both of the task brief's named examples (22 Feb Chair-of-Peter/Lent-vs-Paul, 22 Sept Maurice-vs-Thomas-of- Villanova) are confirmed present and unchanged in both streams. A new test (RG113: admit picks by precedence order, not slug, when they disagree) proves admit actually consults the passed-in precedence value with a synthetic pair whose slug order and precedence order disagree -- teeth a same-band-only regression test could not have caught, since every real collision found in the domain happens to agree on both axes. 271 -> 272 tests, all green; COLITUR_EXHAUSTIVE_SWEEP=1 unaffected.
Diffstat (limited to 'lib/rites/rite_ef/precedence_ef.ml')
-rw-r--r--lib/rites/rite_ef/precedence_ef.ml114
1 files changed, 64 insertions, 50 deletions
diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml
index 5688640..861bb1a 100644
--- a/lib/rites/rite_ef/precedence_ef.ml
+++ b/lib/rites/rite_ef/precedence_ef.ml
@@ -758,38 +758,47 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate)
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, silently
+ deciding 66 days over 2005-2050 (e.g. 22 Sept 2027, an Ember Wednesday:
+ the alphabetical rule admitted "maurice-and-companions-martyrs" over
+ "thomas-of-villanova", both Class3/{!band} entry 24 -- missalemeum shows
+ Thomas). {!band} needs a [context] this function itself does not have
+ (date/season/weekday) -- unlike [dignity], which read [rank] alone --
+ so {!Precedence.resolve} now computes each candidate's own [band] value
+ once, generically, and hands it to [admit] as the trailing [int] on each
+ input triple (see {!Precedence.rules.admit}'s own doc). [comms] below is
+ [(candidate * privilege * int) list], not the pair it used to be. *)
+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)
~(temporal : Vocab_ef.rank Precedence.candidate)
- (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list) :
+ (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
@@ -801,9 +810,11 @@ 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
(* CORRECTED (fix round 1, RG16(a) task): read off [temporal], not
[observed] -- see this function's own doc comment above for the full
@@ -819,9 +830,9 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate)
| 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
@@ -835,17 +846,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
@@ -857,21 +869,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
@@ -884,9 +896,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,