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
|