aboutsummaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-15 01:40:54 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-15 01:40:54 +0200
commit1299815ea03c2b1e4ff19d92d32e386fec24f4e4 (patch)
tree7fe91b561af482d7079fb4808ec52552e3228d08 /bin
parentfe94cd9c4150fd6285dab49b06e23b6db42d131b (diff)
downloadcolitur-1299815ea03c2b1e4ff19d92d32e386fec24f4e4.tar.gz
colitur-1299815ea03c2b1e4ff19d92d32e386fec24f4e4.zip
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.
Diffstat (limited to 'bin')
-rw-r--r--bin/main.ml107
1 files changed, 67 insertions, 40 deletions
diff --git a/bin/main.ml b/bin/main.ml
index bc02654..16daa49 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -122,6 +122,19 @@ let load_ef_lectionary () =
| Error e -> Error (Printf.sprintf "failed to load %s: %s" path e)
| Ok lectionary -> Ok lectionary
+(* Sibling to [load_ef_lectionary] above, same reasoning and the same
+ [result] failure path: data/ef/commons.sexp holds the Commons of the
+ 1962 Missal plus the per-saint assignments that route a readingless
+ class-3 feast to one, and [Rite_ef.context] takes it as [~commons]
+ rather than reading it itself. Its own loader validates the file
+ (duplicate ids, empty formularies, assignments naming a common that does
+ not exist) and reports every failure as [Error]. *)
+let load_ef_commons () =
+ let path = Filename.concat (data_dir ()) "commons.sexp" in
+ match Rite_ef.Lectionary_ef.Commons.load path with
+ | Error e -> Error (Printf.sprintf "failed to load %s: %s" path e)
+ | Ok commons -> Ok commons
+
let day_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t)
=
let t = d.Colitur_kernel.Liturgical_day.temporal in
@@ -155,50 +168,64 @@ let day_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kerne
for the days sharing one liturgical year (calendar.mli's own "pays it
once" cost model assumes exactly this usage: call [year], not [day] in a
loop). *)
-let day_report y =
+(* The three data files this subcommand needs, loaded once and reported
+ through ONE failure path. Flattened out of the nested [match] this used
+ to be when a third loader (the Commons, Task 6) joined the first two:
+ each additional caller-supplied table would otherwise add a level of
+ indentation and a third verbatim copy of the same two-line error-and-exit
+ block. Every loader already returns [(_, string) result] (never raises,
+ never reads at module-initialisation time -- see [load_ef_lectionary]),
+ so chaining them costs nothing and keeps that promise intact. *)
+let load_ef_data () =
match load_ef_layer () with
+ | Error msg -> Error msg
+ | Ok layer -> (
+ match load_ef_lectionary () with
+ | Error msg -> Error msg
+ | Ok lectionary -> (
+ match load_ef_commons () with
+ | Error msg -> Error msg
+ | Ok commons -> Ok (layer, lectionary, commons)))
+
+let day_report y =
+ match load_ef_data () with
| Error msg ->
Printf.eprintf "colitur: %s\n" msg;
exit 2
- | Ok layer -> (
- match load_ef_lectionary () with
- | Error msg ->
- Printf.eprintf "colitur: %s\n" msg;
- exit 2
- | Ok lectionary ->
- let context = Rite_ef.context ~lectionary in
- let module Cal = Colitur_kernel.Calendar in
- let by_rata : (int, (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) Hashtbl.t =
- Hashtbl.create 400
- in
- let index days =
- Array.iter
- (fun (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) ->
- Hashtbl.replace by_rata (D.to_rata d.Colitur_kernel.Liturgical_day.date) d)
- days
- in
- index (Cal.year context layer (y - 1));
- index (Cal.year context layer y);
- let jan1 = match D.make ~year:y ~month:1 ~day:1 with Ok t -> t | Error e -> failwith e in
- let dec31 = match D.make ~year:y ~month:12 ~day:31 with Ok t -> t | Error e -> failwith e in
- let d = ref jan1 in
- while D.compare !d dec31 <= 0 do
- (match Hashtbl.find_opt by_rata (D.to_rata !d) with
- | Some day -> day_line day
- | None ->
- (* Unreachable for any [y] in 1583..9999: the two indexed
- liturgical years jointly cover [year_start (y-1), year_start
- (y+1)), which contains all of civil year [y]
- (calendar.mli). Not a [failwith] -- an out-of-domain [d]
- inside this loop is impossible by construction (jan1/dec31
- are themselves validated in range, and [add_days] only ever
- advances within the same civil year here) -- but a silent
- skip would violate the same "never silently dropped"
- standard the kernel holds itself to, so a gap surfaces
- loudly on stderr rather than as a quietly short year. *)
- Printf.eprintf "colitur: internal error: no resolved day for %s\n" (D.to_iso8601 !d));
- d := D.add_days !d 1
- done)
+ | Ok (layer, lectionary, commons) ->
+ let context = Rite_ef.context ~lectionary ~commons in
+ let module Cal = Colitur_kernel.Calendar in
+ let by_rata : (int, (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) Hashtbl.t =
+ Hashtbl.create 400
+ in
+ let index days =
+ Array.iter
+ (fun (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) ->
+ Hashtbl.replace by_rata (D.to_rata d.Colitur_kernel.Liturgical_day.date) d)
+ days
+ in
+ index (Cal.year context layer (y - 1));
+ index (Cal.year context layer y);
+ let jan1 = match D.make ~year:y ~month:1 ~day:1 with Ok t -> t | Error e -> failwith e in
+ let dec31 = match D.make ~year:y ~month:12 ~day:31 with Ok t -> t | Error e -> failwith e in
+ let d = ref jan1 in
+ while D.compare !d dec31 <= 0 do
+ (match Hashtbl.find_opt by_rata (D.to_rata !d) with
+ | Some day -> day_line day
+ | None ->
+ (* Unreachable for any [y] in 1583..9999: the two indexed
+ liturgical years jointly cover [year_start (y-1), year_start
+ (y+1)), which contains all of civil year [y]
+ (calendar.mli). Not a [failwith] -- an out-of-domain [d]
+ inside this loop is impossible by construction (jan1/dec31
+ are themselves validated in range, and [add_days] only ever
+ advances within the same civil year here) -- but a silent
+ skip would violate the same "never silently dropped"
+ standard the kernel holds itself to, so a gap surfaces
+ loudly on stderr rather than as a quietly short year. *)
+ Printf.eprintf "colitur: internal error: no resolved day for %s\n" (D.to_iso8601 !d));
+ d := D.add_days !d 1
+ done
let usage () =
prerr_endline "colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year>";