(** The rite-parameterised resolver: RG 91 says who wins, RG 92-95 says what happens to the loser, RG 108-111 says how many commemorations are admitted. Three separate rite-supplied functions, because the loser's fate depends on the loser's own rank, not the winner's -- conflating them would resist extension to a second rite. *) (** Which of the day's two office streams a candidate came from. *) type origin = Temporal | Sanctoral [@@deriving sexp] (** RG 111: an admitted commemoration's own standing, distinct from its rank. *) type privilege = Privileged | Ordinary [@@deriving sexp] (** What becomes of a losing candidate. *) type disposition = | Omit (** yields with no trace in the day's celebration *) | Commemorate of privilege (** kept as a commemoration of the observed day *) | Transfer (** moved to the next free day (RG 92-95) *) | Repose (** kept only in a votive/private sense; not commemorated today *) [@@deriving sexp] (** A celebration together with the office stream it was drawn from. Parameterised by the rite's rank type only, matching {!Celebration.t}. *) type 'r candidate = { cel : 'r Celebration.t; origin : origin } [@@deriving sexp] (** The day a resolution is computed for. Parameterised by the rite's season type only -- a context has no rank of its own. *) type 's context = { date : Date.t; season : 's; weekday : Date.weekday } (** The rite's three resolution functions. *) type ('s, 'r) rules = { band : 's context -> 'r candidate -> int; (** RG 91: orders candidates for the day; lower wins. *) disposition : winner:'r candidate -> loser:'r candidate -> disposition; (** RG 92-95: the loser's fate, which depends on the loser's own rank. *) admit : observed:'r candidate -> temporal:'r candidate -> ('r candidate * privilege * int) list -> ('r candidate * privilege) list; (** RG 108-111: how many commemorations are admitted, and in what order; anything filtered out here is recorded in {!resolution.omitted}, not dropped. Each input triple's trailing [int] is that candidate's own {!band} value, computed once by {!resolve} (RG 113: "in admittendis et ordinandis aliis commemorationibus, servetur ordo tabellae praecedentiae" -- ADMITTING and ORDERING commemorations is governed by the same table-of-precedence order {!band} already supplies for picking the day's own winner; docs/research/rules-register.md §4 "Commemorations"). Supplied rather than left for [admit] to compute itself because [admit] has no [context] (date/season/weekday) of its own -- {!resolve} already holds one and calls {!band} with it for every candidate regardless. A rite's [admit] is free to ignore the value entirely (e.g. fall back to [Vocab.rank] alone), the same as it may ignore [temporal] below. [temporal] is {!resolve}'s own [~temporal] argument, passed through unchanged -- the day's temporal-cycle candidate, regardless of whether it won. Fix round 1 (RG16(a) task): before this, a rite's [admit] could only infer properties of the CIVIL DAY (chiefly "is this a Sunday", RG 111(b)'s own two-tier admission rule) from [observed]'s own fields -- a proxy that breaks the moment something OTHER than the day's own temporal candidate can be [observed], the exact shape RG 16(a) introduces (a Feast of the Lord standing in the impeded Sunday's place "cum omnibus iuribus et privilegiis", RG 91 entry 14). This is NOT a kernel definition of "Sunday" -- the kernel does not gain any rite-specific knowledge by this parameter, it only threads through a value {!resolve} already holds; a rite's own [admit] is free to ignore it entirely, the same as [observed]. OBLIGATION ON THE IMPLEMENTATION, not enforced by this type: every candidate this function returns must be a value taken UNCHANGED from its input list, never rebuilt (e.g. via a [{ c with ... }] record update, even one that copies every field back unchanged). {!resolve}'s own [omitted] accounting distinguishes an admitted candidate from a dropped one by PHYSICAL equality ([==]) on the candidate value, not structural equality -- a rebuilt record is [=] to the original but not [==], so {!resolve} cannot match the rebuilt copy against the original it was given. The celebration then surfaces TWICE in the same day's result -- once in {!resolution.commemorations} (the rebuilt copy, admitted) and once in {!resolution.omitted} (the original, which nothing in the admitted set matches). One admission, double-reported, silently rather than raising. This obligation previously lived only in one rite's own module documentation (Rite_ef.Precedence_ef.admit); stated here because this signature -- not any one rite's implementation of it -- is what an author of the next rite reads. *) vigil_feast : 'r candidate -> Slug.t option; (** The feast this candidate is a VIGIL OF, when the rite subjects that vigil to omission because its feast did not keep its own day; [None] for every other candidate, which is what a rite with no such rule returns unconditionally. Exists for RG 33's third omission trigger -- "vel si festum cui praemittitur in alium diem transferri aut ad commemorationem reduci contingat", "or if the feast it precedes happens to be transferred to another day or reduced to a commemoration". Both halves of that clause reduce to ONE observable question, which is why this hook returns a slug rather than a verdict: is the named feast the OBSERVED office on the following day? {!Calendar} asks it and suppresses the vigil when the answer is no. A feast transferred away (RG 96) and a feast outranked into a bare commemoration (RG 94) both fail that test; so does a feast omitted outright, which RG 33 does not enumerate but which is strictly the stronger case. WHY THE RITE NAMES THE FEAST. The kernel could not infer it. RG 34 fixes the vigil on the day BEFORE its feast, so the date is known, but nothing in {!Celebration.t} links the two and the slugs do not reliably derive from one another -- in the EF's own shipped data only two of five vigils ("ef-ascension-vigil"/"ef-ascension", "vigil-of-sts-peter-paul"/"sts-peter-paul") share a stem, while "vigil-of-st-lawrence" precedes "lawrence" and "vigil-of-the-assumption" precedes "assumption-of-the-blessed-virgin-mary". Deriving the feast by string surgery would be wrong for three of the five. Asking "is a Class1 sanctoral office observed tomorrow?" would be a PROXY, and would fire on a day where some UNRELATED I-class feast had transferred in on top of the real one -- the vigil's feast would be absent and the vigil wrongly kept. CALLED ONCE PER CANDIDATE PER DAY, and the resolution of the following day that {!Calendar} performs to answer it does NOT itself apply this rule. That is not an approximation: a vigil is a candidate only on its own day, never on its feast's, so suppressing it cannot change what is observed the day after. The check is therefore a single pass with no fixed point and no recursion -- unlike RG 96's transfers, which genuinely need one. *) } (** The outcome of resolving one day's candidates. *) type 'r resolution = { observed : 'r candidate; commemorations : ('r candidate * privilege) list; deferred : 'r candidate list; omitted : ('r candidate * string) list; (** each with a reason *) } (** Total: the temporal candidate is passed separately, so there is no empty-candidate case. Ties break on slug, so the result never depends on input order. A [Commemoration_only] celebration is held out of the contest and can never be [observed]. Every input candidate appears exactly once in [observed], [commemorations], [deferred] or [omitted] — nothing is dropped silently. *) val resolve : ('s, 'r) rules -> 's context -> temporal:'r candidate -> sanctoral:'r candidate list -> 'r resolution (** RG 111(a) (EF; docs/research/rules-register.md; LT.txt, grep "Ratio admittendi commemorationes"): {i "in diebus liturgicis I classis et in Missis in cantu non conventualibus, nulla admittitur commemoratio, praeter unam privilegiatam"} -- on a liturgical day of the first class, AND at any non-conventual SUNG Mass regardless of the day's own class, at most ONE commemoration is admitted, and only if it is privileged. (b)/(c)/(d), immediately following in the same rubric, give the wider caps -- two ordinary/privileged commemorations depending on class -- that apply everywhere else; {!rules.admit} already computes exactly that wider set, which is why this function's own INPUT is the admitted LOW-MASS list, not a fresh resolution: (a) is not a distinct admission RULE with its own candidate-ranking logic, it is a narrower CAP applied afterwards to the identical admitted, precedence-ordered list -- "at most one, and it must be privileged" is exactly "keep the first admitted entry, if any, that is privileged", nothing else in the list can ever outrank it (RG 113: admission order already follows the rite's own table of precedence, {!rules.admit}'s own citation). Deliberately a pure post-hoc DERIVATION over {!Liturgical_day.t.commemorations}, not a second stored field: the input list is already validated (privilege-tagged, admission-capped); this function adds no new information and can regress in no way the input list itself could not already regress, so a second field would only create a second place for the same fact to drift out of sync with the first -- the identical reasoning {!Liturgical_day.t.temporal}'s own "embedded, not flattened" comment gives for a different field. Exposed here, at the kernel level, rather than left for an output layer to reimplement: the filter is small but the RULE it encodes (RG 111(a)) is not obvious from the type alone, and a caller (the [rubrics] CLI column, a future template, a differential test) should name the rubric, not re-derive "privileged commemorations, capped at one" for itself. SCOPE: "non conventualibus" is read literally -- this models the ordinary (non-conventual) sung Mass only. RG 111(a)'s own text implies a CONVENTUAL sung Mass keeps the wider (b)/(c)/(d) caps even when sung (a choir-obligation distinction), but this engine has no concept of "conventual" at all (no community/choir dimension anywhere in {!Celebration.t} or {!Liturgical_day.t}), so this function's result should be read as "what a normal parish/private Low OR sung Mass admits", never "what every sung Mass, everywhere, admits" -- a documented scope limit, not an oversight. *) val sung_mass_commemorations : ('a * privilege) list -> ('a * privilege) list