From 1299815ea03c2b1e4ff19d92d32e386fec24f4e4 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 15 Aug 2026 01:40:54 +0200 Subject: ef(lectionary): the Commons, chain step 4 Fifteen class-3 saints had no readings anywhere in colitur -- the 6 March - 5 April cluster whose RANKS lectio's generator fix of 2026-08-12 restored without backfilling readings, and lectio has no Commons concept to have backfilled them from. They are exactly the fifteen `status Feast` entries in data/ef/sanctoral.sexp carrying `(citations ())`. The Missal splits them eight/seven, and NOT as the plan predicted. It expected propers for Benedict, Patrick, Thomas Aquinas and Gregory the Great; only Thomas Aquinas has one. Benedict ("Missa Os iusti, de Communi Abbatum"), Patrick ("Missa Statuit, de Communi Confessoris Pontificis I loco") and Gregory the Great ("Missa Si diligis me, de Communi unius aut plurium Summorum Pontificum") are all sent to a Common, with only Oratio, Secreta and Postcommunio printed at the date. All three reversals verified on the page images (pp. 488, 492) as well as in both OCR text layers. PROPERS (data/ef/adjustments.sexp, `Edit ... Set_citation`): thomas-aquinas 7 Mar Sap 7:7-14 / Mt 5:13-19 john-of-god 8 Mar Eccli 31:8-11 / Mt 22:34-46 forty-holy-martyrs 10 Mar Hebr 11:33-39 / Luc 6:17-23 cyril-of-jerusalem 18 Mar Eccli 39:6-14 / Mt 10:23-28 gabriel-the-archangel 24 Mar Dan 9:21-26 / Luc 1:26-38 john-damascene 27 Mar Sap 10:10-17 / Luc 6:6-11 john-of-capistrano 28 Mar Sap 10:10-14 / Luc 9:1-6 francis-of-paola 2 Apr Philipp 3:7-12 / Luc 12:32-34 COMMONS (data/ef/commons.sexp), each quoting the Missal's own instruction at the saint's date: sts-felicitas-perpetua 6 Mar non Virginum I Eccli 51:1-8,12 / Mt 13:44-52 frances-rome 9 Mar non Virginum II Prov 31:10-31 / Mt 13:44-52 gregory-the-great 12 Mar Summ. Pontificum 1 Petri 5:1-4,10-11 / Mt 16:13-19 patrick 17 Mar Conf. Pont. I Eccli 44:16-27;45:3-20 / Mt 25:14-23 benedict 21 Mar Abbatum Eccli 45:1-6 / Mt 19:27-29 isidore-of-seville 4 Apr Doctorum 2 Tim 4:1-8 / Mt 5:13-19 vincent-ferrer 5 Apr Conf. non Pont. I Eccli 31:8-11 / Luc 12:35-40 Every value read in three independent places -- both OCR text layers (two DIFFERENT printings, whose Commune Sanctorum pagination differs by 1-2, so the data keys on heading plus Mass incipit and never on the bracketed page number) and the rendered page images. That caught a genuine typographical error in one printing: it gives St Gabriel's Gospel as "Luc. 2, 26-38" (on the page image, so not an OCR artefact). Luke 1:26-38 is settled by the other printing, by the pericope text in both, and by the SAME edition citing the identical pericope correctly two pages later at the Annunciation. Step 4 EXECUTES SECOND (order 1, 4, 2, 3), not last as the plan sketched. Measured: every day on which one of the fifteen is actually the observed office already has a non-empty step-2 or step-3 answer waiting, so a step 4 placed after the ferial fallback is unreachable on every date in 1583-9999 -- and wrong where it would fire (2038-03-06 would emit Septuagesima II Saturday's Mass on a day whose office is Sts Perpetua and Felicity). Step 4 is also the only step in the chain with a direct primary-source warrant: the Missal names the Mass at each such saint's own date, while steps 2 and 3 rest only on lectio's observed behaviour. Guarded so it can never divert a day whose observed office IS its temporal office -- ferias, Sundays, the Triduum and the RG 78 Saturday Office of the BVM (which deliberately shares the ferial slug) are structurally excluded. Step NUMBERS are unchanged so every "step 3" already written down still names the same branch. The Commons are caller-supplied, like the lectionary: `Rite_ef.context` gains a REQUIRED `~commons`, not an optional one -- a silently-defaulted empty table would give a rite whose class-3 saints quietly lose their Mass, and nothing in layers 3-5 compares citations, so that loss would be invisible. `Commons.load` rejects the four defects indistinguishable downstream from "this saint has no Common": duplicate common id, duplicate assignment, empty formulary, assignment naming a common that does not exist. KNOWN LIMITS, recorded rather than papered over. There is no oracle: lectio has no readings for these feasts and missalemeum's 2026-2027 window never observes one of the fifteen. Eight of the fifteen are NEVER the observed office anywhere in the domain (10-28 March always falls to a Lenten or Passiontide feria that outranks a III-class feast), so they have no end-to-end witness and are covered by dedicated table tests instead. The Commune Sanctorum's own opening rubric permits any Epistle or Gospel of a Common in any Mass of that Common unless one is assigned, so what is emitted is the reading PRINTED WITH the named formulary, not the only lawful one; labelled alternatives (e.g. the widow's Epistle 1 Tim 5:3-10, squarely available to St Frances of Rome) are noted in the data and deliberately not encoded. The BVM Common is NOT authored: the RG 78 Saturday Office shares the ordinary ferial slug, so assigning a Common to it would hand the BVM Mass to every feria sharing that slug; routing it needs a subject-keyed lookup and a season-keyed choice among the Missal's five "Missae de sancta Maria in sabbato" -- new behaviour, not new data. It currently emits its feria's Mass, now pinned as such. Blast radius, measured (git archive HEAD binary vs HEAD, full 1583-9999 `colitur day` sweep, diffed): ZERO differing lines, 3 074 246 lines each side, byte-identical -- the CLI prints no citations, so this change is invisible to layers 2-5 by construction, and the sanctoral overlay perturbed nothing else. 357 tests green (344 before), 358 with the exhaustive sweep. Mutation-tested six ways, all reverted after confirming: relegating step 4 to last reddens exactly the four step-4 pins and nothing else; corrupting a Common's citation, dropping an assignment, dropping a proper Edit, and corrupting either an unreachable proper or an unreachable Common each redden their own pins. The load-bearing assertion is stated over the loaded layer rather than a hard-coded list: every sanctoral entry that can ever BE the observed office now has either a proper or an assigned Common -- fifteen failures before, zero after, and a future re-bootstrap adding a readingless class-3 feast fails there instead of silently emitting its feria's Mass. --- lib/rites/rite_ef/lectionary_ef.ml | 263 ++++++++++++++++++++++++++++-------- lib/rites/rite_ef/lectionary_ef.mli | 98 ++++++++++++-- lib/rites/rite_ef/rite_ef.ml | 29 ++-- lib/rites/rite_ef/rite_ef.mli | 17 ++- 4 files changed, 322 insertions(+), 85 deletions(-) (limited to 'lib') diff --git a/lib/rites/rite_ef/lectionary_ef.ml b/lib/rites/rite_ef/lectionary_ef.ml index 37ec1d8..2658cd3 100644 --- a/lib/rites/rite_ef/lectionary_ef.ml +++ b/lib/rites/rite_ef/lectionary_ef.ml @@ -1,12 +1,116 @@ open Colitur_kernel +(* The Commons of the 1962 Missal, plus the per-saint assignments that route + a readingless class-3 feast to one. Data only -- every value in the + shipped file is transcribed from the Missal and carries its own source + citation there (data/ef/commons.sexp). This module knows the SHAPE and + the invariants, never the values. + + Caller-supplied, exactly as [lectionary] is, and for the same reason + (see this module's own .mli): a rite module that reads the filesystem as + a side effect of being linked breaks every caller that touches none of + its data. *) +module Commons = struct + open Sexplib0.Sexp_conv + + (* [commons]: a Common's id -> the Epistle and Gospel PRINTED WITH the + named Mass formulary. [assigned]: a saint's slug -> the Common his own + day sends him to. Two tables, not one, because the same Common serves + several saints and the two facts have different warrants -- the + formulary is read from the Commune Sanctorum, the assignment from the + saint's own date in the Proprium Sanctorum. *) + type t = { + commons : (Slug.t * Citation.t list) list; + assigned : (Slug.t * Slug.t) list; + } + [@@deriving sexp] + + let empty = { commons = []; assigned = [] } + let formularies t = t.commons + let assignments t = t.assigned + + (* Same discipline as [Colitur_kernel.Lectionary.of_entries]: canonically + sorted, and a duplicate key is an [Error] naming it rather than a + silently-shadowed second answer. *) + let sorted_by_slug xs = List.stable_sort (fun (a, _) (b, _) -> Slug.compare a b) xs + + let first_dup xs = + let rec go = function + | (a, _) :: ((b, _) :: _ as rest) -> if Slug.equal a b then Some a else go rest + | _ -> None + in + go xs + + let of_tables ~commons ~assigned = + let commons = sorted_by_slug commons and assigned = sorted_by_slug assigned in + match first_dup commons with + | Some s -> Error (Printf.sprintf "commons: duplicate common %S" (Slug.to_string s)) + | None -> ( + match first_dup assigned with + | Some s -> Error (Printf.sprintf "commons: duplicate assignment for %S" (Slug.to_string s)) + | None -> ( + (* A formulary with no citations is indistinguishable at the call + site from "this saint has no Common" -- [commons_for] would + return [Some []] and [readings] would emit [] either way. That + is exactly the silent hole this project does not allow, so it + is rejected here where it is still nameable. *) + match List.find_opt (fun (_, cs) -> cs = []) commons with + | Some (s, _) -> + Error (Printf.sprintf "commons: common %S has no citations" (Slug.to_string s)) + | None -> ( + (* An assignment pointing at a Common that does not exist + would otherwise degrade to [None] -- i.e. to "this saint + has no Common", the same answer as no assignment at all -- + so a typo in the data file would be invisible. Named + loudly instead. *) + match + List.find_opt (fun (_, common) -> not (List.mem_assoc common commons)) assigned + with + | Some (saint, common) -> + Error + (Printf.sprintf "commons: %S is assigned to unknown common %S" + (Slug.to_string saint) (Slug.to_string common)) + | None -> Ok { commons; assigned }))) + + let find t saint = + match List.assoc_opt saint t.assigned with + | None -> None + | Some common -> List.assoc_opt common t.commons + + (* Byte-for-byte the failure discipline of [Lectionary.load] (see its own + comments for why each catch-all is placed where it is): every parse and + validation failure comes back as [Error], never as an exception, and + never at module-initialisation time. *) + let load path = + match Sexplib.Sexp.load_sexp path with + | exception Sexplib.Sexp.Parse_error e -> + Error (Printf.sprintf "commons: %s: %s" path e.err_msg) + | exception Sys_error e -> Error (Printf.sprintf "commons: %s" e) + | exception exn -> Error (Printf.sprintf "commons: %s: %s" path (Printexc.to_string exn)) + | sexp -> ( + match t_of_sexp sexp with + | exception Sexplib0.Sexp_conv_error.Of_sexp_error (exn, _) -> + Error (Printf.sprintf "commons: %s: %s" path (Printexc.to_string exn)) + | exception exn -> Error (Printf.sprintf "commons: %s: %s" path (Printexc.to_string exn)) + | parsed -> of_tables ~commons:parsed.commons ~assigned:parsed.assigned) +end + +let commons_for ~commons saint = Commons.find commons saint + (* Step 1: the observed celebration's own proper. + Step 4: the observed SAINT's assigned Common -- see the branch comment in + [readings] for why it sits here, second, and not last. Step 2: the day's own temporal slug. Step 3: a weekday whose own slug has no entry says the preceding Sunday's Mass -- see the implementation comment on that branch in [readings] for the termination argument and why it is the Sunday's TEMPORAL, not observed, identity. + The steps keep their original NUMBERS (the plan's, and every existing + test's and comment's) even though step 4 now runs second: renumbering + would silently invalidate every "step 3" reference already written down. + Execution order is 1, 4, 2, 3. + Nothing here encodes "Lent has daily propers": the presence of an entry in [lectionary] is the sole discriminator -- this function does not branch on season, rank, or any other field to decide whether a temporal slug "ought" @@ -20,7 +124,9 @@ open Colitur_kernel rules ... Have lectio's behaviour; confirm against the Missal's ferial-Mass rubrics when coding") -- that confirmation has not been done; do not read this comment as citing RG/the Missal for the SELECTION rule - itself, only [Lectionary.find]'s presence-or-absence as the mechanism. *) + itself, only [Lectionary.find]'s presence-or-absence as the mechanism. + Step 4 is the one step of the four that DOES have a direct primary-source + warrant; its own comment gives it. *) (* Days from a given weekday back to the preceding Sunday. Sunday itself yields 0, which is why step 3 must guard on it -- see [readings] below. *) let days_since_sunday : Date.weekday -> int = function @@ -32,61 +138,110 @@ let days_since_sunday : Date.weekday -> int = function | Date.Fri -> 5 | Date.Sat -> 6 -let readings ~lectionary ~observed ~temporal ~date ~temporal_at = +let readings ~lectionary ~commons ~observed ~temporal ~date ~temporal_at = match observed.Celebration.citations with | _ :: _ as cs -> cs | [] -> ( - match Lectionary.find lectionary temporal.Temporal.office.Celebration.slug with + (* Step 4: a saint who is the day's observed office and has no proper + says his assigned Common. The assignment is explicit, never + inferred -- see data/ef/commons.sexp. + + ORDER. This runs SECOND, before the temporal fallbacks, not last as + the task brief sketched. The brief's ordering was tried first and is + provably dead code: measured against the real resolver over + 1950-2200, EVERY day on which one of the fifteen readingless + class-3 saints is actually the observed office also has a + non-empty step-2 or step-3 answer waiting (a Septuagesima or + Paschaltide feria resolves through its own slug or its preceding + Sunday's), so a step 4 placed after them is never reached on any + date in the domain. It would also be WRONG where it did fire: on + 2038-03-06 the observed office is Sts Perpetua and Felicity, a + III-class feast that beat the feria, and the Mass said that day is + theirs -- not Septuagesima II Saturday's 2 Cor 11:19-33 / Luke + 8:4-15, which is what the brief's ordering emits. + + WARRANT, and it is the strongest in this chain: the Missal itself, + at each of these saints' own dates, names the Mass to be said -- + "Missa Cognovi, de Communi non Virginum II loco, praeter orationem + sequentem" (9 March), "Missa Os iusti, de Communi Abbatum" + (21 March), and so on. That is a direct instruction about what is + read when the feast is the office of the day, quoted per saint in + data/ef/commons.sexp. Steps 2 and 3, by contrast, rest only on + lectio's observed behaviour (above). So the one step with a primary + source outranks the two without -- which is also simply what the + steps MEAN: steps 2 and 3 answer "what does this day's TEMPORAL + office read", a question that only governs when the temporal office + is the one being celebrated. + + The guard makes that precondition structural rather than a property + of the data file: the Commons are consulted only when the observed + celebration is not itself the day's temporal office. Without it, a + future overlay that assigned a Common to a temporal slug by mistake + would silently replace a feria's Mass; with it, ferias, Sundays, the + Triduum and the RG 78 Saturday Office of the BVM (whose observed + celebration IS its temporal office, deliberately sharing the ferial + slug) can never be diverted here at all. [Validate] already asserts + slug uniqueness per liturgical year, so a sanctoral feast can never + collide with a temporal slug and be wrongly excluded by it. *) + let sanctoral_office = + not (Slug.equal observed.Celebration.slug temporal.Temporal.office.Celebration.slug) + in + match + if sanctoral_office then commons_for ~commons observed.Celebration.slug else None + with | Some cs -> cs | None -> ( - (* Step 3: a feria with no proper of its own says the preceding - Sunday's Mass. WARRANT is the same as step 2's -- lectio's own - observed behaviour, not a confirmed Missal citation: this is the - rule lectio hard-codes as data on the four Advent ferias - (Advent II's readings copied verbatim onto the following - Monday-Saturday) and leaves absent on the other slugs this step - now also reaches; docs/research/rules-register.md already - records the ferial-Mass selection rule itself as unconfirmed - against the primary source. - - Guarded on weekday, but NOT because a Sunday reaching this - branch would loop (fix round 1, coordinator review: the - original comment here claimed exactly that, and it was wrong). - [readings] is not recursive -- step 3's fallback is one flat - [Lectionary.find], never a re-entrant call into [readings] -- - so without the guard, [days_since_sunday Sun = 0] would just - repeat the SAME [Lectionary.find] step 2 already ran and - already got [None] from (same pure inputs, same date), and - return [] once, normally. The chain as a whole terminates - because every step either consults data (a lookup) or, here, - a strictly EARLIER date via [temporal_at] -- no step ever calls - back into [readings] itself, so there is no recursion anywhere - in this function for a cycle to form in the first place. The - real reason for the guard is simpler: a Sunday has no - PRECEDING Sunday to resume -- consulting itself would be - meaningless (it would re-ask the question step 2 just - answered), not dangerous, so the guard exists to make that - intent explicit rather than to prevent a runaway loop that was - never actually possible. - - The preceding Sunday's TEMPORAL slug, never its observed one: - the rubric is the preceding Sunday's Mass even in a year when a - feast displaced that Sunday from being observed (see - test_step3_uses_temporal_not_observed). [temporal_at] gives the - temporal identity of any date, so the Sunday is reached by date - arithmetic and a fresh call to the temporal cycle -- never by - string surgery on [own_slug]: the slug shapes are genuinely - inconsistent across seasons (e.g. [ef-advent-sunday-1] versus - [ef-advent-1-monday], the week number on opposite sides of the - season name), so deriving one from the other textually would be - a latent bug the moment a season's naming convention differs. *) - let offset = days_since_sunday temporal.Temporal.weekday in - if offset = 0 then [] - else - let sunday = Date.add_days date (-offset) in - let sunday_temporal = temporal_at sunday in - match - Lectionary.find lectionary sunday_temporal.Temporal.office.Celebration.slug - with - | Some cs -> cs - | None -> [])) + match Lectionary.find lectionary temporal.Temporal.office.Celebration.slug with + | Some cs -> cs + | None -> ( + (* Step 3: a feria with no proper of its own says the preceding + Sunday's Mass. WARRANT is the same as step 2's -- lectio's own + observed behaviour, not a confirmed Missal citation: this is the + rule lectio hard-codes as data on the four Advent ferias + (Advent II's readings copied verbatim onto the following + Monday-Saturday) and leaves absent on the other slugs this step + now also reaches; docs/research/rules-register.md already + records the ferial-Mass selection rule itself as unconfirmed + against the primary source. + + Guarded on weekday, but NOT because a Sunday reaching this + branch would loop (fix round 1, coordinator review: the + original comment here claimed exactly that, and it was wrong). + [readings] is not recursive -- step 3's fallback is one flat + [Lectionary.find], never a re-entrant call into [readings] -- + so without the guard, [days_since_sunday Sun = 0] would just + repeat the SAME [Lectionary.find] step 2 already ran and + already got [None] from (same pure inputs, same date), and + return [] once, normally. The chain as a whole terminates + because every step either consults data (a lookup) or, here, + a strictly EARLIER date via [temporal_at] -- no step ever calls + back into [readings] itself, so there is no recursion anywhere + in this function for a cycle to form in the first place. The + real reason for the guard is simpler: a Sunday has no + PRECEDING Sunday to resume -- consulting itself would be + meaningless (it would re-ask the question step 2 just + answered), not dangerous, so the guard exists to make that + intent explicit rather than to prevent a runaway loop that was + never actually possible. + + The preceding Sunday's TEMPORAL slug, never its observed one: + the rubric is the preceding Sunday's Mass even in a year when a + feast displaced that Sunday from being observed (see + test_step3_uses_temporal_not_observed). [temporal_at] gives the + temporal identity of any date, so the Sunday is reached by date + arithmetic and a fresh call to the temporal cycle -- never by + string surgery on [own_slug]: the slug shapes are genuinely + inconsistent across seasons (e.g. [ef-advent-sunday-1] versus + [ef-advent-1-monday], the week number on opposite sides of the + season name), so deriving one from the other textually would be + a latent bug the moment a season's naming convention differs. *) + let offset = days_since_sunday temporal.Temporal.weekday in + if offset = 0 then [] + else + let sunday = Date.add_days date (-offset) in + let sunday_temporal = temporal_at sunday in + match + Lectionary.find lectionary sunday_temporal.Temporal.office.Celebration.slug + with + | Some cs -> cs + | None -> []))) diff --git a/lib/rites/rite_ef/lectionary_ef.mli b/lib/rites/rite_ef/lectionary_ef.mli index e54a0b1..baf25d4 100644 --- a/lib/rites/rite_ef/lectionary_ef.mli +++ b/lib/rites/rite_ef/lectionary_ef.mli @@ -3,12 +3,13 @@ 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] is 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 - however suits it, and leaves room for a future diocesan/proper - lectionary overlay to attach without this module changing at all. + [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 @@ -18,17 +19,84 @@ open Colitur_kernel 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. - - Steps 1-3 (Tasks 4-5): the observed celebration's own proper, else the - day's own temporal slug in the lectionary, else -- 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 three gets [] for now -- the Commons (Task 6) are not built here. *) + 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 + + (** 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 -> diff --git a/lib/rites/rite_ef/rite_ef.ml b/lib/rites/rite_ef/rite_ef.ml index ded960b..60065b7 100644 --- a/lib/rites/rite_ef/rite_ef.ml +++ b/lib/rites/rite_ef/rite_ef.ml @@ -11,16 +11,23 @@ module Lectionary_ef = Lectionary_ef open Colitur_kernel -(* [~lectionary], not a value closed over an internal load: fix round 1 - (coordinator review) found the previous version -- [context] as a plain - value, [Lectionary_ef] loading data/ef/lectionary.sexp as a side effect - of being linked -- made `colitur easter ` (no lectionary data - touched at all) die at startup the moment that file was absent from a - bare `dune build`'s own default target. A function mirrors how the - sanctoral [Layer.t] already travels: caller-supplied, not embedded (see - this module's own .mli doc comment on [context] for the fuller - rationale, shared with data/ef/sanctoral.sexp). *) -let context ~lectionary : (Vocab_ef.season, Vocab_ef.rank) Rite.t = +(* [~lectionary] and [~commons], not values closed over an internal load: + fix round 1 (coordinator review) found the previous version -- [context] + as a plain value, [Lectionary_ef] loading data/ef/lectionary.sexp as a + side effect of being linked -- made `colitur easter ` (no + lectionary data touched at all) die at startup the moment that file was + absent from a bare `dune build`'s own default target. A function mirrors + how the sanctoral [Layer.t] already travels: caller-supplied, not + embedded (see this module's own .mli doc comment on [context] for the + fuller rationale, shared with data/ef/sanctoral.sexp). + + [~commons] is REQUIRED, not optional-with-a-default: an omitted + [?commons] would silently give a caller [Commons.empty], i.e. a rite + whose class-3 saints quietly lose their Mass, and nothing in the suite + compares citations across layers 3-5, so that loss would be invisible. + A caller that genuinely has no Commons data passes + [Lectionary_ef.Commons.empty] and says so. *) +let context ~lectionary ~commons : (Vocab_ef.season, Vocab_ef.rank) Rite.t = { Rite.id = Temporal_ef.id; vocab = Vocab_ef.vocab; year_start = Temporal_ef.year_start; @@ -32,4 +39,4 @@ let context ~lectionary : (Vocab_ef.season, Vocab_ef.rank) Rite.t = admit = Precedence_ef.admit }; season_runs = Vocab_ef.seasons; transfer_target = Precedence_ef.transfer_target; - readings = Lectionary_ef.readings ~lectionary } + readings = Lectionary_ef.readings ~lectionary ~commons } diff --git a/lib/rites/rite_ef/rite_ef.mli b/lib/rites/rite_ef/rite_ef.mli index 12f5609..2183799 100644 --- a/lib/rites/rite_ef/rite_ef.mli +++ b/lib/rites/rite_ef/rite_ef.mli @@ -26,10 +26,12 @@ module Lectionary_ef = Lectionary_ef value's own documentation for the termination and forward-progress argument {!Colitur_kernel.Rite.t.transfer_target}'s contract requires). - [readings]: {!Lectionary_ef.readings} partially applied to the caller's - own [~lectionary] -- the observed celebration's own proper, else the - day's own temporal slug, else (a weekday with no entry of its own) the - preceding Sunday's temporal slug, in data/ef/lectionary.sexp (chain - steps 1-3; the Commons are later work). + own [~lectionary] and [~commons] -- the observed celebration's own + proper, else (for a saint who is the day's observed office) his + assigned Common from data/ef/commons.sexp, else the day's own temporal + slug, else (a weekday with no entry of its own) the preceding Sunday's + temporal slug, in data/ef/lectionary.sexp. See {!Lectionary_ef.readings} + for why the Common is consulted second rather than last. Deliberately carries no [sanctoral]/[lectionary] fields the way the original design-doc sketch of [RITE] does: {!Colitur_kernel.Rite.t} (the @@ -44,7 +46,12 @@ module Lectionary_ef = Lectionary_ef that never touches lectionary data at all the moment that file was missing from a bare build -- see the task report). A future diocesan/proper lectionary overlay has a caller-side seam to attach to - for the same reason the sanctoral overlay already does. *) + for the same reason the sanctoral overlay already does. [~commons] + travels the same seam, and is deliberately REQUIRED rather than + defaulted -- see this module's .ml for why a silently-defaulted + {!Lectionary_ef.Commons.empty} would be undetectable by any of the five + validation layers. *) val context : lectionary:Colitur_kernel.Lectionary.t -> + commons:Lectionary_ef.Commons.t -> (Vocab_ef.season, Vocab_ef.rank) Colitur_kernel.Rite.t -- cgit v1.3