diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 11:38:35 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 11:38:35 +0200 |
| commit | e19adb9c7ea369cafe76e7eff50e9248a4a954d0 (patch) | |
| tree | 57824e121606286ebdf8fff68439986dd3300eae /lib/rites/rite_ef/precedence_ef.mli | |
| parent | 0506388da160a15ceddb0cea1a697d737103d5c4 (diff) | |
| parent | 36df2bd0af9a555a09334e14b0d89118be1dbbc0 (diff) | |
| download | colitur-e19adb9c7ea369cafe76e7eff50e9248a4a954d0.tar.gz colitur-e19adb9c7ea369cafe76e7eff50e9248a4a954d0.zip | |
Merge branch 'ef-plan3': Plan 3, the EF resolution engine
Adds the rite-parameterised Precedence resolver, Liturgical_day, Rite,
and Calendar (year as the primitive, because transfers need whole-year
knowledge), the full EF precedence ruleset (RG 91's 28-entry table,
occurrence RG 92-95, commemorations RG 108-111, transfers RG 96-98),
322 bootstrapped sanctoral entries, colitur day <year>, and validation
layers 3-5.
Layer 3 diffs 16801 days against lectio; layer 4 diffs 730 days against
missalemeum; layer 5 pins ~30 dates on the known-tricky years. Both
comparison layers carry cited allow-lists that name the governing RG
paragraph and which engine is right.
The oracle layer earned its place immediately: Holy Thursday was violet
in colitur and lectio alike, because colitur's data was bootstrapped
from lectio and both carried the same error. Only an independent source
could see it. RG 128(b) and RG 122 name it white.
Diffstat (limited to 'lib/rites/rite_ef/precedence_ef.mli')
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.mli | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli new file mode 100644 index 0000000..55f947e --- /dev/null +++ b/lib/rites/rite_ef/precedence_ef.mli @@ -0,0 +1,260 @@ +(** RG 91's Table of Precedence for the EF (1962) rite: ranks any candidate + for a given day by its RG 91 entry number. See + docs/research/rules-register.md §4, whose 28-entry transcription this + module follows line by line. *) + +open Colitur_kernel + +(** The universal (base) sanctoral layer's {!Celebration.t}.layer id. A + Sanctoral-origin candidate whose layer is anything else is an overlay: + "proper" (RG 91 entries 12, 19, 23) unless its layer id also carries + {!indult_prefix} ("indult", entries 13, 20). This id and the prefix are + colitur's own data-modelling convention, not an RG citation -- RG 91 + prescribes the ranking, not a machine encoding for it. Whichever task + loads the real EF sanctoral base layer and its overlays must either + reuse these two constants or this classifier will misfile them. *) +val universal_layer : string + +(** See {!universal_layer}. *) +val indult_prefix : string + +(** Slug suffix marking a celebration as a vigil (RG 91 entries 21, 26), + e.g. "ef-ascension-vigil" -- colitur's own temporal-cycle convention + (rite_ef/temporal_ef.ml's [named]). Also colitur's own convention, not + an RG citation, exposed for the same reason as {!universal_layer}. See + {!vigil_prefix} for the sanctoral data's own, different convention: a + vigil can arrive named either way, and {!band}/{!disposition} must + recognise both. *) +val vigil_suffix : string + +(** Slug prefix marking a celebration as a vigil, e.g. "vigil-of-st-lawrence" + -- the sanctoral data's own convention (data/ef/sanctoral.sexp, adopted + verbatim from lectio's naming, per spec §4.4's "slugs are lectionary keys, + not re-derived"). Also colitur's own convention, not an RG citation -- + see {!universal_layer}. Task 10 bootstrapped four real sanctoral vigils + named this way (St Lawrence 08-09, Sts Peter & Paul 06-28, the Assumption + 08-14, the Nativity of St John the Baptist 06-23; a fifth, Christmas, is + suppressed as a duplicate of the temporal cycle's own "ef-nativity-vigil" + -- see data/ef/adjustments.sexp), none of which end in {!vigil_suffix}: + without this prefix also being checked, {!band} would misfile all four at + 16/24 (an ordinary feast of the same rank) instead of RG 91's 21/26, and + RG 33's vigil omission ({!disposition}'s [is_vigil] test, the same + predicate) would never fire for them either -- two rubrics silently + broken for four celebrations, exactly what Task 7's review predicted + when it asked for {!vigil_suffix} to be exposed. *) +val vigil_prefix : string + +(** Slug prefixes marking a celebration as one of RG 91 entry 18's three + Ember-day sets (Advent, Lent, September -- the Pentecost/Whitsun set is + I class and matched by entry 10 before this is ever consulted). Also + colitur's own convention mirroring rite_ef/temporal_ef.ml's own "ef-<set> + -ember-<day>" slug format, not re-derived from first principles; exposed + so a rename of that format has somewhere to be caught other than a + silently-wrong entry 18. *) +val ember_prefixes : string list + +(** Returned for a candidate shape RG 91's 28-entry table has no row for -- + e.g. a [Class1] vigil that is not the Nativity or Pentecost (entries 5, + 9 are the only I-class vigils the table names), or a [Class4] candidate + also marked as a vigil. Deliberately outside 1..28 and larger than any + real entry, so an unclassified candidate can never win an occurrence + contest by accident; a caller that sees it back knows the shape needs a + new rule, not a silently wrong one. *) +val unclassified : int + +(** [band ctx c]: RG 91's Table of Precedence. Returns the table's own entry + number -- I class 1-13, II class 14-21, III class 22-26, IV class 27-28; + lower wins (see {!Precedence.rules.band}) -- EXCEPT where the table's own + text states an exception: entry 8 (All Souls) reads "yields to an + occurring Sunday", so on a Sunday this returns a value that + loses to entry 15 rather than the literal integer 8 (see the comment on + entry 8 in precedence_ef.ml for the exact value and why). Total over + every candidate {!Precedence.resolve} or {!Calendar} can construct, + including shapes the 1962 table itself does not describe (see + {!unclassified}). *) +val band : Vocab_ef.season Precedence.context -> Vocab_ef.rank Precedence.candidate -> int + +(** RG 33's marker: every Sunday slug this rite's temporal cycle produces + (temporal_ef.ml's [named] and [sunday_slug]) contains this substring; + nothing else {!band} classifies does. Also colitur's own convention, not + an RG citation -- see {!universal_layer} -- exposed for the same reason + as {!vigil_suffix}: a rename of temporal_ef's Sunday-slug format has + somewhere to be caught other than a silently-wrong RG 33 disposition. *) +val sunday_marker : 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 {!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); + - 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] + when the winner is any Sunday ({!sunday_marker}) or itself [Class1] + (RG 33 -- entirely omitted, not merely commemorated). A [Class1] + vigil (Nativity, Pentecost) is outside RG 33 entirely -- RG 30 makes + it preferred to any feast whatsoever, so a real one can never reach + this function as a [loser] in the first place (see the .ml's own + comment on [is_omissible_vigil] for the full argument); + - any other [Class1] loser that is NOT a Sunday ({!sunday_marker}) is + [Transfer] (RG 95 -- only I-class FEASTS have the right of + translation; RG 91's own table lists Sundays as a separate row, entry + 6, from feasts, entries 11-13, so a Sunday is never a "feast" in RG + 95's sense and does not transfer even when impeded by a higher + I-class day. This is also what moves All Souls, RG 91 entry 8, once + it loses to an occurring Sunday -- WHERE it lands is + {!Rite.t.transfer_target}'s job, not this function's); + - a TEMPORAL-origin, non-vigil loser of [Class4] is [Omit] (RG 26, + "Caput IV, De feriis" -- "every feria not named in [RG 23-25] is IV + class ... and IV-class ferias are NEVER commemorated." A SEPARATE + rule from RG 109's honour-class one immediately below, keyed on + ferial CLASS rather than on RG 109's privilege letters: RG 109 sorts + commemorations that already exist into honour classes (RG 108's + differing liturgical hours), it does not itself decide which offices + have the right to be commemorated at all -- that is Caput IV's own + business. A SANCTORAL loser of the same rank is unaffected by this + branch (the [is_temporal] guard): RG 21 defines "feria" as any + weekday, never a saint's day, and RG 111(c)/(d) admit an "ordinary" + commemoration of a losing SAINT freely, with no such class-keyed + gate); + - everything else -- including an impeded I-class Sunday, and a + SANCTORAL loser of any rank below I class -- is [Commemorate], + carrying its real RG 109 privilege (see {!admit} below); RG 109(a) + lists "of a Sunday" as a privileged commemoration category precisely + because an impeded Sunday stays put rather than moving to another + day, and RG 24/25 make a losing II- or III-class FERIA's own + commemoration mandatory when impeded, not merely eligible. + + Total over every winner/loser pair {!Precedence.resolve} or {!Calendar} + can construct: [Vocab_ef.rank] (RG 8) and {!Celebration.status} are both + closed variants, and the five cases above -- an if/else-if chain ending + in the unconditional [Commemorate] catch-all -- exhaust every value + those two fields can take between them; there is no sixth, + "unclassified" case the way {!band} needs one, because this function's + own return type has no such slot to fall into by accident. *) +val disposition : + winner:Vocab_ef.rank Precedence.candidate -> + loser:Vocab_ef.rank Precedence.candidate -> + Precedence.disposition + +(** Slug prefix marking a celebration as one of RG 91 entry 17's days within + the Octave of the Nativity (29-31 Dec -- 26-28 Dec are Stephen, John, the + Innocents, sanctoral, never this prefix). Also colitur's own convention + mirroring rite_ef/temporal_ef.ml's own "ef-nativity-octave-day-%d" slug + format, not an RG citation -- see {!universal_layer} -- exposed for the + same reason as {!vigil_suffix}: a rename of that format has somewhere to + be caught other than a silently-wrong RG 109(c) privilege. *) +val nativity_octave_prefix : string + +(** The September set of {!ember_prefixes}, broken out on its own because RG + 109(d) (§4, "Commemorations") privileges September Ember days under its + own name, and September sits outside RG 109(e)'s three named seasons + (Advent, Lent, Passiontide) under any reading of that clause -- NOT + because the Advent and Lent Ember sets need excluding from (e), which + they do not: (e)'s own bare text privileges them too, the same as any + other Advent/Lent feria (see {!Precedence_ef.privilege_of}'s own (e) + comment in the .ml for the full argument, corrected fix round 1). + {!ember_prefixes} is built from this constant, not a duplicated literal, + so the two cannot silently drift apart. *) +val september_ember_prefix : string + +(** [admit ~observed comms]: RG 108-111 (docs/research/rules-register.md §4, + "Commemorations"). How many of [comms] -- each already tagged with its + real RG 109 privilege by {!disposition} -- RG 111 admits, and which, + given the day actually observed: + - [observed] a [Class1] day: none, except at most one privileged + commemoration (the highest-dignity one, if several are due) -- an + ordinary one is never admitted here, no matter how many are due; + - [observed] a [Class2] Sunday (its slug carries {!sunday_marker}): one, + subject to TWO conditions, not one -- (i) a privileged commemoration, + whenever due, categorically takes the day's one slot over any + ordinary one, not by comparing dignity, so an ordinary commemoration + that would otherwise win on dignity is still dropped; (ii) failing + that, the slot is reserved for a [Class2] candidate SPECIFICALLY + ("de festo II classis", RG 111(b)'s own wording -- a RANK FLOOR, not + "whichever ordinary candidate has the best dignity"): a III- or + IV-class ordinary loser has no standing for this slot at all and is + admitted nothing, even when it is the only candidate due; + - [observed] any other [Class2] day: one, by dignity alone -- no + privilege override and no rank floor, unlike the Sunday case + immediately above; + - [observed] a [Class3] or [Class4] day: at most two, by dignity alone. + + "Dignity" here is [Vocab_ef.rank] (RG 8's four classes), NOT {!band}'s + finer RG 91 entry number -- {!band} needs a [context] (date/season/ + weekday) this function does not receive (see {!Precedence.rules.admit}). + Ties break on slug, matching {!Precedence.compare_by}, so the result + never depends on the order [comms] arrives in. + + Every candidate this returns is a value taken unchanged from [comms], + never rebuilt: {!Precedence.resolve}'s own [dropped]/[omitted] + accounting tells an admitted candidate from a dropped one by physical + equality on the candidate value, so anything this function admitted + stays admitted, and anything it did not is reported in + {!Precedence.resolution.omitted}, never silently lost. Total: every + [Vocab_ef.rank] is one of the four cases above, and every branch is + itself total over an empty or arbitrarily long [comms]. *) +val admit : + observed:Vocab_ef.rank Precedence.candidate -> + (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list -> + (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list + +(** The Annunciation's own bootstrapped slug (data/ef/sanctoral.sexp, Task + 10), reused verbatim by {!transfer_target} to recognise RG 96's named + exception. Not an RG citation -- see {!universal_layer} -- exposed so a + future re-bootstrap that renames the slug has somewhere to be caught + other than a silently-wrong transfer target. *) +val annunciation_slug : string + +(** [transfer_target c origin occupant]: RG 96 (docs/research/rules-register + .md §4, "Transfer/translation") -- where an impeded I-class feast, once + {!disposition} has decided it is [Transfer]-disposed, is placed. This + *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 96's own named exception (Attamen (a), primary-source-verified -- + see {!annunciation_slug}'s comment for the Latin and the register's own + correction note): for the Annunciation specifically, IF that general + target would fall after Easter Sunday itself ("quando est transferendum + post Pascha" -- when it is to be transferred past Easter), the + Annunciation is placed instead at the Monday after Low Sunday (its + [sedes propria]), searching onward from there only if that day is + itself occupied by a blocking class. The exception is CONDITIONAL, not + unconditional: an Annunciation impeded for a reason that resolves + BEFORE Easter (e.g. an ordinary Lent Sunday with a free feria the next + day) takes the general target like any other I-class feast. Operationally + the condition holds exactly when 25 March falls close enough to Easter + that the general walk crosses it -- concretely, when 25 March itself + falls within Holy Week or Easter Week. + + Total, terminating, and its result is always strictly later than + [origin] -- {!Colitur_kernel.Rite.t}.transfer_target's own obligations, + which {!Colitur_kernel.Calendar}'s placement pass relies on and its own + round guard does not itself enforce (calendar.ml's [place_transfers] + bounds ROUNDS across a whole year, not one call's internal walk). + Terminating by a structural bound on the internal walk (max 400 days, + an engineering ceiling, not an RG citation -- see the .ml) AND a guard + at {!Colitur_kernel.Date}'s own domain ceiling (31 December 9999, + beyond which probing [occupant] can itself raise -- see the .ml's + [domain_max_date]), not by an argument about the real 1962 calendar's + own structure, so a rite/data shape this function has not anticipated + fails FINITELY rather than hanging or crashing the caller. Strictly + later than [origin]: the general search starts at [origin + 1] and only + ever advances forward from there; the Annunciation's own alternate + starting point is provably later than 25 March for every representable + year (Easter's documented range, register §0) -- see the .ml for the + full argument. *) +val transfer_target : + Vocab_ef.rank Precedence.candidate -> Date.t -> (Date.t -> Vocab_ef.rank Celebration.t) -> Date.t |
