From 7c3964be29a8902ec6fa3965a67586483313dc4e Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 20:15:14 +0200 Subject: fix(kernel): a transferred candidate can settle as a commemoration, not only as observed Calendar.build_day's `unresolved` check decided whether a Transfer-disposed candidate had genuinely settled at its target by checking only whether it became that day's own `observed` celebration. That was correct for every prior use of Precedence.Transfer: a losing FEAST, which RG-96-style rules guarantee an unblocked target to win outright once it arrives. It is not correct in general. A rite's rules are free to dispose a Celebration.status = Commemoration_only candidate as Transfer too (the EF Major Litanies, RG 80, do exactly this) -- and such a candidate can never become `observed` anywhere, by the same status that makes it eligible to transfer in the first place. The old check mislabelled a cleanly-settled transfer of that shape as "did not converge" (a hardcoded string, not a real read of the placement pass's own convergence) and double-counted it in Validate's own duplicated-sighting check. Replaced with `settled_at`, which re-resolves the target date and accepts either `observed` or membership in that day's own admitted commemorations. A strict superset of the old check -- every existing use (a transferred feast winning its target) is unaffected -- and stays rite-agnostic: it reads only Precedence.resolution's existing fields, no EF-specific knowledge added to the kernel. Found by the exhaustive property sweep (COLITUR_EXHAUSTIVE_SWEEP=1) the moment a rite first produced this shape, not anticipated in advance. --- lib/kernel/calendar.ml | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/kernel/calendar.ml b/lib/kernel/calendar.ml index 5d3572c..27e564f 100644 --- a/lib/kernel/calendar.ml +++ b/lib/kernel/calendar.ml @@ -304,14 +304,49 @@ let build_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.by_date) day it landed and [transferred_out] here, not via [omitted] too -- double-booking it in both would fail Task 12's "appears exactly once" reading of this day alone. *) + (* Whether a transferred/injected candidate is genuinely accounted for at + its assigned target -- true if it won the day outright there (every + [Transfer]-disposed candidate this kernel produced before a rite could + transfer a [Celebration.status = Commemoration_only] one: a losing + FEAST, which RG 96's own "next day not I or II class" guarantees an + unblocked day to win once it arrives -- [occupant_of] alone answered + this), OR if it survives at the target as one of the day's own admitted + COMMEMORATIONS instead (a shape [place_transfers] itself never used to + produce, because nothing before could dispose a [Commemoration_only] + candidate as [Transfer] -- {!Precedence.resolve} holds such a candidate + out of the band contest entirely, so it can never win a day outright, + only ever be commemorated on one; a rite is nonetheless free to + [Transfer] one to a named date, e.g. RG 80's Major Litanies, RG 81's + own "nihil fit in Officio" making [observed] structurally impossible + for it anywhere). [occupant_of] alone under-reports this second shape + as unsettled, which previously had no live witness to catch it: a + transferred candidate that only ever becomes a commemoration, never the + day's own office, was mislabelled here with [unconverged_reason] (a + hardcoded string, not a true read of the placement pass's own + convergence -- {!place_transfers} itself had already reached a fixed + point) and double-counted by {!Validate}'s own "duplicated" check + (sighted once in [omitted] here under that wrong label, and correctly + again in [commemorations] at its target) -- found by this kernel's own + exhaustive property sweep once a rite (Rite_ef, RG 80) first produced + this shape, not guessed at in advance. Kept rite-agnostic: nothing here + reads anything EF-specific, only {!Precedence.resolution}'s own + [observed]/[commemorations] fields, the same two channels + {!Liturgical_day.t} already promises never to lose. *) + let settled_at target slug = + let _, _, target_resolution = resolve_with_injected rite idx injected target in + let matches (c : 'r Precedence.candidate) = + Slug.equal c.Precedence.cel.Celebration.slug slug + in + matches target_resolution.Precedence.observed + || List.exists (fun (c, _) -> matches c) target_resolution.Precedence.commemorations + in let unresolved c = - let slug = Slug.to_string c.Precedence.cel.Celebration.slug in - if Hashtbl.mem out_of_range slug then true + let slug = c.Precedence.cel.Celebration.slug in + if Hashtbl.mem out_of_range (Slug.to_string slug) then true else - match Hashtbl.find_opt assignment slug with + match Hashtbl.find_opt assignment (Slug.to_string slug) with | None -> true - | Some (_, target) -> - not (Slug.equal (occupant_of rite idx injected target).Celebration.slug c.Precedence.cel.Celebration.slug) + | Some (_, target) -> not (settled_at target slug) in let reason_for c = if Hashtbl.mem out_of_range (Slug.to_string c.Precedence.cel.Celebration.slug) then -- cgit v1.3 From 3364e23fc49dc5b49576a97315326de030376e31 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 20:15:28 +0200 Subject: feat(ef): the Major Litanies -- entity, RG80 transfer, RG109(f) privilege RG 80 (Caput X, "De Litaniis maioribus et minoribus", A): the Major Litanies are assigned to 25 April, transferred to the following Tuesday (always Easter+2) when that date is Easter Sunday or Easter Monday. RG 81, same division: nothing is done in the Office, only in the Mass. RG 109(f) (Caput XVI): a privileged commemoration. Entity: Commemoration_only, Fixed(4,25), via data/ef/adjustments.sexp's `Add major-litanies` -- RG 81 denies it Office standing entirely, which is exactly what Commemoration_only already means to Precedence.resolve (held out of the observed contest by construction). Rank Class4 deliberately, not Class1, to keep the new RG109(f) privilege branch live rather than shadowed by privilege_of's existing "of a I-class day" branch. Colour Violet by consistency with the sibling Minor Litanies; subject Saint, the same "no special exclusion" default this codebase's other Commemoration_only entries use. Names not primary-sourced, flagged as such in the data file's own comment. Transfer: reuses the existing RG 96 placement machinery (Precedence.disposition's Transfer constructor + Calendar's place_transfers/resolve_with_injected) rather than a new candidate channel -- RG 80's own transfer is structurally the same operation RG 96 already performs for an impeded I-class feast. disposition gains a branch, checked before the Commemoration_only catch-all: the Litanies losing to winner slug "ef-easter-sunday" or "ef-easter-1-monday" is Transfer, not Commemorate. transfer_target gains a branch, checked before the general RG 96 search is even computed, returning Easter+2 directly with no search at all -- necessary, not merely simpler, since Easter+2 is itself I-class and a search from there would walk past the exact day RG 80 names (a commemoration needs no unoccupied day the way a displaced feast does). Privilege: privilege_of's previously-dead RG 109(f) branch now recognises the Litanies' own slug. No other code change -- admit's existing Class1/Class2-Sunday branches already implement "a privileged commemoration categorically takes the slot" correctly, from an earlier task. Full blast-radius measurement, adjudication of the resulting missalemeum divergence, and the mutation proofs are in the task's own report, .superpowers/sdd/2026-08-12-colitur-rg16a/major-litanies-report.md. --- data/ef/adjustments.sexp | 88 ++++++++++++++++ lib/rites/rite_ef/precedence_ef.ml | 198 +++++++++++++++++++++++++++++++----- lib/rites/rite_ef/precedence_ef.mli | 60 +++++++++-- 3 files changed, 309 insertions(+), 37 deletions(-) (limited to 'lib') diff --git a/data/ef/adjustments.sexp b/data/ef/adjustments.sexp index 740180c..a5e7c6f 100644 --- a/data/ef/adjustments.sexp +++ b/data/ef/adjustments.sexp @@ -165,6 +165,87 @@ ; (Commemoration_only entries are never the OBSERVED celebration, so this ; entry's OWN colour is never printed by the current pipeline, the same ; note `Edit eusebius-confessor` above makes for its own colour field). +; +; `Add major-litanies` -- NEW (ef-major-litanies task). RG 80 (Caput X, "De +; Litaniis maioribus et minoribus", §A "De Litaniis maioribus"; all three +; primary documents -- both photographic scans AND the electronic +; transcription -- word for word, no divergence to adjudicate): "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." RG 81, same division: "81. De Litaniis maioribus +; nihil fit in Officio, sed tantum in Missa. Earum autem commemoratio non +; est habenda commemoratio 'de Tempore'." RG 109(f) (Caput XVI, "De +; Commemorationibus"): "f) de Litaniis maioribus, in Missa" -- a +; privileged commemoration. The calendarium's own April table (all three +; documents, e.g. the transcription: "iv c vii 25 Litania Maior. - S. +; MARCI EVANGELISTAE, II classis.") prints "Litania Maior" as part of 25 +; April's own descriptive line, alongside St Mark, not as a second, +; separately class-numbered row -- corroborating RG 81's "nihil fit in +; Officio" the same way Maurice's own "Commemoratio ... " line (no class +; number) corroborates a demoted feast elsewhere in this project (see +; precedence_ef.ml's [band], top-of-function guard). +; +; MODELLING DECISION: `Commemoration_only`, `Fixed(4, 25)`, the same shape +; as `commemoration-of-st-peter` above -- because RG 81 denies the +; Litanies any Office standing at all ("nihil fit in Officio"), so they +; can never be the OBSERVED day (RG 91's table enumerates "dies +; liturgici", Office days, and this is explicitly none), only ever a Mass +; commemoration. `Precedence_ef.disposition`/`.privilege_of` wire RG +; 109(f)'s privilege and RG 80's own transfer condition (both cited in +; full at `Precedence_ef.major_litanies_slug` and its two call sites); see +; that module for the reasoning and precedence_ef.ml's own comments for +; why the transfer reuses the existing RG 96 placement machinery +; ({!Colitur_kernel.Precedence.Transfer}) rather than new machinery. +; +; `rank Class4`: RG 91's Table of Precedence has NO row for the Major +; Litanies at all (RG 81 denies them Office standing; the calendarium's +; own printed line above confirms it), so there is no class this data +; field could faithfully report -- the same situation `band`'s own +; top-of-function guard already documents for every `Commemoration_only` +; entry ("never had a row in the table for that occasion to begin with"). +; `Class4` is chosen as the lowest/inertest value available, deliberately +; NOT `Class1` (which would make `Precedence_ef.privilege_of`'s branch (b), +; "of a I-class day", grant this entry Privileged status for the WRONG +; cited reason, silently making the (f) branch this task wires dead code +; again). `band` ignores rank entirely for any `Commemoration_only` +; candidate (checked first, unconditionally); `admit`'s only rank-keyed +; test for an ordinary (non-privileged) II-class-Sunday commemoration +; never applies here either, since this entry is always Privileged. No +; live behavioural consequence either way -- recorded as a deliberate, +; honestly-flagged placeholder, not a citation. +; +; `colour Violet`: NOT independently scan-verified for this specific +; entry (out of this task's scope -- Mass propers are Plan 4, `citations` +; stays `()`). Matches, by internal consistency, the colour this same +; codebase already assigns the sibling Minor Litanies/Rogation days +; (`rite_ef/temporal_ef.ml`'s own Rogation branch: `~colour:Colour.Violet` +; for RG 87's Monday/Tuesday before Ascension) -- both are "Litaniae", +; major and minor, sharing the same penitential/processional character; +; RG 80/81/109(f) themselves say nothing about colour. Inert today +; regardless (`Commemoration_only` entries are never OBSERVED, so this +; field is never printed by the current pipeline -- the same note `Edit +; eusebius-confessor` above makes for its own colour field). +; +; `subject Saint`: no clean fit among {Lord, Bvm, Saint, Temporal} -- the +; Litanies are tied to no particular Divine Person or to the BVM (RG 80/81 +; name no one), and are far from an ordinary saint's feast either, but +; `Subject.Temporal` is reserved for the temporal cycle's OWN office +; (`subject.mli`), which this Fixed, sanctoral-origin entry structurally +; is not. `Saint` is the same "no special exclusion applies" default this +; codebase's own other `Commemoration_only` sanctoral entries already use +; (RG 110's own Peter/Paul companion entries, `commemoration-of-st-peter` +; immediately below) -- its only live behavioural consequence anywhere in +; this codebase, RG 112(a)/(d)'s Lord/BVM mutual-exclusion rules, is one +; this entry should NOT trigger either way, so `Saint` is safe as well as +; consistent. +; +; `names`: English ("The Major Litanies") is a plain translation of RG +; 80's own "Litaniae maiores", not independently primary-sourced (no +; English title exists in any of the three documents, all Latin). Polish +; ("Litanie Większe") likewise a plain vernacular rendering, not verified +; against any external Polish liturgical reference -- flagged here rather +; than presented as sourced, the same honesty this file's own +; `commemoration-of-st-peter` entry gives its own colour field above. ((id ef-adjustments) (directives ((Suppress vigil-of-christmas) @@ -177,4 +258,11 @@ (names ((en "St. Peter") (pl "\197\155w. Piotra, Aposto\197\130a"))) (rank Class3) (status Commemoration_only) (colour Red) + (subject Saint) (citations ()) (layer ef-universal))))) + (Add + ((date (Fixed (month 4) (day 25))) + (cel + ((slug major-litanies) + (names ((en "The Major Litanies") (pl "Litanie Większe"))) + (rank Class4) (status Commemoration_only) (colour Violet) (subject Saint) (citations ()) (layer ef-universal)))))))) diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index 7b64b83..e8c05cd 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -651,6 +651,48 @@ let nativity_octave_prefix = "ef-nativity-octave-day-" [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 @@ -741,23 +783,30 @@ let privilege_of (c : Vocab_ef.rank Precedence.candidate) : Precedence.privilege 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) (§4): "of the Major Rogations, in Mass" -- the - Major Litanies (25 April, RG 80) are STILL not computed anywhere in - this codebase (temporal_ef.ml's own comment on [temporal]'s Rogation - branch, CORRECTED final fix wave item 7: they did not, in fact, - "arrive with Plan 3's sanctoral" -- Plan 3 shipped without them, - register §6 tracks this as an open item with no plan yet committed to - build it), so no candidate this engine can currently construct - represents one. There is no existing slug - convention to anchor a check to, and guessing one risks silently - misclassifying whatever a future task does name it -- a wrong citation - is worse than a missing one, so this is left unimplemented and flagged - in the task report rather than guessed. Deliberately NOT matched by - anything above: 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), so they correctly fall through to - "ordinary" below, not this category. *) + (* (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) @@ -793,6 +842,63 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate) 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 @@ -1656,14 +1762,52 @@ let rec search_from (occupant : Date.t -> Vocab_ef.rank Celebration.t) (steps : 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 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 let easter = Computus.gregorian_easter (Date.year origin) 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 + 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 diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli index 765b573..419613a 100644 --- a/lib/rites/rite_ef/precedence_ef.mli +++ b/lib/rites/rite_ef/precedence_ef.mli @@ -120,17 +120,40 @@ val band : Vocab_ef.season Precedence.context -> Vocab_ef.rank Precedence.candid somewhere to be caught other than a silently-wrong RG 33 disposition. *) val sunday_marker : string +(** The Major Litanies' own invented slug (RG 80, Caput X "De Litaniis + maioribus et minoribus" §A; RG 81; RG 109(f) -- see the .ml's own + citation, above [privilege_of], for the full text). Not adopted from + lectio, which does not compute the Major Litanies at all (this task's + own measurement) -- colitur's own key, data/ef/adjustments.sexp's + matching [Add] entry. Exposed for the same reason as + {!annunciation_slug} below: a future rename in the data with no + matching rename here silently stops {!disposition}, {!admit}'s RG + 109(f) privilege, and {!transfer_target}'s own RG 80 branch from ever + recognising a live candidate again. *) +val major_litanies_slug : string + (** [disposition ~winner ~loser]: RG 92-95, 33, 21-27, 94 (docs/research/ rules-register.md §4, "Occurrence", "Vigils" and "Caput IV, 'De feriis'"). What becomes of a losing candidate, decided by the LOSER's own rank and status (RG 95), except RG 33's vigil omission, which also reads the winner: + - a loser whose slug is {!major_litanies_slug} (ef-major-litanies task) + is [Transfer], NOT [Commemorate], when the WINNER's own slug is + Easter Sunday or Easter Monday (RG 80: 25 April's Major Litanies are + transferred to the following Tuesday, always Easter+2, when 25 April + is itself one of those two days) -- checked first, ahead of the + [Commemoration_only] bullet immediately below (of which this would + otherwise be a case), because RG 80's own condition overrides RG 81's + default "always Commemorate" for these two specific winners only. See + the .ml's own citation, and {!transfer_target}'s own RG 80 branch for + where the transfer lands; - a {!Celebration.status} of [Commemoration_only] is always - [Commemorate] (checked first: it can never win -- see - {!Precedence.resolve} -- and, by that same status's own definition, - already denotes an office with nothing left to translate, so it never - transfers either; not itself a further RG citation beyond RG 93's - general four-mechanism statement above); + [Commemorate] otherwise (checked first among the remaining cases: it + can never win -- see {!Precedence.resolve} -- and, by that same + status's own definition, already denotes an office with nothing left + to translate under RG 95's ordinary rule, so it never transfers + either there; not itself a further RG citation beyond RG 93's general + four-mechanism statement above); - a [Class2] or [Class3] loser whose slug marks it a vigil ({!vigil_suffix} OR {!vigil_prefix} -- both conventions this codebase's data uses, see {!vigil_prefix}'s own comment) is [Omit] @@ -243,6 +266,14 @@ val september_ember_prefix : string - [observed] a [Class3] or [Class4] day: at most two, by {!band}'s table order alone. + RG 109(f) (ef-major-litanies task): every candidate in [comms] arrives + already tagged with its real privilege by {!disposition}, which now + includes {!major_litanies_slug} tagged [Privileged] -- this function + itself needed no change for it, the same "no new machinery" shape RG + 109(a)/(c)/(d)/(e) already had: it competes for whichever of the four + cases above its host day falls into, exactly like any other privileged + candidate. + ADDED, ef-holyname-rg110 task: RG 110 (docs/research/rules-register.md §4, Caput XIV) -- "In Officio et Missa S. Petri semper fit commemoratio S. Pauli, et vicissim... pro unica habeantur" -- whenever @@ -339,11 +370,20 @@ val annunciation_slug : string *is* {!Colitur_kernel.Rite.t}.transfer_target; see that field's own fuller rationale for why the search has to be rite-supplied at all. - RG 96's own rule: the next following day whose currently-resolved - occupant is not I or II class (read off [Vocab_ef.rank], RG 8's dignity - -- not {!band}'s finer occurrence-table entry, the same distinction - {!admit} draws for RG 111). This general target is computed for EVERY - candidate, always, first. + RG 80 (ef-major-litanies task): checked FIRST, ahead of everything + below -- when [c] is {!major_litanies_slug}, the target is Easter+2 + directly, with no RG 96 search at all (this candidate is only ever a + commemoration at its target, RG 81's own "nihil fit in Officio", which + needs no unoccupied day the way a displaced FEAST does; the general + RG 96 search below would in fact walk PAST Easter+2, since it is itself + I-class). See the .ml's own citation for the full text and the "why not + the general search" argument. + + RG 96's own rule, for every OTHER candidate: the next following day + whose currently-resolved occupant is not I or II class (read off + [Vocab_ef.rank], RG 8's dignity -- not {!band}'s finer occurrence-table + entry, the same distinction {!admit} draws for RG 111). This general + target is computed for every such candidate, always, first. RG 96's own named exception (Attamen (a), primary-source-verified -- see {!annunciation_slug}'s comment for the Latin and the register's own -- cgit v1.3 From 10f0e964ceb2999b330ca4b3c6545e24eeb71f6c Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 20:55:28 +0200 Subject: fix(kernel): a transferred candidate can settle by being capped out, too (fix round 1, F1) The prior fix (settled_at) recognised two settlement channels for a transferred candidate at its target -- winning outright (observed) or surviving as a commemoration -- but missed a third: reaching the target and then being CAPPED OUT there, by admit's own RG-111-style admission count limit or by disposition's own Omit. That candidate lands in the target's own omitted list, genuinely settled and accurately labelled, but settled_at did not check that list, so the origin reported it as unresolved under the same wrong, hardcoded unconverged_reason -- the exact original bug, one level further out. Unreachable on shipped EF data (the Major Litanies are the only privileged Commemoration_only candidate real data carries, and no second one can ever share Easter+2), but reachable by construction: a second privileged Commemoration_only entry on the Litanies' own transfer target that outranks it in admit's Class1 selection, or -- without any synthetic data -- forcing the Litanies' own RG 109(f) privilege to Ordinary, which makes the transferred candidate lose that same cap against its own real target. Fixed by adding target-omitted membership as a third disjunct in settled_at. New regression test in test_calendar.ml, built the same way: the real EF layer plus one synthetic privileged Commemoration_only entry on the real 2011 transfer target, sorting ahead of the Litanies so it wins the Class1 slot. Mutation-verified to fail specifically when the third disjunct is removed. COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force stays clean after the fix, confirming it changes no shipped day's output. --- lib/kernel/calendar.ml | 104 +++++++++++++++++++++++++++++++++++++------------ test/test_calendar.ml | 90 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 168 insertions(+), 26 deletions(-) (limited to 'lib') diff --git a/lib/kernel/calendar.ml b/lib/kernel/calendar.ml index 27e564f..f2a03a7 100644 --- a/lib/kernel/calendar.ml +++ b/lib/kernel/calendar.ml @@ -305,33 +305,86 @@ let build_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.by_date) double-booking it in both would fail Task 12's "appears exactly once" reading of this day alone. *) (* Whether a transferred/injected candidate is genuinely accounted for at - its assigned target -- true if it won the day outright there (every - [Transfer]-disposed candidate this kernel produced before a rite could - transfer a [Celebration.status = Commemoration_only] one: a losing - FEAST, which RG 96's own "next day not I or II class" guarantees an - unblocked day to win once it arrives -- [occupant_of] alone answered - this), OR if it survives at the target as one of the day's own admitted + its assigned target -- true under any of THREE conditions, not two + (fix round 1, ef-major-litanies task -- the first version of this + function, and this comment, claimed two, missing exactly the same + shape one channel further out than the bug it had just fixed; see + below for how that was found). + + (1) It won the day outright there (every [Transfer]-disposed candidate + this kernel produced before a rite could transfer a + [Celebration.status = Commemoration_only] one: a losing FEAST, which + RG 96's own "next day not I or II class" guarantees an unblocked day + to win once it arrives -- [occupant_of] alone used to answer this, and + still would). + + (2) It survives at the target as one of the day's own admitted COMMEMORATIONS instead (a shape [place_transfers] itself never used to produce, because nothing before could dispose a [Commemoration_only] - candidate as [Transfer] -- {!Precedence.resolve} holds such a candidate - out of the band contest entirely, so it can never win a day outright, - only ever be commemorated on one; a rite is nonetheless free to - [Transfer] one to a named date, e.g. RG 80's Major Litanies, RG 81's - own "nihil fit in Officio" making [observed] structurally impossible - for it anywhere). [occupant_of] alone under-reports this second shape - as unsettled, which previously had no live witness to catch it: a - transferred candidate that only ever becomes a commemoration, never the - day's own office, was mislabelled here with [unconverged_reason] (a - hardcoded string, not a true read of the placement pass's own - convergence -- {!place_transfers} itself had already reached a fixed - point) and double-counted by {!Validate}'s own "duplicated" check - (sighted once in [omitted] here under that wrong label, and correctly - again in [commemorations] at its target) -- found by this kernel's own - exhaustive property sweep once a rite (Rite_ef, RG 80) first produced - this shape, not guessed at in advance. Kept rite-agnostic: nothing here - reads anything EF-specific, only {!Precedence.resolution}'s own - [observed]/[commemorations] fields, the same two channels - {!Liturgical_day.t} already promises never to lose. *) + candidate as [Transfer] -- {!Precedence.resolve} holds such a + candidate out of the band contest entirely, so it can never win a day + outright, only ever be commemorated on one; a rite is nonetheless free + to [Transfer] one to a named date, e.g. RG 80's Major Litanies, RG + 81's own "nihil fit in Officio" making [observed] structurally + impossible for it anywhere). The FIRST version of this function + stopped here, at (1) and (2) -- {!Liturgical_day.t}'s own doc comment + promises [observed] and [commemorations] are never silently lost, and + this read as "the same two channels", which is where the "two, not + three" miscount came from: that promise is about {!Liturgical_day.t}'s + OWN five fields, not an exhaustive account of every way + {!Precedence.resolve} can dispose of a candidate at one date. + + (3) It reaches the target and is disposed there as [Omit] by + {!Precedence.rules.disposition} itself (RG 33's vigil omission, RG + 16(a)'s Sunday suppression, RG 26's IV-class-feria omission, the + Lord-vs-Lord exclusion, ...) OR is offered to + {!Precedence.rules.admit} there but capped out by an admission-count + limit RG 111 itself imposes (e.g. a Class1 day admits only ONE + privileged commemoration; a second one due the same day, or the SAME + Litanies candidate no longer privileged, loses that slot) -- both + land in the target's own [omitted], with their own accurate, + already-diagnostic reason ("omitted: yielded to a higher day" / + "omitted: admission limit reached"), and both are just as genuinely + "delivered and considered" as (1)/(2), not stuck anywhere. Missing + this third channel reproduces the EXACT ORIGINAL BUG this function was + written to fix, one level further out: a candidate settled (via (3)) + at its target was still reported [unresolved] at its ORIGIN, under + the same wrong, hardcoded [unconverged_reason] label, and + double-counted by {!Validate}'s own "duplicated" check the same way. + + Found, not merely reasoned to: fix round 1's review reproduced it two + ways. Constructively, a second privileged [Commemoration_only] entry + placed on the Litanies' own transfer target (Easter+2) that sorts + ahead of it forces the Litanies to lose {!Precedence.rules.admit}'s + own Class1 "one privileged commemoration only" cap there. And, + already present in this branch's own mutation-testing record without + being run to ground at the time: mutation 3 (this task's own report, + `privilege_of`'s RG 109(f) branch forced to [Ordinary]) makes the + transferred Litanies itself lose that SAME Class1 cap at its OWN + target -- no second candidate needed, since an [Ordinary] commemoration + has no standing at all against a [Class1] day's privileged-only + admission rule ({!Precedence_ef.admit}'s own Class1 case). That + mutation's 9th failure, the exhaustive `Validate` property sweep on a + random year, was this bug; the report noted the failure and declined + to diagnose it before reverting the mutation, which is precisely how + it survived one fix round. + + Unreachable on shipped EF data today (every `Commemoration_only` + candidate this rite's own real data carries is at most [Class3] + except the Litanies themselves, and no second privileged + commemoration can ever fall on Easter+2 -- {!Precedence_ef + .privilege_of}'s own (a)-(e) categories are all Sunday/Ember/Advent- + Lent-Passiontide/Nativity-octave shaped, none of which Easter+2 is or + can be), which is exactly why it survived this far: nothing in the + shipped calendar has ever exercised it. Fails LOUDLY (a wrong, + misleading label) rather than silently, and is fixed here rather than + left as a documented residual, since the fix is a one-line + generalisation of the same check, not new machinery. Kept + rite-agnostic: nothing here reads anything EF-specific, only + {!Precedence.resolution}'s own [observed]/[commemorations]/[omitted] + fields -- THREE of {!Precedence.resolution}'s four fields (the fourth, + [deferred], denotes a candidate that has NOT yet settled at this date, + by definition, so it is correctly never consulted here). *) let settled_at target slug = let _, _, target_resolution = resolve_with_injected rite idx injected target in let matches (c : 'r Precedence.candidate) = @@ -339,6 +392,7 @@ let build_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.by_date) in matches target_resolution.Precedence.observed || List.exists (fun (c, _) -> matches c) target_resolution.Precedence.commemorations + || List.exists (fun (c, _) -> matches c) target_resolution.Precedence.omitted in let unresolved c = let slug = c.Precedence.cel.Celebration.slug in diff --git a/test/test_calendar.ml b/test/test_calendar.ml index 93cd0bb..22f5f04 100644 --- a/test/test_calendar.ml +++ b/test/test_calendar.ml @@ -426,6 +426,90 @@ let test_transfer_guard_records_failure_instead_of_looping () = in Alcotest.(check bool) "non-convergence is recorded rather than silently dropped or hung" true stuck +(* ef-major-litanies task, fix round 1 (F1) -- a regression test for a + THIRD settlement channel `build_day`'s own `settled_at` had to learn to + recognise: a transferred candidate that reaches its target and is then + CAPPED OUT there by the target day's own RG 111 admission-count limit + (not `observed`, not surviving as a `commemoration` -- the two channels + the first version of this fix checked), rather than settling cleanly. + Missing it reproduces the exact original bug ONE LEVEL FURTHER OUT: the + origin wrongly reports the transferred candidate as + `unconverged_reason`, even though placement genuinely converged. + + Unreachable on real EF data ALONE (the Major Litanies, RG 80, are the + only privileged `Commemoration_only` candidate real data carries, and + no second one can ever coincide with Easter+2) -- reproduced here the + same way the fix-round review did: one synthetic privileged + `Commemoration_only` candidate, added directly to the REAL EF layer + (not a hand-built synthetic rite -- this bug is about the real Litanies + candidate's own real transfer, so the real rite is the honest fixture), + on the real Litanies' own real 2011 transfer target (26 April -- Easter + 2011 = 24 April, so 25 April is Easter Monday, RG 80's second trigger, + landing on Easter+2 = 26 April), with a slug ("aaa-probe") sorting + ahead of "major-litanies" so it wins {!Rite_ef.Precedence_ef.admit}'s + Class1 "one privileged commemoration only" selection there, capping the + Litanies out. *) +let real_ef_layer_for_transfer_probes = + match Colitur_kernel.Layer.load Rite_ef.Vocab_ef.rank_of_sexp "../data/ef/sanctoral.sexp" with + | Error e -> failwith ("../data/ef/sanctoral.sexp: " ^ e) + | Ok layer -> ( + match Colitur_kernel.Overlay.load Rite_ef.Vocab_ef.rank_of_sexp "../data/ef/adjustments.sexp" with + | Error e -> failwith ("../data/ef/adjustments.sexp: " ^ e) + | Ok overlay -> + let layer, diagnostics = Colitur_kernel.Overlay.apply layer overlay in + if diagnostics <> [] then failwith "unexpected overlay diagnostics loading the real EF layer"; + layer) + +let test_transferred_commemoration_only_capped_out_at_target_settles_cleanly () = + let probe_date = + match Colitur_kernel.Date_spec.fixed ~month:4 ~day:26 with Ok d -> d | Error e -> failwith e + in + let probe = + { Colitur_kernel.Layer.date = probe_date; + cel = + Cel.make ~slug:(Sl.of_string_exn "aaa-probe") ~rank:Rite_ef.Vocab_ef.Class1 + ~status:Cel.Commemoration_only ~colour:Colitur_kernel.Colour.White + ~subject:Colitur_kernel.Subject.Saint ~layer:"synthetic-probe" () + } + in + let augmented_layer = Colitur_kernel.Layer.set real_ef_layer_for_transfer_probes probe in + (* Liturgical year "2010" (Advent 2010 -- eve of Advent 2011) covers both + 25 and 26 April 2011. *) + let year = C.year Rite_ef.context augmented_layer 2010 in + let find_date target = + match Array.to_list year |> List.find_opt (fun d -> D.compare d.LD.date target = 0) with + | Some d -> d + | None -> failwith "date not found in resolved year" + in + let origin = find_date (mk 2011 4 25) and target = find_date (mk 2011 4 26) in + let slug_s (c : Rite_ef.Vocab_ef.rank Cel.t) = Sl.to_string c.Cel.slug in + Alcotest.(check string) "2011-04-25 is Easter Monday, RG80's second trigger" "ef-easter-1-monday" + (slug_s origin.LD.observed); + let omitted_s d = List.map (fun (c, r) -> (slug_s c, r)) d.LD.omitted in + Alcotest.(check (list (pair string string))) "origin: major-litanies is NOT in [omitted] at all -- it \ + genuinely, cleanly transferred away, no false 'did not converge'" [] + (List.filter (fun (s, _) -> s = "major-litanies") (omitted_s origin)); + 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 + in + Alcotest.(check (list (pair string string))) "origin: no [omitted] entry anywhere claims non-convergence \ + (the exact regression this test guards against, stated directly rather than only via the slug check \ + above)" [] + (List.filter (fun (_, r) -> contains_substring r ~needle:"did not converge") (omitted_s origin)); + Alcotest.(check (list string)) "origin: [transferred_out] still correctly names major-litanies -> target" + [ "major-litanies->2011-04-26" ] + (List.map + (fun (c, d) -> Printf.sprintf "%s->%s" (slug_s c) (D.to_iso8601 d)) + origin.LD.transferred_out); + Alcotest.(check (list string)) "target: aaa-probe wins the Class1 privileged slot (sorts ahead of \ + major-litanies at the tied [unclassified] band)" [ "aaa-probe" ] + (List.map (fun (c, _) -> slug_s c) target.LD.commemorations); + Alcotest.(check bool) "target: major-litanies is capped out into [omitted] there, with the REAL \ + admission-limit reason, not lost and not mislabelled" true + (List.mem ("major-litanies", "omitted: admission limit reached") (List.map (fun (c,r) -> (slug_s c, r)) target.LD.omitted)) + let suite = ( "Calendar", [ Alcotest.test_case "year covers every day" `Quick test_year_covers_every_day; @@ -447,4 +531,8 @@ let suite = Alcotest.test_case "transfer target outside year is recorded not lost" `Quick test_transfer_target_outside_year_is_recorded_not_lost; Alcotest.test_case "transfer guard records failure instead of looping" `Quick - test_transfer_guard_records_failure_instead_of_looping ] ) + test_transfer_guard_records_failure_instead_of_looping; + Alcotest.test_case + "ef-major-litanies fix round 1 (F1): a transferred candidate capped out at its own target settles \ + cleanly, no false 'did not converge'" + `Quick test_transferred_commemoration_only_capped_out_at_target_settles_cleanly ] ) -- cgit v1.3 From cbf6826edc77e3ad5a813237bae27e367221452c Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 20:55:43 +0200 Subject: docs(ef): fix round 1 corrections (F2-F5) -- inert fields, a date, an attribution, RG434(b) F2: the task report's "colour/subject/names on the Litanies entry are currently inert" was false for names -- test_oracle.ml's identity axis reads Names.find on every commemoration, and the invented English name is exactly why 2026-04-25 registers as a Comm_identity_mismatch, the reason M5 had to be re-gated. Corrected precisely, field by field, in the report. F3: Easter 2027 is 28 March, not 18 April, as previously stated in the report's own "layer 4 is blind to the transfer" reasoning. The conclusion survives (28 March is not 25 April); the date is corrected. F4: "the exhaustive property sweep caught this immediately" was a wrong attribution, in the report, the register, and calendar.ml's own comment. The committed exhaustive sweep walks 1583-9998 in order and aborts at the first failure; the years actually reported (5700, 4747) were draws from prop_invariants, the default dune test run's 200-year random sample. Corrected in all three places. F5: the Litanies' privileged commemoration is due in the MASS specifically (RG 108 + RG 81), never a separate Office answer -- colitur emits one resolved day, and the M20 adjudication rests on that being the Mass reading. Stated explicitly now in M20's own note, the register, and precedence_ef.ml's own RG111(b) comment. Also added to M20 and the register: Rubricae Generales Missalis Romani n. 434(b) ("VIII - De diversis Missae partibus", "D) De orationibus", "I - De orationibus in genere"), verified word for word against all three documents -- word-identical to RG 111(b) but explicitly scoped to the Mass ("post orationem Missae"), answering the objection that RG 111 (Caput XVI, which RG 106 says governs both Office and Mass) might be read as Office-shaped. Moves the M20 adjudication from "moderate-high, not certain" to near-certain. --- CLAUDE.md | 13 +++++++++--- data/ef/expected-divergences-missalemeum.sexp | 8 +++++-- lib/rites/rite_ef/precedence_ef.ml | 30 ++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/CLAUDE.md b/CLAUDE.md index 74f6a40..6e8289a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -573,9 +573,16 @@ original, distinct architectural reason. along the way, kept rite-agnostic: `calendar.ml`'s `build_day` used to decide "did a transfer settle" by checking ONLY whether the candidate became `observed` at its target — impossible by construction for a - `Commemoration_only` candidate (RG 81), caught by the exhaustive - property sweep the moment a rite first produced this shape. Full - account: `.superpowers/sdd/2026-08-12-colitur-rg16a/major- + `Commemoration_only` candidate (RG 81), caught by `prop_invariants`' + SAMPLED 200-year property (the default `dune test` run), NOT the + committed exhaustive sweep (which walks in order and would have found + it deterministically at year 1638, not the later, seed-dependent year + the sample happened to draw) — attribution corrected in fix round 1 + (F4). Fix round 1 also found and closed a THIRD settlement channel + `settled_at` still missed (a transferred candidate capped out by + admission limits AT its own target, F1) — unreachable on shipped data, + caught by the same sampled property while mutation-testing the RG 109(f) + privilege. Full account: `.superpowers/sdd/2026-08-12-colitur-rg16a/major- litanies-report.md`. **Rogation Wednesday remains genuinely blocked**, and for the ORIGINAL diff --git a/data/ef/expected-divergences-missalemeum.sexp b/data/ef/expected-divergences-missalemeum.sexp index afc793e..6eeeabd 100644 --- a/data/ef/expected-divergences-missalemeum.sexp +++ b/data/ef/expected-divergences-missalemeum.sexp @@ -215,7 +215,11 @@ REVISED, ef-bvm-saturday task: 395 of the 730 days in this window used to carry (note "30 June, both years in this window (2026-06-30, 2027-06-30): colitur now shows +commemoration-of-st-peter (data/ef/adjustments.sexp's own `Add` directive) where missalemeum shows nothing at all -- not even Peter listed under \"displaced\", meaning missalemeum's own engine never constructs a candidate for this commemoration in the first place, the SAME shape this task found in lectio's own source data (`~/git/projects/lectio/internal/caldata/tridentine-calendar.ini` has no 06-30 companion entry either -- an upstream data gap both reference engines share, not a colitur bootstrap miss). Verdict colitur, the same shape as M1/M3/M8/M10 above (each already \"missalemeum does not implement X\"): the calendarium's own text is unconditional and confirmed on both photographic scans, and colitur's own machinery for the other two RG 110 pairs (25 January, 22 February) already produces the identical shape correctly. Diff shape [Comm_presence] alone: rank/colour/observed-identity all already agreed before this fix (the OBSERVED celebration on both dates is unchanged, `in-commemoratione-sancti-pauli-apostoli`, sanctoral-origin with a resolvable name matching missalemeum's own title exactly on both rows) -- only the new commemoration's PRESENCE is new, so identity comparison is never even reached (this file's own header: identity is only attempted once presence and count already agree). 2, one per year, independently re-derived against the fixture's own two 06-30 rows before being set here, not assumed from the citation's own two-pair symmetry.") (expected_rows 2)) ((id M20) - (citation "RG 109(f) + RG 111(b), Caput XVI \"De Commemorationibus\" (docs/research/rules-register.md §4, both primary-source-verified word for word, all three documents): RG 109(f) lists \"de Litaniis maioribus, in Missa\" in the SAME closed privileged list as (a)-(e); RG 111(b): \"in dominicis II classis, una tantum admittitur commemoratio, scilicet de festo II classis, QUÆ TAMEN OMITTITUR SI COMMEMORATIO PRIVILEGIATA FACIENDA SIT\" -- the II-class-feast commemoration is dropped if a privileged one is due") + (citation "RG 109(f) + RG 111(b), Caput XVI \"De Commemorationibus\", CORROBORATED by RG 434(b) (\"VIII - De diversis Missae partibus\", \"D) De orationibus\", \"I - De orationibus in genere\"), added fix round 1 (F5) -- all primary-source-verified word for word, all three documents (docs/research/rules-register.md §4). RG 109(f): \"de Litaniis maioribus, in Missa\" -- in the SAME closed privileged list as (a)-(e). RG 111(b): \"in dominicis II classis, una tantum admittitur commemoratio, scilicet de festo II classis, QUAE TAMEN OMITTITUR SI COMMEMORATIO PRIVILEGIATA FACIENDA SIT.\" RG 434(b), WORD-IDENTICAL, but explicitly scoped to the MASS (\"post orationem Missae\"), which is exactly where RG 109(f)'s own privilege lives (\"in Missa\"): \"in dominicis II classis, nulla alia admittitur oratio, praeter commemorationem festi II classis, quae tamen omittitur si commemoratio privilegiata facienda sit.\" The second citation answers the objection that RG 111 sits among Office-and-Mass rubrics and might be read as Office-shaped: RG 434 cannot be so read, and says the identical thing.") (verdict colitur) - (note "NEW (ef-major-litanies task). 25 April 2027: RG 80's own transfer condition (25 April = Easter Sunday or Monday) does NOT fire this year, so the Litanies stay on 25 April itself -- which this year happens to be an ordinary II-class Sunday (RG 91 entry 15). St Mark (II class, entry 16) also loses to the Sunday. Two losing candidates, ONE slot (RG 111(b)): colitur admits the Litanies, not Mark; missalemeum's raw fixture row shows the reverse (verified directly against test/fixtures/missalemeum-ef-2026-2027.txt's own 2027-04-25 line -- see [M5]'s own corrected note above for the exact text, which previously mis-transcribed this same row backwards). ADJUDICATED, not merely asserted: {!Precedence_ef.admit}'s [Class2, true] branch already implements RG 111(b)'s privilege-overrides-ordinary clause exactly as quoted above, and this project's own prior work (precedence_ef.ml's \"Fix, Task 16\" comment) already primary-source-verified that same clause independently of this task -- the Litanies winning here is the SAME mechanism, not a special case written for it; St Mark meets the fate any ordinary Class2 commemoration meets when a privileged one is also due (RG 16(a)'s Transfiguration/Sixtus shape is the nearest existing witness, though there the privileged side wins the whole DAY, not merely the commemoration slot). HONESTLY FLAGGED: this is the first real (non-synthetic) data point this codebase has for \"an ordinary Class2 feast and a privileged non-feast commemoration both losing to the identical Sunday\" -- every other witness for this admit branch in test_precedence_ef.ml is hand-built. Read plainly, RG 111(b)'s text supports colitur's outcome here; missalemeum's divergence is consistent with this project's already-documented pattern of RG 108-111 gaps in that oracle (M1, M8, M10 above each already \"missalemeum does not implement X\") -- plausibly one more instance of the same generator not modelling RG 109(f)'s privilege for this rare, single-date observance, not evidence the RG 111(b) mechanism itself is mis-read. Recorded as an adjudicated verdict, not a certainty -- revisit first if a future primary-source pass finds textual grounds narrowing RG 109(f)'s privilege specifically. Identity-gated (test_oracle.ml's own [m20_commemoration_matches]): pins that colitur's own sole admitted commemoration really is [major-litanies].") + (note "NEW (ef-major-litanies task). 25 April 2027: RG 80's own transfer condition (25 April = Easter Sunday or Monday) does NOT fire this year, so the Litanies stay on 25 April itself -- which this year happens to be an ordinary II-class Sunday (RG 91 entry 15). St Mark (II class, entry 16) also loses to the Sunday. Two losing candidates, ONE slot (RG 111(b)/RG 434(b)): colitur admits the Litanies, not Mark; missalemeum's raw fixture row shows the reverse (verified directly against test/fixtures/missalemeum-ef-2026-2027.txt's own 2027-04-25 line -- see [M5]'s own corrected note above for the exact text, which previously mis-transcribed this same row backwards). ADJUDICATED, not merely asserted: {!Precedence_ef.admit}'s [Class2, true] branch already implements RG 111(b)'s privilege-overrides-ordinary clause exactly as quoted above, and this project's own prior work (precedence_ef.ml's \"Fix, Task 16\" comment) already primary-source-verified that same clause independently of this task -- the Litanies winning here is the SAME mechanism, not a special case written for it; St Mark meets the fate any ordinary Class2 commemoration meets when a privileged one is also due (RG 16(a)'s Transfiguration/Sixtus shape is the nearest existing witness, though there the privileged side wins the whole DAY, not merely the commemoration slot). + + SCOPE, made explicit (fix round 1, F5): RG 108 (\"Commemorationes privilegiatae fiunt in Laudibus et in Vesperis necnon in omnibus Missis; commemorationes vero ordinariae fiunt tantum in Laudibus, in Missis conventualibus et in omnibus Missis lectis\") + RG 81 (\"nihil fit in Officio, sed tantum in Missa\") together mean the Litanies' own privileged slot is due IN THE MASS specifically; nothing here computes or asserts a separate OFFICE-level answer for the same day. colitur emits ONE resolved day, not one per liturgical hour -- this adjudication, and every one of the 829 domain-wide Sunday-displacement days it governs, is the MASS answer. Defensible for a Mass-facing engine (this codebase's own stated scope, citations/readings, is Mass-oriented throughout), but stated here rather than left implicit, since it is what the whole adjudication rests on. + + HONESTLY FLAGGED, now closer to certain than first written: this is the first real (non-synthetic) data point this codebase has for \"an ordinary Class2 feast and a privileged non-feast commemoration both losing to the identical Sunday\" -- every other witness for this admit branch in test_precedence_ef.ml is hand-built. First written \"moderate-high, not certain\"; RG 434(b)'s own independent, Mass-specific repetition of RG 111(b)'s exact rule (fix round 1 finding, above) closes the main remaining doubt (that RG 111 might be Office-shaped) and moves this near-certain. missalemeum's divergence remains consistent with this project's already-documented pattern of RG 108-111 gaps in that oracle (M1, M8, M10 above each already \"missalemeum does not implement X\") -- plausibly one more instance of the same generator not modelling RG 109(f)'s privilege for this rare, single-date observance. Identity-gated (test_oracle.ml's own [m20_commemoration_matches]): pins that colitur's own sole admitted commemoration really is [major-litanies].") (expected_rows 1)) diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index e8c05cd..b3c9ab5 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -1615,7 +1615,35 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) 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. *) + 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" -- + WORD-IDENTICAL to the clause above, but 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 ] | [] -> ( -- cgit v1.3 From 72b19098905cd609d87783f6e68311de4f1b61a0 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 21:18:58 +0200 Subject: docs: a confidence raised while its revisit trigger was deleted Five follow-ups from the fix-round re-review, none touching a computed result. M20's note had said "moderate-high, not certain" with a specific revisit trigger attached. The fix round raised the label to "near-certain" and deleted the trigger in the same edit. Upgrading a confidence while removing the condition that would lower it again is the one move this record must not make, so the trigger is restored alongside the higher label: no primary text anywhere names the Major Litanies in a Mass-orations-count worked example, only the general II-class-Sunday rule twice over. RG 434(b) closes the Office-shaped doubt and nothing further. "WORD-IDENTICAL" overstated the relation between RG 111(b) and n. 434(b). Only the trailing "quae tamen omittitur si commemoratio privilegiata facienda sit" is verbatim in both; the openings differ, n. 434(b) recasting the rule into the orations register. The claim was self-refuting -- both texts are quoted adjacent to it -- and the argument never depended on it. Now "identical in its operative clause". And n. 434 is not "a different part of the same document": the running heads put RG 111 under Rubricae generales and n. 434 under Rubricae generales Missalis Romani, two distinct corpora bound in one volume, which is the entire force of the corroboration. The code comment had understated its own point. Also corrects the register's LT line range for n. 434(b) (3574-3576, not 3564-3570; the (b) clause is not in the cited range), and records in calendar.ml the diagnostic that channel (3) trades away: a rite whose transfer_target names a date its own disposition omits used to raise a loud, mislabelled Validate failure and is now silent at the origin. The kernel cannot distinguish that from a deliberate omission without rite knowledge it must not have, so accepting it is right -- but the signal is gone, and that should be written down rather than discovered later. --- data/ef/expected-divergences-missalemeum.sexp | 2 +- lib/kernel/calendar.ml | 15 ++++++++++++++- lib/rites/rite_ef/precedence_ef.ml | 13 ++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/data/ef/expected-divergences-missalemeum.sexp b/data/ef/expected-divergences-missalemeum.sexp index 6eeeabd..96d6405 100644 --- a/data/ef/expected-divergences-missalemeum.sexp +++ b/data/ef/expected-divergences-missalemeum.sexp @@ -221,5 +221,5 @@ REVISED, ef-bvm-saturday task: 395 of the 730 days in this window used to carry SCOPE, made explicit (fix round 1, F5): RG 108 (\"Commemorationes privilegiatae fiunt in Laudibus et in Vesperis necnon in omnibus Missis; commemorationes vero ordinariae fiunt tantum in Laudibus, in Missis conventualibus et in omnibus Missis lectis\") + RG 81 (\"nihil fit in Officio, sed tantum in Missa\") together mean the Litanies' own privileged slot is due IN THE MASS specifically; nothing here computes or asserts a separate OFFICE-level answer for the same day. colitur emits ONE resolved day, not one per liturgical hour -- this adjudication, and every one of the 829 domain-wide Sunday-displacement days it governs, is the MASS answer. Defensible for a Mass-facing engine (this codebase's own stated scope, citations/readings, is Mass-oriented throughout), but stated here rather than left implicit, since it is what the whole adjudication rests on. - HONESTLY FLAGGED, now closer to certain than first written: this is the first real (non-synthetic) data point this codebase has for \"an ordinary Class2 feast and a privileged non-feast commemoration both losing to the identical Sunday\" -- every other witness for this admit branch in test_precedence_ef.ml is hand-built. First written \"moderate-high, not certain\"; RG 434(b)'s own independent, Mass-specific repetition of RG 111(b)'s exact rule (fix round 1 finding, above) closes the main remaining doubt (that RG 111 might be Office-shaped) and moves this near-certain. missalemeum's divergence remains consistent with this project's already-documented pattern of RG 108-111 gaps in that oracle (M1, M8, M10 above each already \"missalemeum does not implement X\") -- plausibly one more instance of the same generator not modelling RG 109(f)'s privilege for this rare, single-date observance. Identity-gated (test_oracle.ml's own [m20_commemoration_matches]): pins that colitur's own sole admitted commemoration really is [major-litanies].") + HONESTLY FLAGGED, now closer to certain than first written: this is the first real (non-synthetic) data point this codebase has for \"an ordinary Class2 feast and a privileged non-feast commemoration both losing to the identical Sunday\" -- every other witness for this admit branch in test_precedence_ef.ml is hand-built. First written \"moderate-high, not certain\"; RG 434(b)'s own independent, Mass-specific repetition of RG 111(b)'s exact rule (fix round 1 finding, above) closes the main remaining doubt (that RG 111 might be Office-shaped) and moves this near-certain -- but NOT to certain, and the revisit trigger stands: no primary text anywhere names the Major Litanies in a Mass-orations-count worked example, only the general II-class-Sunday rule, twice over. RG 434(b) closes the Office-shaped doubt and nothing further. If a future primary-source pass finds textual grounds narrowing RG 109(f)'s privilege specifically, THIS is the entry to revisit first. (That sentence was dropped in the same edit that raised the label to near-certain, and is restored here: upgrading a confidence while deleting its revisit trigger is the one move this record must not make.) missalemeum's divergence remains consistent with this project's already-documented pattern of RG 108-111 gaps in that oracle (M1, M8, M10 above each already \"missalemeum does not implement X\") -- plausibly one more instance of the same generator not modelling RG 109(f)'s privilege for this rare, single-date observance. Identity-gated (test_oracle.ml's own [m20_commemoration_matches]): pins that colitur's own sole admitted commemoration really is [major-litanies].") (expected_rows 1)) diff --git a/lib/kernel/calendar.ml b/lib/kernel/calendar.ml index f2a03a7..cffdf69 100644 --- a/lib/kernel/calendar.ml +++ b/lib/kernel/calendar.ml @@ -384,7 +384,20 @@ let build_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.by_date) {!Precedence.resolution}'s own [observed]/[commemorations]/[omitted] fields -- THREE of {!Precedence.resolution}'s four fields (the fourth, [deferred], denotes a candidate that has NOT yet settled at this date, - by definition, so it is correctly never consulted here). *) + by definition, so it is correctly never consulted here). + A SIGNAL TRADED AWAY, named because it is real (fix-round re-review): + channel (3) accepts both shapes of [omitted] -- the admission cap, and + a rite whose own [disposition] omits the candidate AT the target its + own [transfer_target] named. For the cap this is unambiguously + "settled". For the second it is a judgement: before this change that + shape produced a loud, if mislabelled, [Validate] "unconverged" + failure; now it is silent at the origin and honestly reported at the + target. The kernel cannot tell "the rite deliberately omitted it + there" from "the rite chose a bad target" without rite knowledge it + must not have, so accepting it is the right call -- but the diagnostic + it used to give up is gone. Unreachable in [Rite_ef] today: only the + Major Litanies transfer as [Commemoration_only], and RG 96's search + guarantees a transferred FEAST an unblocked target. *) let settled_at target slug = let _, _, target_resolution = resolve_with_injected rite idx injected target in let matches (c : 'r Precedence.candidate) = diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index b3c9ab5..c9e6a55 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -1627,7 +1627,18 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) documents: "in dominicis II classis, nulla alia admittitur oratio, praeter commemorationem festi II classis, quae tamen omittitur si commemoratio privilegiata facienda sit" -- - WORD-IDENTICAL to the clause above, but explicitly scoped "post + 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 -- cgit v1.3