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
|
open Colitur_kernel
(** The EF lectionary resolution chain. All rubric knowledge about what a day
with no proper falls back to lives here, not in the kernel.
[lectionary] and [commons] are both caller-supplied, not loaded by this
module -- the same reasoning rite_ef.mli's own [context] doc comment
already gives for why the sanctoral {!Colitur_kernel.Layer.t} stays a
separate argument rather than an embedded field: it lets a caller load
data/ef/lectionary.sexp and data/ef/commons.sexp however suits it, and
leaves room for a future diocesan/proper lectionary overlay to attach
without this module changing at all.
An eager filesystem read at module initialisation was tried first and
reverted (fix round 1, coordinator review): [readings] used to close
over a [lectionary] value loaded as a side effect of this module being
LINKED, so `colitur easter <year>` -- which touches no lectionary data
at all -- died at startup the moment data/ef/lectionary.sexp was
missing from a bare `dune build`'s own default target (it was only
present because test/dune's own deps happened to materialise it,
masking the gap in every test run). See the task report for the
reproduction. The Commons follow the same path for the same reason. *)
(** The Commons of the 1962 Missal (Epistle and Gospel citations only) and
the per-saint assignments that route a readingless class-3 feast to one.
Two tables rather than one, because the two facts have different
warrants and different lifetimes: a FORMULARY is read from the Commune
Sanctorum and is the same for every saint sent to it, while an
ASSIGNMENT is read from one saint's own date in the Proprium Sanctorum.
A diocesan overlay adds assignments; it rarely adds formularies.
Assignment is explicit per saint, never inferred: {!Colitur_kernel.Subject.t}
is [Temporal|Saint|Bvm|Lord] and {!Colitur_kernel.Celebration.t} carries
no martyr/confessor/virgin/bishop/abbot classification at all, so there
is nothing to infer one from -- those words appear only inside display
names. A saint with no proper and no assignment gets no Common. *)
module Commons : sig
type t
(** No formularies and no assignments -- the identity for this table, and
what a caller that genuinely has no Commons data should pass. Every
lookup returns [None]; nothing is silently invented. *)
val empty : t
(** Loads from a sexp file. Parse and validation failures come back as
[Error], never as an exception, and never at module-initialisation
time -- the same contract {!Colitur_kernel.Lectionary.load} makes.
[Error] (never a silently-degraded lookup) on: a duplicate common id;
a duplicate assignment for one saint; a formulary with no citations
(indistinguishable downstream from "no Common at all"); and an
assignment naming a common that does not exist (likewise). *)
val load : string -> (t, string) result
(** Builds a table directly, with exactly the validation {!load} applies to
a file (same four [Error]s, same canonical sort). Exposed so that a
caller can construct Commons from something other than a sexp file --
a future diocesan overlay, or a test that needs a table the shipped
data deliberately does not contain. The guard on {!readings}' step 4
is one such case: no shipped assignment names a temporal slug, so the
only way to exercise the guard at all is to build a table that does. *)
val of_tables :
commons:(Slug.t * Citation.t list) list ->
assigned:(Slug.t * Slug.t) list ->
(t, string) result
(** The Commons themselves, canonically sorted by id. *)
val formularies : t -> (Slug.t * Citation.t list) list
(** Saint slug -> common id, canonically sorted by saint. *)
val assignments : t -> (Slug.t * Slug.t) list
end
(** The Common assigned to a saint who has no proper, if any. Exposed for the
golden pins, which must show WHICH Common fired, not merely that two
citations appeared.
Takes the table explicitly for the same reason {!readings} takes
[~lectionary]: the data is the caller's, not this module's. *)
val commons_for : commons:Commons.t -> Slug.t -> Citation.t list option
(** The day's Epistle and Gospel citations, or [].
Four steps, in EXECUTION order 1, 4, 2, 3 (the numbers are the plan's and
are kept as written, so that every "step 3" already recorded in a test
name, comment or report still means the same branch):
- {b Step 1} -- the observed celebration's own proper.
- {b Step 4} -- a saint who is the day's observed office and has no
proper says his assigned Common. Runs before the temporal fallbacks,
not after them: this is the only step in the chain with a direct
primary-source warrant (the Missal names the Mass at each such saint's
own date), and placing it last makes it unreachable on every date in
1583-9999 as well as wrong on the days it would fire. The full
argument, with the measurement behind it, is on the branch itself.
Guarded so it can only ever apply to a SANCTORAL observed office --
a feria, a Sunday, the Triduum and the RG 78 Saturday Office of the
BVM (whose observed celebration is its own temporal office) are
structurally excluded, not merely absent from the data.
- {b Step 2} -- the day's own temporal slug in the lectionary.
- {b Step 3} -- for a weekday whose own slug has no entry, the preceding
Sunday's temporal slug (never its observed one; a Sunday is guarded
out because it has no PRECEDING Sunday to resume, not because
consulting itself would loop -- [readings] is not recursive, see its
own implementation comment).
A day matching none of the four gets []. *)
val readings :
lectionary:Lectionary.t ->
commons:Commons.t ->
observed:Vocab_ef.rank Celebration.t ->
temporal:(Vocab_ef.season, Vocab_ef.rank) Temporal.t ->
date:Date.t ->
temporal_at:(Date.t -> (Vocab_ef.season, Vocab_ef.rank) Temporal.t) ->
Citation.t list
|