summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CLAUDE.md37
-rw-r--r--bin/main.ml62
-rw-r--r--test/cli.t74
3 files changed, 163 insertions, 10 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 0819c88..9f8352c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -224,13 +224,13 @@ against missalemeum; layer 5 pins ~30 dates.
ever exercised between two `Commemoration_only` candidates, neither of which has
any RG 91 table position to compare in the first place.
-## Current state (Plans 1–3 DONE — verify with `git log`)
+## Current state (Plans 1–3 + the EF lectionary DONE — verify with `git log`)
**Plans 1 + 2 are on `main` (35 commits). Plan 3 and its follow-on fix/feature
tasks (RG 16(a), Holy Family/RG 112(a), Holy Name/RG 110, the Sacred Triduum,
the BVM Saturday Office, the Major Litanies) have landed on a chain of feature
-branches since — test count keeps climbing task by task (325 tests green, 326
-with the exhaustive sweep, as of `ef-major-litanies`; this line is not kept in
+branches since — test count keeps climbing task by task (369 tests green, 370
+with the exhaustive sweep, as of `ef-lectionary`; this line is not kept in
lockstep with every task, `git log`/`dune test` are the actual source of
truth).** The kernel, the **complete EF
temporal cycle**, the **resolution engine**, the **sanctoral data**, and **all five
@@ -277,7 +277,19 @@ earlier one; M5 corrected (a prior note had 2027's own outcome backwards) and
M20 added by the `ef-major-litanies` task, M18 394 not 395 accordingly).
Fixtures live in `test/fixtures/` with asserted SHA-256s.
-**CLI**: `colitur easter <year>`, `temporal <year>`, `day <year>`.
+**CLI**: `colitur easter <year>`, `temporal <year>`, `day <year>`,
+`readings <year>`.
+
+`readings` is a **separate command, not extra columns on `day`**, for a
+mechanical reason worth not rediscovering: a citation contains spaces and
+commas (`Ezech 34:11-16`, `Ecclus 51:1-8, 12`) while a `day` row is
+space-separated with a variable-length `+slug` commemoration tail, so
+appending them there leaves the row unsplittable by field number. `day`'s
+format is therefore **byte-identical** to what it was before the lectionary
+existed (asserted in `test/cli.t`). Both are a **stopgap**, not the project's
+answer to output: the design still calls for one schema rendered through a
+logic-less template engine — two ad-hoc formats are easier to retire than one
+overloaded format with unwritten parsing rules.
`band` is **provably total** over everything the engine constructs: zero
`unclassified` across all 8 417 years, for a 28-branch hand-transcribed table.
@@ -304,8 +316,21 @@ dune exec colitur -- day 2026 | head
## What's next
-- **Plan 4 — OF rite module** (proves `RITE` generalizes) → the lectionary
- bootstrap and citations → full output/rendering → hardening and a first tag.
+- **The EF lectionary is DONE** (branch `ef-lectionary`, 20 commits): the four-
+ step reading-resolution chain, its data (`data/ef/lectionary.sexp`,
+ `commons.sexp`, sanctoral propers), all five validation layers extended to
+ citations, and `colitur readings`. Layer 2 asserts that **every day of every
+ year 1583–9999 resolves exactly one Epistle and one Gospel** — measured, and
+ mutation-proved live rather than silently inert. **Chants (Psalm, Second,
+ Tract, Alleluia, Sequence) remain deliberately unbuilt**: no source, no
+ oracle, and `Validate`'s own `citations` check now *rejects* any part outside
+ First/Gospel, so one appearing would be a defect rather than a feature
+ arriving early.
+- **Plan 4 — OF rite module** (proves `RITE` generalizes) → full output/
+ rendering → hardening and a first tag. (The lectionary bootstrap and
+ citations this line used to defer to Plan 4 landed early, on
+ `ef-lectionary`; what remains here is OF's own lectionary, not the
+ mechanism, which is now built and rite-agnostic.)
**All four behaviour items below are now RESOLVED** (RG 16(a) and
commemoration identity, closed on branch `ef-rg16a`; Holy Family/RG 112(a)
and observed identity, closed on branch `ef-rg112-rg110`; Holy Name of
diff --git a/bin/main.ml b/bin/main.ml
index 16daa49..01cdbf8 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -157,6 +157,51 @@ let day_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kerne
(Colitur_kernel.Colour.to_string cel.Colitur_kernel.Celebration.colour)
commemorations
+(* The reading citations for a day, as its own row shape rather than extra
+ columns on [day_line]'s.
+
+ A SEPARATE COMMAND, not a widening of `colitur day`, and the reason is
+ mechanical rather than aesthetic: a citation contains spaces and commas
+ ("Ezech 34:11-16", "Ecclus 51:1-8, 12"), while [day_line]'s row is
+ space-separated with a variable-length "+slug" commemoration tail.
+ Appending citations there would leave the row unsplittable -- no [awk]/
+ [cut] field number could recover where the Epistle ends -- which is the
+ opposite of the composability the row is shaped for. So `day` keeps its
+ format byte-identical (nothing downstream of it changes at all) and the
+ citations get a row whose own fields are " | "-delimited, safe for values
+ containing spaces.
+
+ This is deliberately a stopgap, and should not be mistaken for the
+ project's answer to output formatting: the design calls for one schema
+ rendered through a logic-less template engine (CSV/JSON/S-expression),
+ which is where this belongs eventually. Two ad-hoc column formats are
+ easier to retire later than one overloaded format with parsing rules
+ nobody wrote down.
+
+ "-" for an absent part, matching [temporal_report]'s own [field]
+ convention for an empty column. On the EF data as it stands no day can
+ actually print "-" -- {!Colitur_kernel.Validate}'s "citations"/
+ "citations-unresolved" checks assert exactly one First and one Gospel on
+ every day of every year 1583..9999 -- but the CLI must not assume a
+ guarantee the kernel makes about DATA rather than about types. *)
+let readings_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t)
+ =
+ let cel = d.Colitur_kernel.Liturgical_day.observed in
+ let part_ref p =
+ match
+ List.find_opt
+ (fun (c : Colitur_kernel.Citation.t) -> c.Colitur_kernel.Citation.part = p)
+ d.Colitur_kernel.Liturgical_day.citations
+ with
+ | Some c -> c.Colitur_kernel.Citation.reference
+ | None -> "-"
+ in
+ Printf.printf "%s %s | %s | %s\n"
+ (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date)
+ (Colitur_kernel.Slug.to_string cel.Colitur_kernel.Celebration.slug)
+ (part_ref Colitur_kernel.Citation.First)
+ (part_ref Colitur_kernel.Citation.Gospel)
+
(* One civil year, Jan 1 - Dec 31, matching [temporal_report]'s own scan --
NOT one liturgical year: [Colitur_kernel.Calendar.year] resolves a single
Advent-anchored liturgical year, which straddles two civil years, so a
@@ -187,7 +232,12 @@ let load_ef_data () =
| Error msg -> Error msg
| Ok commons -> Ok (layer, lectionary, commons)))
-let day_report y =
+(* The resolved-year walk, shared by [day_report] and [readings_report]: they
+ differ only in how each day is printed, and the two-liturgical-year
+ indexing below (with its own reasoning about civil-vs-liturgical spans) is
+ exactly the part that must not be duplicated and drift. [line] is the only
+ difference between the two commands. *)
+let resolved_year_report ~line y =
match load_ef_data () with
| Error msg ->
Printf.eprintf "colitur: %s\n" msg;
@@ -211,7 +261,7 @@ let day_report y =
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
+ | Some day -> line day
| None ->
(* Unreachable for any [y] in 1583..9999: the two indexed
liturgical years jointly cover [year_start (y-1), year_start
@@ -227,8 +277,13 @@ let day_report y =
d := D.add_days !d 1
done
+let day_report y = resolved_year_report ~line:day_line y
+let readings_report y = resolved_year_report ~line:readings_line y
+
let usage () =
- prerr_endline "colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year>";
+ prerr_endline
+ "colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur \
+ readings <year>";
exit 2
let with_year ys f =
@@ -244,4 +299,5 @@ let () =
| [| _; "easter"; ys |] -> with_year ys easter_report
| [| _; "temporal"; ys |] -> with_year ys temporal_report
| [| _; "day"; ys |] -> with_year ys day_report
+ | [| _; "readings"; ys |] -> with_year ys readings_report
| _ -> usage ()
diff --git a/test/cli.t b/test/cli.t
index 5a646a4..8de4591 100644
--- a/test/cli.t
+++ b/test/cli.t
@@ -17,7 +17,7 @@ A year outside the supported domain is rejected (exit 2):
No/garbage arguments give a usage error (exit 2):
$ colitur
- colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year>
+ colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year>
[2]
The EF temporal cycle for a year, one line per day:
@@ -119,3 +119,75 @@ A year outside the supported domain is rejected (exit 2):
$ colitur day 1000
colitur: year 1000 out of range 1583..9999
[2]
+
+The reading citations for a year, one line per day: date, the observed day's
+slug, then the Epistle and the Gospel, " | "-delimited.
+
+A SEPARATE command rather than extra columns on `colitur day`, for a
+mechanical reason: a citation contains spaces and commas ("Ezech 34:11-16",
+"Ecclus 51:1-8, 12"), while a `day` row is space-separated with a
+variable-length "+slug" commemoration tail, so appending them there would
+leave the row unsplittable by field number. `day`'s own format is therefore
+byte-identical to what it was before readings existed -- asserted directly,
+below.
+
+ $ colitur readings 2026 | head -3
+ 2026-01-01 ef-circumcision | Titus 2:11-15 | Luke 2:21
+ 2026-01-02 ef-christmas-1-friday | Titus 2:11-15 | Luke 2:21
+ 2026-01-03 ef-christmas-1-saturday | Gal 4:1-7 | Luke 2:33-40
+
+ $ colitur readings 2026 | wc -l
+ 365
+
+2 January reads the Circumcision's own Mass, not its own: the Missal's
+Mass-propers rubric printed under the Sanctissimi Nominis Iesu heading --
+"Diebus ferialibus a 2 ad 5 ianuarii Missa dicitur ut die 1 ianuarii, cum
+Gloria et praefatione de Nativitate, sine Credo et Communicantes proprio"
+(docs/research/scan1.txt:6523-6526, corroborated scan2.txt:7216). Note this
+is NOT RG 17(a), which fixes only WHEN the Holy Name is kept; the two are
+different rules sharing a date window. Hence the identical citation on the
+1st and the 2nd above, which is correct and not a duplicated row.
+
+One line per step of the reading-resolution chain, the same four cases
+test_golden.ml pins against the photographic scans (each literal there was
+read out of the Missal before being compared with colitur -- these assert
+that the CLI path agrees with the library path, nothing more):
+
+ $ colitur readings 2038 | grep '^2038-03-08 '
+ 2038-03-08 john-of-god | Ecclus 31:8-11 | Matt 22:34-46
+
+ $ colitur readings 2026 | grep '^2026-02-23 '
+ 2026-02-23 ef-lent-1-monday | Ezech 34:11-16 | Matt 25:31-46
+
+ $ colitur readings 2025 | grep '^2025-12-01 '
+ 2025-12-01 ef-advent-1-monday | Rom 13:11-14 | Luke 21:25-33
+
+ $ colitur readings 2038 | grep '^2038-03-06 '
+ 2038-03-06 sts-felicitas-perpetua | Ecclus 51:1-8, 12 | Matt 13:44-52
+
+The Advent line above is the ferial resumption (chain step 3) and is the one
+worth reading twice: 1 December 2025 is the Monday after Advent I, and
+Advent's ferias -- unlike Lent's -- have no Mass of their own, so the
+citation shown IS Advent I Sunday's ("ad Romanos. Rom. 13, 11-14" /
+"dum Lucam. Luc. 21, 25-33", scan1.txt:4878 + 4912). A regression that
+stopped walking back would change this line and nothing else.
+
+Every day of the year carries both parts -- no "-" anywhere. This is the CLI
+view of the invariant Validate asserts over the whole 1583..9999 domain
+("citations" / "citations-unresolved"), so it is a spot-check of one year
+rather than the guarantee itself:
+
+ $ colitur readings 2026 | grep -c ' | - \| | -$'
+ 0
+ [1]
+
+`colitur day`'s own format is unaffected by any of the above:
+
+ $ colitur day 2026 | head -1
+ 2026-01-01 thursday christmastide - ef-circumcision class-1 white
+
+A year outside the supported domain is rejected (exit 2):
+
+ $ colitur readings 1000
+ colitur: year 1000 out of range 1583..9999
+ [2]