(** 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 -> ('r candidate * privilege) 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. 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. *) } (** 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