From cba04b3033f23b5c2515c9e648f2b83344716224 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 17 Aug 2026 15:24:27 +0200 Subject: feat(cli): colitur readings, one line per day The lectionary has been resolvable since Task 4 but invisible from the command line: `colitur day` prints no citations, so the branch's whole deliverable could only be seen through the test suite. A separate command rather than extra columns on `day`, which is where the plan pointed. Its instruction was to append the Epistle and Gospel to each row "matching the existing column style", and that turns out not to be possible: 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 leaves the row unsplittable -- no awk or cut field number recovers where the Epistle ends. That is the opposite of what the row is shaped for. So `day` keeps its format byte-identical, asserted directly in cli.t rather than left implicit, and the citations get their own row with " | "-delimited fields, safe for values containing spaces. Both formats are a stopgap and say so in the source: the design calls for one schema rendered through a logic-less template engine, and two ad-hoc formats are easier to retire later than one overloaded format whose parsing rules nobody wrote down. The year walk is now shared. day_report and readings_report differ only in how a day is printed, and the two-liturgical-year indexing -- with its own reasoning about civil-versus-liturgical spans -- is exactly the part that must not be duplicated and drift. "-" for an absent part, though no EF day can currently print one: layer 2 asserts exactly one First and one Gospel on every day of every year 1583-9999. The CLI still does not assume a guarantee the kernel makes about data rather than about types. cli.t gains the four chain-step cases test_golden.ml pins against the scans, so the CLI path is checked to agree with the library path, plus the 2 January line, which reads the Circumcision's Mass under the Missal's own ferial rubric (scan1:6523-6526) and not RG 17(a). Verified the cram tests actually run and have teeth: corrupting one expectation produces a diff and exits 1. CLAUDE.md: state, test count (369, 370 with the sweep), the readings command and why it is separate, and What's next -- the lectionary landed early rather than inside Plan 4, so what remains there is OF's own lectionary, not the mechanism. Chants stay deliberately unbuilt, and Validate now rejects any part outside First/Gospel. --- CLAUDE.md | 37 ++++++++++++++++++++++++++----- bin/main.ml | 62 ++++++++++++++++++++++++++++++++++++++++++++++++--- test/cli.t | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 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 `, `temporal `, `day `. +**CLI**: `colitur easter `, `temporal `, `day `, +`readings `. + +`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 | colitur temporal | colitur day "; + prerr_endline + "colitur: usage: colitur easter | colitur temporal | colitur day | colitur \ + readings "; 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 | colitur temporal | colitur day + colitur: usage: colitur easter | colitur temporal | colitur day | colitur readings [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] -- cgit v1.3