diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-22 11:21:14 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-22 11:21:14 +0200 |
| commit | fd8a4f115d3c62450368cf9b3fed5705ce78e805 (patch) | |
| tree | f935ec0e2e4c39cb3ddbba364ebc1f873c1b0574 | |
| parent | 27b07b447be9cf960d1a44e480d8356245481d17 (diff) | |
| download | colitur-fd8a4f115d3c62450368cf9b3fed5705ce78e805.tar.gz colitur-fd8a4f115d3c62450368cf9b3fed5705ce78e805.zip | |
feat(cli): colitur rubrics prints the day's Mass formulary
Separate from day for the reason readings is: a formulary name contains
spaces and day's row is space-separated with a variable-length tail.
Modelled on readings' own structure (year parsing, --overlay plumbing,
error handling): --overlay is accepted, since an overlay can change
which celebration is observed and hence which Mass is said. --lang,
--raw and --sigla-* are refused rather than silently ignored -- this
row resolves no display name and no citation for any of them to
affect.
Output is TAB-separated (date, formulary slug, source), not space or
" | " like the two existing reports: a resolved formulary NAME is a
column a later task may add, and it can carry both spaces and
punctuation a citation never does, ruling out either separator already
in use.
day and readings verified byte-identical (sha256) against a v0.10.1
build across 1583/1900/2026/2038/9999. Man page documents rubrics
beside readings and renders through groff with no errors.
| -rw-r--r-- | bin/main.ml | 82 | ||||
| -rw-r--r-- | man/colitur.1 | 91 | ||||
| -rw-r--r-- | test/cli.t | 93 |
3 files changed, 248 insertions, 18 deletions
diff --git a/bin/main.ml b/bin/main.ml index 18fd2f5..e00e020 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -362,6 +362,44 @@ let readings_line ~lang ~sigla (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.r (part_ref Colitur_kernel.Citation.Gospel) name_suffix +(* Task 4 (celebrant-rubrics-phase1): the day's own Mass formulary -- which + slug's Mass is actually said, and how that was decided + ({!Colitur_kernel.Mass_formulary.source}: proper/own/preceding-sunday/ + common/votive). + + A SEPARATE command from `day` and `readings`, for the identical mechanical + reason CLAUDE.md already records for `readings`: a formulary NAME (not + built yet, but the reason this row is shaped the way it is) contains + spaces ("Mass of the 9th Sunday after Pentecost"), and `day`'s own row is + fixed-width space-separated with a variable-length "+slug" tail, so + appending anything with its own internal whitespace there would leave the + row unsplittable by field number. + + TAB-separated rather than reusing [readings_line]'s " | " -- deliberately + a THIRD delimiter, not a second use of the existing one -- because a + later column on this row (the resolved formulary name, once one exists) + can itself contain a literal "|" inside punctuation a citation never + does, and because TAB is what stays unambiguous once a field may carry + both spaces and arbitrary punctuation. This is also why `rubrics` does + not (yet) take --lang/--raw/--sigla-*: there is no display name or + citation on this row for any of them to resolve. + + [d.formulary] is documented as [Some] on every day of every year for EF, + asserted by {!Colitur_kernel.Validate}'s own ["formulary"] check -- but + the type itself permits [None] (a rite with no lectionary), so this + prints "-" rather than pattern-matching partially and crashing on a + guarantee that belongs to DATA, not to the type. *) +let rubrics_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) + = + let said, via = + match d.Colitur_kernel.Liturgical_day.formulary with + | Some f -> + ( Colitur_kernel.Slug.to_string f.Colitur_kernel.Mass_formulary.said, + Colitur_kernel.Mass_formulary.source_to_string f.Colitur_kernel.Mass_formulary.via ) + | None -> ("-", "-") + in + Printf.printf "%s\t%s\t%s\n" (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) said via + (* 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 @@ -456,6 +494,7 @@ let resolved_year_report ~line ~overlays y = let day_report ~lang ~overlays y = resolved_year_report ~line:(day_line ~lang) ~overlays y let readings_report ~lang ~sigla ~overlays y = resolved_year_report ~line:(readings_line ~lang ~sigla) ~overlays y +let rubrics_report ~overlays y = resolved_year_report ~line:rubrics_line ~overlays y (* Task 8: `colitur emit` -- the five template-family emitters built in Tasks 5-7, wired to a year RANGE rather than a single year, because a @@ -1182,7 +1221,9 @@ usage: colitur temporal <year> the temporal cycle, one line per day colitur day <year> the resolved day identity, one line per day colitur readings <year> the Mass reading citations, one line per day + colitur rubrics <year> the Mass formulary said, one line per day colitur day|readings <year> [--overlay FILE ...] [--lang CODE|FILE] [--raw] + colitur rubrics <year> [--overlay FILE ...] colitur emit --format csv|json|sexp|xml|ics --from Y --to Y [--overlay FILE ...] [--dtstamp S] [--lang CODE|FILE] [--raw] render a resolved year range through one of five emitters @@ -1216,6 +1257,8 @@ output formats: 2026-04-05 sunday paschaltide 1 ef-easter-sunday class-1 white readings date slug | Epistle | Gospel [| name] 2026-12-25 ef-nativity | Heb 1:1-12 | John 1:1-14 + rubrics date, formulary slug, source -- TAB-separated + 2026-01-01[TAB]ef-circumcision[TAB]own A citation contains spaces, so readings uses " | " between its fields while day stays space-separated; that is why they are separate commands rather @@ -1228,6 +1271,17 @@ output formats: for that particular day, the trailing field is simply absent, which is what makes --raw byte-identical to this program's pre-naming output. + rubrics prints which Mass is actually said and how that was decided + (source: proper/own/preceding-sunday/common/votive) -- not always the + day's own: a weekday with no proper resumes the preceding Sunday's, a + saint with no proper says his assigned Common. TAB-separated rather than + space or " | ": a resolved formulary NAME is a column a later version may + add, and it can carry both spaces and punctuation a citation never does, + which rules out either alternative already in use above. --overlay is + accepted (the observed celebration it changes decides the formulary); + --lang/--raw/--sigla-* are refused -- this row resolves no display name + and no citation for any of them to affect. + emit one schema (season, week, slug, rank, colour, subject, names, citations, commemorations), rendered five ways: csv (RFC 4180, one header for the whole run), json, sexp, xml (schema/colitur- @@ -1240,9 +1294,10 @@ overlays: the shipped universal one, never instead of it, so local feasts add to it rather than replacing it. Later files win over earlier ones, and over the universal calendar, when they name the same - slug. Accepted on `day`, `readings`, `emit`, `table`, `render` and - `publish` -- `easter` and `temporal` read no sanctoral data, so - the flag is refused there rather than silently ignored. + slug. Accepted on `day`, `readings`, `rubrics`, `emit`, `table`, + `render` and `publish` -- `easter` and `temporal` read no + sanctoral data, so the flag is refused there rather than + silently ignored. An overlay is applied, NOT validated: colitur's test layers assert things about the shipped calendar and cannot vouch for a file you @@ -1435,10 +1490,10 @@ let print_help () = let usage () = prerr_endline "colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur \ - readings <year> | colitur emit --format FMT --from Y --to Y | colitur table --year Y --template \ - FILE | colitur render --template FILE --year Y | colitur publish --from Y --to Y --out DIR | \ - colitur lang --list|--dump CODE|--check FILE | colitur config --show | colitur check FILE | \ - colitur new-overlay (try: colitur --help)"; + readings <year> | colitur rubrics <year> | colitur emit --format FMT --from Y --to Y | colitur \ + table --year Y --template FILE | colitur render --template FILE --year Y | colitur publish \ + --from Y --to Y --out DIR | colitur lang --list|--dump CODE|--check FILE | colitur config --show \ + | colitur check FILE | colitur new-overlay (try: colitur --help)"; exit 2 let with_year ys f = @@ -2269,6 +2324,19 @@ let () = ~sigla_tradition_flag:sigla_tradition ~config in with_year ys (readings_report ~lang:lang_t ~sigla ~overlays:effective_overlays) + | [ "rubrics"; ys ] -> + (* --overlay accepted, same reasoning as `readings`: an overlay can + change which celebration is observed, hence which Mass formulary + is said. --lang/--raw/--sigla-* are refused, unlike `readings` + -- this row resolves no display name and no citation for any of + them to affect. *) + reject_emit "rubrics"; + reject_table "rubrics"; + reject_publish "rubrics"; + reject_lang "rubrics"; + reject_lang_sub "rubrics"; + reject_sigla "rubrics"; + with_year ys (rubrics_report ~overlays:effective_overlays) | [ "emit" ] -> ( reject_table "emit"; reject_publish "emit"; diff --git a/man/colitur.1 b/man/colitur.1 index 4a3c580..5a8ebbf 100644 --- a/man/colitur.1 +++ b/man/colitur.1 @@ -3,7 +3,7 @@ colitur \- deterministic liturgical calendar and lectionary engine (Roman rite, 1962) .SH SYNOPSIS .B colitur -.BR easter | temporal | day | readings +.BR easter | temporal | day | readings | rubrics .I YEAR .br .B colitur @@ -17,6 +17,11 @@ colitur \- deterministic liturgical calendar and lectionary engine (Roman rite, .RB [ \-\-raw ] .br .B colitur +.B rubrics +.I YEAR +.RB [ \-\-overlay " FILE" " ...]" +.br +.B colitur .B emit .BI \-\-format " FMT" .BI \-\-from " YEAR" @@ -116,6 +121,15 @@ occurrence, commemoration and transfer. .BI readings " YEAR" The Mass reading citations, one line per day. .TP +.BI rubrics " YEAR" +The Mass formulary actually said, one line per day \(em not always the +day's own: a weekday with no proper resumes the preceding Sunday's, a +saint with no proper says his assigned Common, and RG 78/309(a)'s votive +Saturday Mass of Our Lady is said in place of an unoccupied office's own. +See +.B OUTPUT FORMAT +below. +.TP .B emit Render a civil\-year range through one of five emitters \(em .BR csv ", " json ", " sexp ", " xml " or " ics . @@ -210,7 +224,7 @@ for the config file's location and precedence in full. .TP .BI \-\-overlay " FILE" Apply a user calendar on top of the shipped one. Repeatable and ordered; -.BR day ", " readings ", " emit ", " table ", " render " and " publish +.BR day ", " readings ", " rubrics ", " emit ", " table ", " render " and " publish only. See .B OVERLAYS below. @@ -442,17 +456,80 @@ own trailing field, above. 2038\-03\-06 sts\-felicitas\-perpetua | Ecclus 51:1\-8, 12 | Matt 13:44\-52 | Ss. Perpetuae et Felicitatis Mm. .fi .RE +.SS rubrics +.RS +.nf +date [TAB] formulary\-slug [TAB] source +.fi +.RE .PP -Both reports are one line per day and ordered by date, so they compose with +The day's own Mass formulary: which slug's Mass is actually said, and how +that was decided. +.B rubrics +separates its fields with a literal TAB \(em not a plain space like +.B day +or +.RB \(lq " | " \(rq +like +.B readings +\(em because a resolved formulary NAME (a column a later version may add, +not this one) can carry both spaces and punctuation a citation never does, +which rules out either separator already in use above. A separate command +for the identical mechanical reason +.B day +is separate from +.BR readings : +.BR day 's +own row is fixed\-width space\-separated with a variable\-length +.RI + slug +tail, so appending anything with its own internal whitespace there would +leave it unsplittable by field number. +.I source +is one of +.BR proper ", " own ", " preceding\-sunday ", " common " or " votive . +.RS +.nf + +2026\-01\-01 [TAB] ef\-circumcision [TAB] own +2038\-03\-08 [TAB] john\-of\-god [TAB] proper +2025\-12\-01 [TAB] ef\-advent\-sunday\-1 [TAB] preceding\-sunday +.fi +.RE +.PP +Unlike +.B day +and +.BR readings , +.B rubrics +resolves no display name and no citation, so it takes none of +.BR \-\-lang ", " \-\-raw +or any +.B \-\-sigla\-* +flag \(em refused rather than silently ignored, the same discipline +.B \-\-overlay +gets on +.B easter +and +.BR temporal . +.PP +All three reports are one line per day and ordered by date, so they compose +with .BR grep (1), .BR awk (1) and .BR join (1) in the ordinary way. Pass .B \-\-raw -to restore the pre\-naming byte\-exact output of either report \(em no -trailing field at all \(em for a script written against it before this -feature existed. +to restore the pre\-naming byte\-exact output of +.B day +or +.B readings +\(em no trailing field at all \(em for a script written against either +before the naming feature existed; +.B rubrics +is already in that form and has no +.B \-\-raw +of its own to pass. .SH EMIT .BI "colitur emit " \-\-format " FMT " \-\-from " YEAR " \-\-to " YEAR" renders the same resolved day \(em season, week, slug, rank, colour, @@ -903,7 +980,7 @@ shapes: .RE .PP Accepted on -.BR day ", " readings ", " emit ", " table ", " render " and " publish . +.BR day ", " readings ", " rubrics ", " emit ", " table ", " render " and " publish . .BR easter " and " temporal read no sanctoral data at all, so the flag would have no effect there and is .I refused @@ -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 readings <year> | colitur emit --format FMT --from Y --to Y | colitur table --year Y --template FILE | colitur render --template FILE --year Y | colitur publish --from Y --to Y --out DIR | colitur lang --list|--dump CODE|--check FILE | colitur config --show | colitur check FILE | colitur new-overlay (try: colitur --help) + colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> | colitur rubrics <year> | colitur emit --format FMT --from Y --to Y | colitur table --year Y --template FILE | colitur render --template FILE --year Y | colitur publish --from Y --to Y --out DIR | colitur lang --list|--dump CODE|--check FILE | colitur config --show | colitur check FILE | colitur new-overlay (try: colitur --help) [2] The EF temporal cycle for a year, one line per day: @@ -199,6 +199,91 @@ A year outside the supported domain is rejected (exit 2): colitur: year 1000 out of range 1583..9999 [2] +Rubrics (Task 4, celebrant-rubrics-phase1): the day's own Mass formulary, one +row per day: date, the formulary's slug (which Mass is actually said, not +always the day's own), and how that was decided +(proper/own/preceding-sunday/common/votive). TAB-separated -- not space like +`day`, not " | " like `readings` -- because a resolved formulary NAME (a +later column, not this one) can itself contain both spaces and punctuation a +citation never does, and TAB is what stays unambiguous either way. A +separate command for the same mechanical reason `readings` is: `day`'s row +is fixed-width space-separated with a variable-length "+slug" tail. + + $ colitur rubrics 2026 | head -3 + 2026-01-01 ef-circumcision own + 2026-01-02 ef-christmas-1-friday own + 2026-01-03 ef-christmas-1-saturday votive + + $ colitur rubrics 2026 | wc -l + 365 + +1 January reads "own", not "proper": the Circumcision is a TEMPORAL-origin +office (Rite_ef.Temporal_ef, not the sanctoral Layer), so step 1 of the +resolution chain (a SANCTORAL observed celebration's own proper) does not +apply to it -- step 2 does (the day's own temporal slug in the lectionary), +tagged `own`. Contrast a real sanctoral saint with his own proper: + + $ colitur rubrics 2038 | grep '^2038-03-08' + 2038-03-08 john-of-god proper + +A saint with no proper of his own says his assigned Common (step 4): + + $ colitur rubrics 2038 | grep '^2038-03-06' + 2038-03-06 common-of-non-virgins-1 common + +A weekday with no proper of its own resumes the preceding Sunday's, never +its own observed slug -- 1 December 2025 is the Monday after Advent I, and +Advent's ferias have no Mass of their own (step 3): + + $ colitur rubrics 2025 | grep '^2025-12-01' + 2025-12-01 ef-advent-sunday-1 preceding-sunday + +3 January 2026 above ("votive") is the RG 78/309(a) Saturday Mass of Our +Lady, said IN PLACE of the day's own office's Mass while the office (an +unoccupied IV-class Saturday) itself is kept unchanged -- distinct from +`own` precisely because a votive Mass is not the day's own, even though +`said` names the identical slug either way. + +A year outside the supported domain is rejected (exit 2): + + $ colitur rubrics 1000 + colitur: year 1000 out of range 1583..9999 + [2] + +--overlay is accepted, the same reasoning `readings` gets it: an overlay can +change which celebration is observed, hence which Mass formulary is said. +Baseline, the unoccupied Saturday says Our Lady's votive Mass; with the +diocesan overlay's local patron observed instead (no proper or Common of his +own in the fixture), the chain falls all the way back to step 3: + + $ colitur rubrics 2026 --overlay fixtures/overlay-example-diocesan.sexp | grep '^2026-07-11' + 2026-07-11 ef-time-after-pentecost-sunday-6 preceding-sunday + $ colitur rubrics 2026 | grep '^2026-07-11' + 2026-07-11 ef-time-after-pentecost-6-saturday votive + +`--lang`/`--raw`/`--sigla-*` are refused rather than silently ignored, unlike +`readings`: this row resolves no display name and no citation for any of +them to affect. + + $ colitur rubrics 2026 --lang en + colitur: --lang/--raw have no effect on `rubrics`; refusing rather than ignoring them + [2] + + $ colitur rubrics 2026 --raw + colitur: --lang/--raw have no effect on `rubrics`; refusing rather than ignoring them + [2] + + $ colitur rubrics 2026 --sigla-style en + colitur: --sigla-style/--sigla-book/--sigla-tradition have no effect on `rubrics`; refusing rather than ignoring them + [2] + +`colitur day` and `colitur readings` are unaffected by any of the above: + + $ colitur day 2026 --raw | head -1 + 2026-01-01 thursday christmastide - ef-circumcision class-1 white + $ colitur readings 2026 --raw | head -1 + 2026-01-01 ef-circumcision | Titus 2:11-15 | Luke 2:21 + An explicit COLITUR_DATA_DIR that does not hold the data is an error (exit 2), never a silent fall-back to some other copy. A packager or operator who names a directory has stated an intent; quietly calendaring off a different one @@ -247,7 +332,7 @@ The error path is the mirror image -- nothing on stdout, exit 2: Every command the help lists is a command the binary actually accepts. This is the check that catches help text drifting away from the dispatch: - $ for c in easter temporal day readings; do colitur $c 2026 > /dev/null || echo "$c FAILED"; done + $ for c in easter temporal day readings rubrics; do colitur $c 2026 > /dev/null || echo "$c FAILED"; done --version prints the version alone, to standard output, exit 0. Deliberately not embedded in the help text above: this pin would then have to be edited @@ -327,14 +412,14 @@ A flag needing a value, given none: $ colitur day 2026 --overlay colitur: --overlay needs a file path - colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> | colitur emit --format FMT --from Y --to Y | colitur table --year Y --template FILE | colitur render --template FILE --year Y | colitur publish --from Y --to Y --out DIR | colitur lang --list|--dump CODE|--check FILE | colitur config --show | colitur check FILE | colitur new-overlay (try: colitur --help) + colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> | colitur rubrics <year> | colitur emit --format FMT --from Y --to Y | colitur table --year Y --template FILE | colitur render --template FILE --year Y | colitur publish --from Y --to Y --out DIR | colitur lang --list|--dump CODE|--check FILE | colitur config --show | colitur check FILE | colitur new-overlay (try: colitur --help) [2] An unknown option is rejected rather than treated as a positional word: $ colitur day 2026 --diocese colitur: unknown option --diocese - colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> | colitur emit --format FMT --from Y --to Y | colitur table --year Y --template FILE | colitur render --template FILE --year Y | colitur publish --from Y --to Y --out DIR | colitur lang --list|--dump CODE|--check FILE | colitur config --show | colitur check FILE | colitur new-overlay (try: colitur --help) + colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> | colitur rubrics <year> | colitur emit --format FMT --from Y --to Y | colitur table --year Y --template FILE | colitur render --template FILE --year Y | colitur publish --from Y --to Y --out DIR | colitur lang --list|--dump CODE|--check FILE | colitur config --show | colitur check FILE | colitur new-overlay (try: colitur --help) [2] The shipped example overlay is runnable documentation, and it must actually |
