aboutsummaryrefslogtreecommitdiff
path: root/lib/rites/rite_ef/lectionary_ef.mli
blob: 0797c0d68d74d948544459dc9dab93243b7e1b46 (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
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 -- its own id
    ALONGSIDE its citations, not the citations alone: {!readings}' step 4
    needs the id to name which Common fired in the {!Colitur_kernel.Mass_formulary.t}
    it builds. Exposed for the golden pins too, 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 -> (Slug.t * Citation.t list) option

(** The Mass actually said -- which formulary, and how that was decided --
    paired with the day's Epistle and Gospel citations. [(None, [])] when
    none of the four steps below answers.

    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). Each step's own
    {!Colitur_kernel.Mass_formulary.source} is built at the point the step
    decides, not re-derived afterwards from the citations it returns:

    - {b Step 1} -- the observed celebration's own proper.
      {!Colitur_kernel.Mass_formulary.Proper}, [said] the observed slug.
    - {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.
      {!Colitur_kernel.Mass_formulary.Common}, [said] the Common's own id.
    - {b Step 2} -- the day's own temporal slug in the lectionary.
      {!Colitur_kernel.Mass_formulary.Own_slug}, [said] that slug. The RG
      309(a) Saturday votive Mass of Our Lady also answers here (structurally,
      not as a fifth numbered step), but is tagged
      {!Colitur_kernel.Mass_formulary.Votive} instead, not [Own_slug]: RG
      431(e) ("in Missis votivis IV classis ... de B. Maria Virg. quae in
      sabbato celebrantur", docs/research/LT.txt) classifies it, in the
      Missal's own words, as a "Missa votiva" said IN PLACE of the day's own
      office's Mass, the office (RG 78) itself being kept, corroborated by RG
      309(a)'s own "iuxta temporum diversitatem" naming the five seasonal
      Masses RG 431(e) speaks of. (CORRECTED, fix round 2: this previously
      also cited the Latin Mass Society Ordo as witnessing the tag directly,
      reading its "V Mass of BVM" as an abbreviation for "votive" -- the
      coordinator's own misreading. The Ordo's "I"-"V" are ROMAN NUMERALS
      naming which of the five seasonal Masses is said, not a marker of the
      Mass's kind; see the implementation comment on this branch for the
      full account and the validation use this correction still leaves
      available for Task 6.) [said] is still that slug: only the source
      constructor differs from an ordinary Step 2 lookup, because the guard
      that reaches this branch only ever fires when the observed celebration
      already IS the day's own temporal office -- see the implementation
      comment on that branch.
    - {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).
      {!Colitur_kernel.Mass_formulary.Preceding_sunday}, [said] that
      Sunday's temporal slug.

    A day matching none of the four gets [(None, [])]. *)
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) ->
  Mass_formulary.t option * Citation.t list