aboutsummaryrefslogtreecommitdiff
path: root/lib/rites/rite_ef/lectionary_ef.mli
blob: caefc749680d3e56d9d42d7ca64bc2082eec44c1 (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
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