summaryrefslogtreecommitdiff
path: root/lib/kernel/precedence.mli
blob: a0b4b1ae44fcbc1d3af3cb1c918b1d8db04f81bd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
(** 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