aboutsummaryrefslogtreecommitdiff
path: root/lib/kernel/precedence.mli
blob: 05022c513b4e9b6b85b060e576c5b5e3bb4dd45f (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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
(** 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