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. --- bin/main.ml | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) (limited to 'bin') 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 () -- cgit v1.3