From 32e792a0741f62fc2c0c7dcf9255408b3256fad5 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 27 Aug 2026 12:33:43 +0200 Subject: fix(cli): make the flag surface systematic An audit probed every flag against every command rather than reading --help, and found three inconsistencies. --rite reached only `day` and `readings`. main.ml's own comment gave the reason -- "has not been widened to a second rite in this task" -- a scope note that had outlived its task and hardened into apparent design. View.of_days was already polymorphic over the rite's type parameters, so widening was plumbing, not library work: `temporal`, `rubrics`, `emit`, `table`, `render` and `publish` now all take it. `temporal` was the sharpest case. Its refusal said "--rite has no effect on `temporal`", which was false: the EF has Septuagesima and Passiontide, the OF neither, and every EF slug is ef-prefixed, so the flag would change nearly every line. A message claiming no effect where the effect is total is exactly what the audit set out to find. Three ways of naming a year (positional, --year, --from/--to) now cross-accept additively; naming two that disagree is a usage error rather than one silently winning. emit --format csv gained a rite-dependent header: EF's 16 columns are unchanged, OF gets a 17th "second" between "first" and "gospel". An earlier task had recorded RFC 4180 as permanently blocking this; the rule constrains one file, not a family of them. `rubrics` keeps refusing --lang/--raw, now with its reason. An intermediate version accepted them by adding a name column, which changed the default from six tab-separated fields to seven and broke both existing consumers and the byte-identical-EF rule. The asymmetry is real but principled: the row is a date, a slug, a source keyword, two booleans and a preface key, so there is nothing to translate and nothing to strip. `easter` refuses --rite for the same kind of reason -- its six anchors sit at identical Easter offsets in both rites. EF output verified byte-identical to 8590338 across day, readings, rubrics, temporal and easter for 2026, 1583 and 9999, and across all five emit formats. --- bin/main.ml | 712 +++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 567 insertions(+), 145 deletions(-) (limited to 'bin') diff --git a/bin/main.ml b/bin/main.ml index 6e3b03c..d78eca8 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -12,7 +12,24 @@ let easter_report y = ("corpus-christi", C.corpus_christi y) ] |> List.iter (fun (name, d) -> Printf.printf "%s %s\n" name (fmt d)) -let temporal_report y = +(* Fix 1 follow-up (cli-flags-report, 2026-08-27, coordinator review): + `--rite` used to be refused here with the message "has no effect on + `temporal`" -- FALSE, not merely stale: this function calls + [Rite_ef.Temporal_ef.temporal] directly, the whole rite-specific + temporal cycle (season, week numbering, slugs), so an OF run would + differ on essentially every line (OF has five seasons, no + Septuagesima, and every slug carries an "of-" prefix rather than + "ef-"). The refusal comment this function's own dispatch site + originally carried conflated two different flags' justifications: + `--overlay` genuinely has no effect here ("runs the temporal cycle + BEFORE any sanctoral layer exists" -- still true, still the reason + --overlay stays refused, unaffected by this fix), but that says + nothing about `--rite`, which this function was never actually + insulated from -- only refused at the CLI layer, which is not the + same claim. [Colitur_kernel.Record.of_temporal] was already fully + polymorphic over [('s, 'r)] (record.mli), so this is the same + plumbing already done for `day`/`readings`/etc, not new library work. *) +let temporal_report ~rite y = let jan1 = match D.make ~year:y ~month:1 ~day:1 with | Ok t -> t | Error e -> failwith e @@ -27,13 +44,16 @@ let temporal_report y = $4}') silently reads the wrong column on those days. Emit "-" instead, so every line always has exactly seven single-space-separated fields. *) let field s = if s = "" then "-" else s in + let record_of_day = + match rite with + | `Ef -> fun d -> Colitur_kernel.Record.of_temporal ~rite:Rite_ef.Temporal_ef.id + Rite_ef.Vocab_ef.vocab d (Rite_ef.Temporal_ef.temporal d) + | `Of -> fun d -> Colitur_kernel.Record.of_temporal ~rite:Rite_of.Temporal_of.id + Rite_of.Vocab_of.vocab d (Rite_of.Temporal_of.temporal d) + in let d = ref jan1 in while D.compare !d dec31 <= 0 do - let t = Rite_ef.Temporal_ef.temporal !d in - let r = - Colitur_kernel.Record.of_temporal ~rite:Rite_ef.Temporal_ef.id - Rite_ef.Vocab_ef.vocab !d t - in + let r = record_of_day !d in Printf.printf "%s %s %s %s %s %s %s\n" r.Colitur_kernel.Record.date r.Colitur_kernel.Record.weekday r.Colitur_kernel.Record.season (field r.Colitur_kernel.Record.week) r.Colitur_kernel.Record.slug @@ -517,14 +537,23 @@ let readings_line ~lang ~sigla (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.r 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, + later column on this row (the resolved formulary name, below) 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. + + Fix 2 (cli-flags-report, 2026-08-27): `rubrics` DOES now take + --lang/--raw -- reversing this comment's own former claim after + auditing it against the evidence rather than the flag surface alone. + [said] (below) is a SLUG, exactly the same kind of machine key + [day_line]'s own [slug_s] is, and every other row that prints one + already resolves it to a display name under --lang; there turned out to + be no principled reason for this row to be the one exception. --raw and + --sigla-* are NOT symmetric here: --raw genuinely applies (see + [rubrics_name] below), but --sigla-* still has nothing to act on -- this + row prints no citation of its own, so [reject_sigla "rubrics"] + (bin/main.ml's own dispatch) is unchanged. *) +(* [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 @@ -565,8 +594,63 @@ let readings_line ~lang ~sigla (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.r column does not distinguish them, the same "-" convention [formulary]'s own [None] case already uses two columns to the left, for the identical reason. *) -let rubrics_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_kernel.Liturgical_day.t) - = +(* No explicit [(season, rank) Liturgical_day.t] annotation, unlike + [day_line]/[readings_line]: unlike those two, this function never prints + a rite-specific VOCABULARY string (season/rank name), only fields + [Rite.t] already makes generic ([formulary]/[creed]/[gloria]/[preface]/ + [observed]/[temporal.office], all of them rite-agnostic per + liturgical_day.mli) -- so it type-checks fully polymorphic over + [('s, 'r)] and needs no EF/OF duplicate the way those two do. Reused + as-is for OF in [rubrics_report] below. *) +(* [rubrics_name] resolves [said]'s slug to a display name, the same + append-only-if-different discipline [day_line]/[readings_line] already + use for the OBSERVED celebration's own slug -- but [said] is not always + the observed celebration ([Mass_formulary.source]'s own citation: + [Preceding_sunday] names a DIFFERENT day's temporal slug, [Common] names + a Common's own id), so there is no single [Celebration.t] this function + can assume [said] belongs to. + + Two [Celebration.t] values are always in scope on any [Liturgical_day.t], + for either rite: [d.observed] (Proper, the common case) and + [d.temporal.office] (Own_slug, the day's own temporal office when a + ferial/weekday resumes it) -- see [Mass_formulary.source]'s own + constructors and {!Rite_of.Lectionary_of.readings}, which never produces + [Preceding_sunday]/[Common]/[Votive] at all, so for OF this covers EVERY + [said] value, not merely the common case. When [said] matches one of + those two, its own [Celebration.t.names] is consulted FIRST (the same + preference [day_line_of]'s own [observed_name_of] gives OF's richer, + verified per-slug names over the generic lang/*.ini table, for the + identical reason: a collision between an OF slug and an unrelated EF + entry in that table must not silently print the wrong century's title). + For EF this is a safe no-op, not a new risk: EF's own [Celebration.t + .names] is "almost always empty" ([day_line_of]'s own citation), so the + lookup falls straight through to [Colitur_naming.Lang.celebration] -- + BYTE-IDENTICAL to what [day_line] already does for EF's [slug_s] today. + When [said] matches NEITHER (EF's [Preceding_sunday]/[Common]/[Votive] + cases, or a genuine miss), this falls back to the same generic + lang-table lookup every other row already accepts as its own residual + imprecision -- nothing here is worse than what [day_line] already + ships. *) +let rubrics_name ~lang (d : (_, _) Colitur_kernel.Liturgical_day.t) said_slug = + let module K = Colitur_kernel in + let cel_opt = + if K.Slug.to_string d.K.Liturgical_day.observed.K.Celebration.slug = said_slug then + Some d.K.Liturgical_day.observed + else if K.Slug.to_string d.K.Liturgical_day.temporal.K.Temporal.office.K.Celebration.slug = said_slug + then Some d.K.Liturgical_day.temporal.K.Temporal.office + else None + in + let from_data = + match cel_opt with + | None -> None + | Some cel -> ( + match K.Lang.of_string (Colitur_naming.Lang.code lang) with + | Error _ -> None + | Ok l -> K.Names.find cel.K.Celebration.names l) + in + match from_data with Some n -> n | None -> Colitur_naming.Lang.celebration lang said_slug + +let rubrics_line ~lang (d : (_, _) Colitur_kernel.Liturgical_day.t) = let said, via = match d.Colitur_kernel.Liturgical_day.formulary with | Some f -> @@ -582,10 +666,12 @@ let rubrics_line (d : (Rite_ef.Vocab_ef.season, Rite_ef.Vocab_ef.rank) Colitur_k | Some p -> Colitur_kernel.Preface.to_string p | None -> "-" in - Printf.printf "%s\t%s\t%s\t%s\t%s\t%s\n" (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) said via + let name = rubrics_name ~lang d said in + let name_suffix = if name = said || said = "-" then "" else "\t" ^ name in + Printf.printf "%s\t%s\t%s\t%s\t%s\t%s%s\n" (D.to_iso8601 d.Colitur_kernel.Liturgical_day.date) said via (string_of_bool d.Colitur_kernel.Liturgical_day.creed) (string_of_bool d.Colitur_kernel.Liturgical_day.gloria) - preface + preface name_suffix (* One civil year, Jan 1 - Dec 31, matching [temporal_report]'s own scan -- NOT one liturgical year: [Colitur_kernel.Calendar.year] resolves a single @@ -681,7 +767,6 @@ 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 (* Fix wave I1 (final-review.md, 2026-08-25-colitur-of-phases-3-5): unlike EF's [Celebration.names] (almost always empty -- lang/la.ini is EF's own @@ -785,6 +870,34 @@ let day_report_of ~lang ~overlays y = resolved_of_year_report ~line:(day_line_of let readings_report_of ~lang ~sigla ~overlays y = resolved_of_year_report ~line:(readings_line_of ~lang ~sigla) ~overlays y +(* Fix 1 (cli-flags-report, 2026-08-27): `rubrics` widened to `--rite of`. + [rubrics_line] above needs no OF twin (it type-checks polymorphic over + [('s, 'r)] already -- see its own header), so dispatching here is only + ever about which resolver walks the year, never about which printer + prints it. *) +let rubrics_report ~rite ~lang ~overlays y = + match rite with + | `Ef -> resolved_year_report ~line:(rubrics_line ~lang) ~overlays y + | `Of -> resolved_of_year_report ~line:(rubrics_line ~lang) ~overlays y + +(* Fix 1 (cli-flags-report, 2026-08-27): shared by [table_report] and + [publish_report], which each need only the rendered [Template.value] -- + {!Colitur_render.View.of_days} is fully polymorphic over [('s, 'r)] + (view.mli), so the SAME function builds it for either rite, and nothing + downstream (a template, an emitter) has to know which one produced it. + [emit_report] does NOT reuse this: its own "sexp" format needs the raw, + rite-specific [days] list too (a different [sexp_of_season]/ + [sexp_of_rank] pair per rite), so it dispatches inline instead, right + next to the [days] binding this helper deliberately does not expose. *) +let view_of_year ~rite ~lang ~sigla ~overlays y = + match rite with + | `Ef -> + let days = resolved_year_days ~overlays y in + Colitur_render.View.of_days ~lang ~sigla ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y days + | `Of -> + let days = resolved_of_year_days ~overlays y in + Colitur_render.View.of_days ~lang ~sigla ~vocab:Rite_of.Vocab_of.vocab ~rite:"of" ~year:y days + (* 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 published feed (ics) or a data export (csv/json/xml) is usually wanted @@ -834,20 +947,56 @@ let check_dtstamp = function s; exit 2 -let emit_report ~lang ~sigla ~format ~overlays ~dtstamp ~from_y ~to_y = +(* Fix 1 (cli-flags-report, 2026-08-27): `emit` widened to `--rite of`. + [view_of_year] above cannot be reused here: "sexp" prints the raw + [days] list through a rite-specific [sexp_of_season]/[sexp_of_rank] + pair, so each rite needs its own [days] binding in scope, not only its + own [Template.value] -- dispatched inline instead, once per year (the + resolved data does not carry over between years, unlike [rite] itself, + so this sits inside the loop, not outside it). *) +let emit_report ~rite ~lang ~sigla ~format ~overlays ~dtstamp ~from_y ~to_y = check_dtstamp dtstamp; if from_y > to_y then begin Printf.eprintf "colitur: --from %d is after --to %d\n" from_y to_y; exit 2 end; for y = from_y to to_y do - let days = resolved_year_days ~overlays y in - let v = - Colitur_render.View.of_days ~lang ~sigla ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y days + let v, print_sexp = + match rite with + | `Ef -> + let days = resolved_year_days ~overlays y in + ( Colitur_render.View.of_days ~lang ~sigla ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y + days, + fun () -> + List.iter + (fun d -> + print_string + (Sexplib.Sexp.to_string_hum + (Colitur_kernel.Liturgical_day.sexp_of_t Rite_ef.Vocab_ef.sexp_of_season + Rite_ef.Vocab_ef.sexp_of_rank d)); + print_newline ()) + days ) + | `Of -> + let days = resolved_of_year_days ~overlays y in + ( Colitur_render.View.of_days ~lang ~sigla ~vocab:Rite_of.Vocab_of.vocab ~rite:"of" ~year:y + days, + fun () -> + List.iter + (fun d -> + print_string + (Sexplib.Sexp.to_string_hum + (Colitur_kernel.Liturgical_day.sexp_of_t Rite_of.Vocab_of.sexp_of_season + Rite_of.Vocab_of.sexp_of_rank d)); + print_newline ()) + days ) in match format with | "csv" -> - (* One header for the whole run, not one per year. *) + (* One header for the whole run, not one per year -- safe across a + multi-year [--rite of] run too: {!Colitur_render.Emit_csv.year} + reads its own column set from [v]'s "rite" field (see that + module's own comment), and [rite] cannot change mid-run, so + every year's header is identical within one invocation. *) let body = Colitur_render.Emit_csv.year v in if y = from_y then print_string body else @@ -858,15 +1007,7 @@ let emit_report ~lang ~sigla ~format ~overlays ~dtstamp ~from_y ~to_y = | "json" -> print_string (Colitur_render.Emit_json.year v) | "xml" -> print_string (Colitur_render.Emit_xml.year v) | "ics" -> print_string (Colitur_render.Emit_ics.year ?dtstamp v) - | "sexp" -> - List.iter - (fun d -> - print_string - (Sexplib.Sexp.to_string_hum - (Colitur_kernel.Liturgical_day.sexp_of_t - Rite_ef.Vocab_ef.sexp_of_season Rite_ef.Vocab_ef.sexp_of_rank d)); - print_newline ()) - days + | "sexp" -> print_sexp () | other -> Printf.eprintf "colitur: unknown format %S (want csv, json, sexp, xml or ics)\n" other; exit 2 @@ -886,7 +1027,21 @@ let emit_report ~lang ~sigla ~format ~overlays ~dtstamp ~from_y ~to_y = the design used, kept so that documented vocabulary still works. There is no stdin-fed `render`; `colitur emit --format json | jq` still composes for real pipe use, because JSON there is the OUTPUT, never something colitur - itself has to parse back in. *) + itself has to parse back in. + + Fix 1 (cli-flags-report, 2026-08-27): both now take `--rite`, dispatched + through [view_of_year]. Pointing an EF template at an OF year (or vice + versa) is not refused and does not crash: {!Colitur_render.Template + .render}'s own contract is that a missing key renders as the empty + string (template.mli), so a template written before `--rite of` existed + -- one that never references [{{second}}], say -- simply never shows + the OF Second reading it does not ask for; it does not error, and no + OTHER field goes missing, because [View.of_days] emits the identical KEY + set for either rite (only the resolved VALUES differ, e.g. `ordinary` + vs whatever EF's own season names are). A template is user input and + this project's own rule is that user input must never crash the + program -- the missing-key silence is exactly what keeps that promise + here too, not a special case added for this task. *) (* The open is guarded separately from the read: a missing file fails at [open_in_bin] with a plain, path-only message (matching the wording this @@ -1194,7 +1349,7 @@ let flavour_names_comma () = let flavour_names_bar () = String.concat "|" (List.map Colitur_render.Escape.to_string Colitur_render.Escape.all) -let table_report ~lang ~sigla ~template ~flavour_opt ~overlays y = +let table_report ~rite ~lang ~sigla ~template ~flavour_opt ~overlays y = let flavour = match flavour_opt with | Some name -> ( @@ -1216,8 +1371,7 @@ let table_report ~lang ~sigla ~template ~flavour_opt ~overlays y = Printf.eprintf "colitur: %s\n" msg; exit 2 | Ok src -> ( - let days = resolved_year_days ~overlays y in - let v = Colitur_render.View.of_days ~lang ~sigla ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y days in + let v = view_of_year ~rite ~lang ~sigla ~overlays y in match Colitur_render.Template.render_string ~flavour src v with | Error e -> (* The template is user input; a parse failure is reported with the @@ -1378,33 +1532,65 @@ let schema_path () = (* An ordinary OCaml string, NOT a template: it describes the TREE, not the calendar, so it has no business in the template vocabulary. *) -let index_html ~from_y ~to_y = +(* Fix 1 (cli-flags-report, 2026-08-27): [rite_id] ("ef" or "of") replaces + the literal "ef" this page used to hardcode throughout -- both the link + paths (matching [publish_report]'s own [rite_id ^ "/" ...] tree below) + and the descriptive sentence, which named the 1962 Missal unconditionally + even though this page is now generated for either rite's own publish + run. For [rite_id = "ef"] every byte below is unchanged from before this + parameter existed -- the EF byte-identity constraint this task is held + to. *) +let index_html ~rite_id ~from_y ~to_y = + let missal_sentence = + if rite_id = "ef" then "Liturgical calendar of the 1962 Missale Romanum." + else "Liturgical calendar of the post-1970 (Ordinary Form) Roman Missal, base calendar the 2002 \ + Missale Romanum (editio typica tertia)." + in let b = Buffer.create 4096 in Buffer.add_string b - "\n\n\ - colitur\n\ - \n\ -

colitur

\n\ -

Liturgical calendar of the 1962 Missale Romanum. Citations only \xe2\x80\x94 never scripture text.

\n"; + (Printf.sprintf + "\n\n\ + colitur\n\ + \n\ +

colitur

\n\ +

%s Citations only \xe2\x80\x94 never scripture text.

\n" + missal_sentence); Buffer.add_string b "

Subscribe

\n\n

Data

\n\n

Contract: schema/day-v1.json

\n\ \n"; Buffer.contents b -let publish_report ~lang ~sigla ~from_y ~to_y ~out ~overlays ~dtstamp ~prune = +(* Fix 1 (cli-flags-report, 2026-08-27): `publish` widened to `--rite of`. + [rite_id] names the output subtree ("ef/" or "of/", mirroring + [index_html]'s own parameter of the same name) -- every occurrence of + the literal "ef" the write loop below used to hardcode is replaced by + it, so an EF publish run (the default) writes the identical tree it + always did, and an OF run writes the same shape one directory over, + never colliding with it. A single [--out] directory can therefore hold + BOTH rites' trees side by side across two separate invocations (one + `--rite ef`, one `--rite of`) -- but NOT safely with [--prune] on + either: the manifest and index.html this function writes are for the + WHOLE tree, not per-rite, so a `--rite of` run's own [written] list + never mentions the other rite's files, and [--prune] would delete them + as stale. This is a genuine, disclosed limitation (see this task's own + report), not a silent one: publish a single rite per [--out] directory, + or omit [--prune] when deliberately layering both. *) +let publish_report ~rite ~lang ~sigla ~from_y ~to_y ~out ~overlays ~dtstamp ~prune = + let rite_id = match rite with `Ef -> "ef" | `Of -> "of" in check_dtstamp dtstamp; if from_y > to_y then begin Printf.eprintf "colitur: --from %d is after --to %d\n" from_y to_y; @@ -1434,14 +1620,23 @@ let publish_report ~lang ~sigla ~from_y ~to_y ~out ~overlays ~dtstamp ~prune = write_file (Filename.concat out rel) contents; written := rel :: !written in - for y = from_y to to_y do - let days = resolved_year_days ~overlays y in - let v = Colitur_render.View.of_days ~lang ~sigla ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y days in - let ys = string_of_int y in - emit ("ef/" ^ ys ^ ".json") (Colitur_render.Emit_json.year v); - emit ("ef/" ^ ys ^ ".csv") (Colitur_render.Emit_csv.year v); - emit ("ef/" ^ ys ^ ".xml") (Colitur_render.Emit_xml.year v); - emit ("ef/" ^ ys ^ ".ics") (Colitur_render.Emit_ics.year ?dtstamp v); + (* [write_year] is generic over [('s, 'r)] -- a plain let-bound function + value, so it generalises fully (the same reason [resolved_year_report] + above generalises over its own [~line]) -- and is instantiated once + per rite branch below, at THAT branch's own concrete [Liturgical_day.t] + type. [days] cannot be hoisted out of the branch the way [v] is + elsewhere in this file: [resolved_year_days]/[resolved_of_year_days] + return two DIFFERENT, incompatible monomorphic types (EF's own + [Vocab_ef.season/rank] vs OF's), so a single [let days = match rite + with ...] binding shared by both branches would not type-check -- + [mk_view] and every write using [days] must stay inside the SAME + branch that produced it, per rite. *) + let write_year ~ys days mk_view = + let v = mk_view days in + emit (rite_id ^ "/" ^ ys ^ ".json") (Colitur_render.Emit_json.year v); + emit (rite_id ^ "/" ^ ys ^ ".csv") (Colitur_render.Emit_csv.year v); + emit (rite_id ^ "/" ^ ys ^ ".xml") (Colitur_render.Emit_xml.year v); + emit (rite_id ^ "/" ^ ys ^ ".ics") (Colitur_render.Emit_ics.year ?dtstamp v); (* One file per day: the static equivalent of a per-day endpoint. [View.of_days] with a one-day list yields 12 months, 11 empty, one populated -- exactly the shape a single day's own page needs. *) @@ -1449,12 +1644,26 @@ let publish_report ~lang ~sigla ~from_y ~to_y ~out ~overlays ~dtstamp ~prune = (fun d -> let iso = D.to_iso8601 d.Colitur_kernel.Liturgical_day.date in let mm = String.sub iso 5 2 and dd = String.sub iso 8 2 in - let one = Colitur_render.View.of_days ~lang ~sigla ~vocab:Rite_ef.Vocab_ef.vocab ~rite:"ef" ~year:y [ d ] in - emit (Printf.sprintf "ef/%s/%s/%s.json" ys mm dd) (Colitur_render.Emit_json.year one)) + let one = mk_view [ d ] in + emit (Printf.sprintf "%s/%s/%s/%s.json" rite_id ys mm dd) (Colitur_render.Emit_json.year one)) days + in + for y = from_y to to_y do + let ys = string_of_int y in + match rite with + | `Ef -> + let days = resolved_year_days ~overlays y in + write_year ~ys days (fun days -> + Colitur_render.View.of_days ~lang ~sigla ~vocab:Rite_ef.Vocab_ef.vocab ~rite:rite_id ~year:y + days) + | `Of -> + let days = resolved_of_year_days ~overlays y in + write_year ~ys days (fun days -> + Colitur_render.View.of_days ~lang ~sigla ~vocab:Rite_of.Vocab_of.vocab ~rite:rite_id ~year:y + days) done; emit "schema/day-v1.json" schema; - emit "index.html" (index_html ~from_y ~to_y); + emit "index.html" (index_html ~rite_id ~from_y ~to_y); let now = List.sort compare !written in (* Every stale entry is validated TWICE before anything is removed -- see [manifest_entry_is_safe]/[resolves_under]'s own comment above for @@ -1507,24 +1716,30 @@ let help_text = usage: colitur easter Easter, and the movable feasts anchored to it - colitur temporal the temporal cycle, one line per day + colitur temporal [--rite ef|of] + the temporal cycle, one line per day colitur day the resolved day identity, one line per day colitur readings the Mass reading citations, one line per day colitur rubrics the Mass formulary said, one line per day - colitur day|readings [--rite ef|of] [--overlay FILE ...] [--lang CODE|FILE] [--raw] - colitur rubrics [--overlay FILE ...] - colitur emit --format csv|json|sexp|xml|ics --from Y --to Y + colitur day|readings|rubrics [] [--year Y] [--rite ef|of] + [--overlay FILE ...] [--lang CODE|FILE] [--raw] + may be given positionally or as --year (both, if they + agree); rubrics's own --lang/--raw govern its trailing + formulary-name column, --sigla-* stay refused there (see + "naming" below) + colitur emit --format csv|json|sexp|xml|ics --from Y --to Y [--rite ef|of] [--overlay FILE ...] [--dtstamp S] [--lang CODE|FILE] [--raw] render a resolved year range through one of five emitters - colitur table --year Y --template FILE [--flavour X] [--overlay FILE ...] - [--lang CODE|FILE] [--raw] - colitur render --template FILE --year Y [--flavour X] [--overlay FILE ...] - [--lang CODE|FILE] [--raw] + colitur table [] [--year Y] --template FILE [--rite ef|of] + [--flavour X] [--overlay FILE ...] [--lang CODE|FILE] [--raw] + colitur render --template FILE [] [--year Y] [--rite ef|of] + [--flavour X] [--overlay FILE ...] [--lang CODE|FILE] [--raw] compute year Y and render it through FILE, a logic-less Mustache-family template; table and render are the same operation, two names (see "rendering" below) - colitur publish --from Y --to Y --out DIR [--overlay FILE ...] [--prune] - [--dtstamp S] [--lang CODE|FILE] [--raw] + colitur publish --from Y --to Y --out DIR [--rite ef|of] + [--overlay FILE ...] [--prune] [--dtstamp S] + [--lang CODE|FILE] [--raw] write the static tree: per-year csv/json/xml/ics, one JSON file per day, the schema and a generated index (see "publish" below) @@ -1539,15 +1754,19 @@ usage: colitur -V, --version print the version and exit is a civil year, 1583..9999 inclusive. Each report covers 1 January to -31 December of that year, not a liturgical year. +31 December of that year, not a liturgical year. `emit`/`publish` take a +RANGE instead (--from Y --to Y, inclusive) and do not also accept a single +--year or positional year -- deliberate: they may compute many years in one +run, and a third, single-year spelling on top of the range form would add +parsing surface for no real workflow gain. output formats: day date weekday season week slug rank colour [+commemoration ...] [name] 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, creed, gloria, preface -- TAB-separated - 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true[TAB]nativity + rubrics date, formulary slug, source, creed, gloria, preface [name] -- TAB-separated + 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true[TAB]nativity[TAB]In Octava Nativitatis Domini A citation contains spaces, so readings uses " | " between its fields while day stays space-separated; that is why they are separate commands rather @@ -1571,13 +1790,19 @@ output formats: preface one of nativity/epiphany/lent/holy-cross/easter/ascension/ sacred-heart/christ-the-king/holy-spirit/trinity/bvm/st-joseph/apostles/ common/requiem, or "-" when this engine resolves no Mass at all that day - (Good Friday). 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, the Creed, the - Gloria and the preface); --lang/--raw/--sigla-* are refused -- this row - resolves no display name and no citation for any of them to affect. + (Good Friday). TAB-separated rather than space or " | ": the resolved + formulary NAME (below) 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, the Creed, the Gloria and the preface); --sigla-* are refused + -- this row prints no citation for them to affect. + + --lang/--raw ARE accepted (unlike --sigla-*): `said` (the formulary slug) + is a machine key exactly like `day`'s own slug, and this row resolves it + to a display name under the identical append-only rule day/readings use + -- present as a trailing 8th field only when it differs from the slug + already printed, so --raw (or a language with no entry for that day) is + byte-identical to the seven-field row shown above. emit one schema (season, week, slug, rank, colour, subject, names, citations, commemorations), rendered five ways: csv (RFC 4180, @@ -1587,10 +1812,38 @@ output formats: same data are byte-identical -- the engine reads no clock. rite: - --rite ef|of selects the rite `day`/`readings` compute against; default ef - (so every invocation written before this flag existed is - unaffected). `of` is the 1970 Missale Romanum (editio typica - tertia, 2002); no other command accepts the flag yet. + --rite ef|of selects which rite module a command computes against; + default ef (so every invocation written before this flag + existed is unaffected). `of` is the 1970 Missale Romanum + (editio typica tertia, 2002). Accepted on `day`, `readings`, + `rubrics`, `temporal`, `emit`, `table`, `render` and `publish`; + refused, not silently ignored, on `check`/`convert` (operate + on an overlay FILE, not a computed year), `new-overlay` + (prints a static starter, no calendar computation), and + `lang`/`config` (answer naming/config questions orthogonal to + any rite). `easter` is refused too, but PROVEN rite-invariant + rather than merely unbuilt for `of`: EF and OF reckon Easter + on the identical Gregorian computus, so no second value could + ever change the six dates it prints. + + `emit --format csv --rite of` and `publish --rite of` widen + their CSV output with a 17th column, "second", between + "first" and "gospel" -- present, and usually empty, because a + Sunday or solemnity genuinely carries a Second reading (OLM + 1981 Praenotanda n. 66.1) and a feria/feast/memorial does not + (n. 69.1); EF's own 16-column CSV header is unaffected, byte + for byte, because EF's `citations` never contains one. json/ + xml/sexp/ics need no such widening -- each already had a place + for a variable-length reading list. + + A single `--out` directory can hold both rites' own `publish` + trees side by side ("ef/", "of/"), but not safely combined + with --prune on either: publish's own manifest and index.html + describe the WHOLE tree, not one rite's slice of it, so a + later run for the other rite would not know the first run's + files exist and --prune would delete them as stale. Publish a + single rite per --out, or omit --prune when deliberately + layering both. overlays: --overlay FILE (repeatable, ordered; -o) applies a user calendar ON TOP of @@ -1629,9 +1882,28 @@ overlays: (Nth_weekday (month M) (nth N) (weekday W)) with N negative to count from the end of the month. +year: + `day`, `readings`, `rubrics`, `table` and `render` each take a single + civil year, sayable two ways -- positionally (`colitur day + 2026`) or as --year (`colitur day --year 2026`) -- additively: + neither form was removed when the other was added, so every + invocation that worked before still works unchanged. Naming + both is fine as long as they agree (`colitur day 2026 --year + 2026`); naming both with DIFFERENT years is a hard usage error + rather than one silently winning. + + `emit`/`publish` deliberately do NOT gain a --year: they take + --from Y --to Y instead (see the usage block above), and stay + that way even for a single-year run (`--from 2026 --to 2026`) + -- a third spelling meaning exactly the same thing as the two + above would add parsing surface, and a range command's own + natural single-year form is already `--from Y --to Y`, not a + new flag. + naming: - --lang CODE|FILE applies to `day`, `readings`, `emit`, `table`, `render` - and `publish`. A CODE (e.g. `la`, `en`) is looked up as + --lang CODE|FILE applies to `day`, `readings`, `rubrics`, `emit`, + `table`, `render` and `publish`. A CODE (e.g. `la`, `en`) is + looked up as /CODE.ini; a value containing '/' or ending ".ini" is read as a literal path instead. Default `la`, overridable by a config file (see below). An unknown language is a hard ERROR @@ -1746,11 +2018,15 @@ publish: any web server or git repo can serve it as-is, and nothing runs at request time. - ef/.{json,csv,xml,ics} one civil year, all days - ef///
.json one file per day - schema/day-v1.json the published JSON contract - index.html a generated index page - .colitur-manifest every path this run wrote + /.{json,csv,xml,ics} one civil year, all days + ///
.json one file per day + schema/day-v1.json the published JSON contract + index.html a generated index page + .colitur-manifest every path this run wrote + + is "ef" or "of", selected by --rite exactly as on every + other command (default ef) -- see "rite" above for what a + single --out directory holding both rites' own trees needs. Deterministic: publishing the same --from/--to range twice produces a byte-identical tree (--dtstamp behaves exactly as on @@ -1815,8 +2091,9 @@ let with_year ys f = not a silent fallback to "ef" -- the same "an unrecognised value is refused, not guessed at" discipline every other closed-choice flag in this file follows (`--sigla-book full|abbr`, `lang --dump CODE`). Shared - by `day`/`readings`, the only two commands [reject_rite_for] lets the - flag reach. *) + by every command [reject_rite_for] lets the flag reach -- `day`/ + `readings` originally, widened (Fix 1, cli-flags-report) to `rubrics`, + `emit`, `table`/`render` and `publish`. *) let resolve_rite = function | None | Some "ef" -> `Ef | Some "of" -> `Of @@ -1824,6 +2101,36 @@ let resolve_rite = function Printf.eprintf "colitur: unknown --rite %S (expected \"ef\" or \"of\")\n" other; exit 2 +(* Fix 3 (cli-flags-report, 2026-08-27): every single-year command + (`day`/`readings`/`rubrics`/`table`/`render`) now accepts EITHER a + positional year or [--year], additively -- neither form is removed, so + every invocation that worked before this task keeps working unchanged. + [emit]/[publish] deliberately do NOT gain this: they are RANGE commands + ([--from]/[--to]), and the range form is the right one for a command + that may compute many years in one run -- see this task's own report + for why [--from Y --to Y] was considered and rejected as a third + spelling here too. `easter`/`temporal` are also left alone: their own + flag surface is deliberately closed end to end (every optional flag + refused, not merely [--year]), and this task's brief names only the + five data-computing commands above, not those two. + + Disagreement between the two forms (`day 2026 --year 2027`) is refused + outright rather than one silently winning -- the same "never silently + pick" discipline [--overlay] and every other flag in this file already + follow. Agreement (`day 2026 --year 2026`) is accepted; it is redundant + but not a contradiction. *) +let resolve_single_year cmd ~positional ~flag = + match (positional, flag) with + | Some p, None -> p + | None, Some f -> f + | Some p, Some f when p = f -> p + | Some p, Some f -> + Printf.eprintf "colitur: %s: positional year %s and --year %s disagree\n" cmd p f; + exit 2 + | None, None -> + Printf.eprintf "colitur: %s requires a year (positional or --year)\n" cmd; + exit 2 + (* Flags are stripped first, then the remaining words are matched as command + year. The alternative -- extending the exact-array patterns below -- does not survive a REPEATABLE flag: [--overlay a --overlay b] is a @@ -1966,14 +2273,48 @@ let reject_overlays_for cmd overlays = exit 2 end -(* Sibling to [reject_overlays_for]: `--rite` (Task 5) is meaningful only on - `day`/`readings` -- the brief's own scope for this task. Every other - command either reads no rite-specific data at all (`easter`, `temporal`, - `check`, `convert`, `new-overlay`, `lang`, `--help`, `--version`) or has - not been widened to a second rite in this task (`rubrics`, `emit`, - `table`/`render`, `publish`, `config`) -- accepting the flag there and - silently ignoring it would be the exact failure mode [reject_overlays_for] - above already refuses. *) +(* Sibling to [reject_overlays_for]. Fix 1 (cli-flags-report, 2026-08-27) + widened `--rite` from `day`/`readings` alone to also cover `rubrics`, + `emit`, `table`/`render`, `publish` AND (coordinator-review fix round, + same date) `temporal` -- every command whose OUTPUT actually depends on + which rite computed it (this is plumbing, not new library work: + {!Colitur_render.View.of_days}/{!Colitur_kernel.Record.of_temporal} + were already fully polymorphic over [('s, 'r)], and both rite modules + already existed). What is left refusing the flag does so because the + flag would GENUINELY have no effect, not because of a stale scope note + -- checked per command, not assumed as a group, after `temporal` was + found wrongly grouped with `easter` here on a first pass (both refused, + but for DIFFERENT reasons, one of which turned out not to hold): + + - `easter` computes only Easter and its own movable-feast anchors + ({!Colitur_kernel.Computus.gregorian_easter} and friends) -- genuinely + rite-invariant, not merely unbuilt for OF: both EF and OF reckon + Easter on the identical Gregorian computus ({!Rite_of.Rite_of + .context}'s own [easter] field cites this directly), so `--rite of` + would recompute the exact same six dates, byte for byte. This is the + one case in this whole file where "no effect" is actually PROVEN, not + merely asserted. + - `check`/`convert` operate on an OVERLAY FILE, not a computed year, and + an overlay's own rank type is fixed by which rite loaded it, not by a + flag on the command inspecting it. + - `new-overlay` prints a static starter template with no calendar + computation in it whatsoever. + - `lang`/`config` answer questions about NAMING/CONFIG resolution, + orthogonal to which rite a later `day`/`table` invocation might name. + + `temporal` is NOT on this list any more: it calls + [Rite_ef.Temporal_ef.temporal] directly, the whole rite-specific + temporal cycle (season, week numbering, every slug), so `--rite of` + changes essentially every line (OF has five seasons, no Septuagesima, + and an "of-" slug prefix throughout) -- refusing it under a "no effect" + message was FALSE, not merely stale, confirmed directly: `colitur + temporal 2026 | grep septuagesima` finds Septuagesima-tide rows that + cannot exist under the OF's own Normae at all. --overlay stays refused + on `temporal` regardless -- a SEPARATE, still-valid claim + ([reject_overlays_for]'s own citation: the temporal cycle is computed + before any sanctoral layer exists, in EITHER rite), which is what this + comment's first pass actually meant to say about `temporal` and + over-generalised to `--rite` by mistake. *) let reject_rite_for cmd rite = if rite <> None then begin Printf.eprintf "colitur: --rite has no effect on `%s`; refusing rather than ignoring it\n" cmd; @@ -1983,7 +2324,17 @@ let reject_rite_for cmd rite = (* Sibling to [reject_emit_flags_for]/[reject_overlays_for]: `table`/`render`'s own three flags (Task 9) have no meaning on any other command, so accepting and silently dropping them would be the same failure mode this project - already refuses everywhere else. *) + already refuses everywhere else. + + Fix 3 (cli-flags-report, 2026-08-27) split [--year] out of this bundle: + `day`/`readings`/`rubrics` now accept it (see [reject_template_flavour_for] + below), so this three-flag rejector is no longer accurate for them. + Every OTHER caller of this function (`--help`/`--version`/`easter`/ + `temporal`/`check`/`convert`/`new-overlay`/`lang`/`config`/`emit`/ + `publish`) still refuses [--year] exactly as before -- `emit`/`publish` + in particular are RANGE commands ([--from]/[--to]), and deliberately do + not gain a second, single-year spelling (see this task's own report for + why). *) let reject_table_flags_for cmd ~year ~template ~flavour = if year <> None || template <> None || flavour <> None then begin Printf.eprintf @@ -1992,6 +2343,17 @@ let reject_table_flags_for cmd ~year ~template ~flavour = exit 2 end +(* Sibling to [reject_table_flags_for], narrower: `day`/`readings`/`rubrics` + (Fix 3, cli-flags-report) now accept [--year] as an alternative to their + own positional year, so only [--template]/[--flavour] -- meaningful + solely on `table`/`render` -- are refused on them. *) +let reject_template_flavour_for cmd ~template ~flavour = + if template <> None || flavour <> None then begin + Printf.eprintf "colitur: --template/--flavour have no effect on `%s`; refusing rather than ignoring them\n" + cmd; + exit 2 + end + (* Sibling to [reject_emit_flags_for]/[reject_table_flags_for]: `--format` has no meaning on `publish` (it always writes all four whole-year formats plus the per-day JSON tree, never a single chosen one), so @@ -2507,6 +2869,7 @@ let () = let reject_emit = reject_emit_flags_for ~format ~from_y ~to_y ~dtstamp in let reject_rite cmd = reject_rite_for cmd rite in let reject_table = reject_table_flags_for ~year ~template ~flavour in + let reject_template_flavour cmd = reject_template_flavour_for cmd ~template ~flavour in let reject_publish = reject_publish_flags_for ~out ~prune in let reject_lang = reject_lang_for ~lang ~raw in let reject_lang_sub = reject_lang_subcommand_flags_for ~dump ~check ~list ~show in @@ -2525,6 +2888,28 @@ let () = on account of a language it never uses -- those commands reject [--lang]/[--raw] outright instead, via [reject_lang] above. *) let resolved_lang () = load_lang ~raw ~flag:lang ~config:(Colitur_naming.Config.lang config) in + (* Fix 2 (cli-flags-report, 2026-08-27; corrected against the EF + byte-identity constraint, same date): `rubrics` gained + --lang/--raw, but MUST NOT change `colitur rubrics `'s own + no-flag output -- that exact invocation is one of the five this + task's own brief holds to byte-identical output against a + pre-branch build, and `rubrics` never resolved a language at all + before this feature existed, so ANY non-identity default would + break it. [resolved_lang]'s own default ("la", [load_lang]'s + third-priority fallback, shared by every OTHER naming-bearing + command) is therefore NOT reused here: [rubrics_lang] resolves + through the ordinary flag/config chain only when [--lang] or + [--raw] was ACTUALLY given on this invocation; with neither, it + is [Lang.raw] outright, regardless of any `lang = ...` a config + file sets for every other command. This is a deliberate, + documented exception to "flag > config > default" -- config-level + naming is a standing preference for commands that HAD naming + before rubrics did; a user who never asked `rubrics` for a name at + all keeps getting exactly what they always got. *) + let rubrics_lang () = + if raw || lang <> None then load_lang ~raw ~flag:lang ~config:(Colitur_naming.Config.lang config) + else Colitur_naming.Lang.raw + in (* Config supplies a DEFAULT overlay list only when NO --overlay was given at all -- not merged with a partial CLI list -- so the precedence stays exactly flag > config > default, the same @@ -2567,14 +2952,13 @@ let () = with_year ys easter_report | [ "temporal"; ys ] -> reject_overlays_for "temporal" overlays; - reject_rite "temporal"; reject_emit "temporal"; reject_table "temporal"; reject_publish "temporal"; reject_lang "temporal"; reject_lang_sub "temporal"; reject_sigla "temporal"; - with_year ys temporal_report + with_year ys (temporal_report ~rite:(resolve_rite rite)) | "check" :: (_ :: _ as files) -> reject_overlays_for "check" overlays; reject_rite "check"; @@ -2662,20 +3046,38 @@ let () = ~flavour_flag:flavour ~overlays_flag:overlays ~sigla_style_flag:sigla_style ~sigla_book_flag:sigla_book ~sigla_tradition_flag:sigla_tradition config - | [ "day"; ys ] -> + (* Fix 3 (cli-flags-report, 2026-08-27): `day`/`readings`/`rubrics` + each match "cmd :: rest" with [rest] a positional year (as + before) or empty (a bare command word, [--year] carrying the + year instead) -- [List.length rest <= 1] keeps a genuine extra + argument (`colitur day 2026 extra`) falling through to [usage ()] + exactly as it always has. [resolve_single_year] then reconciles + that against [--year], accepting either, agreeing, and refusing + disagreement -- see its own citation above. *) + | "day" :: rest when List.length rest <= 1 -> reject_emit "day"; - reject_table "day"; + reject_template_flavour "day"; reject_publish "day"; reject_lang_sub "day"; reject_sigla "day"; + let ys = + resolve_single_year "day" + ~positional:(match rest with [ ys ] -> Some ys | _ -> None) + ~flag:year + in (match resolve_rite rite with | `Ef -> with_year ys (day_report ~lang:(resolved_lang ()) ~overlays:effective_overlays) | `Of -> with_year ys (day_report_of ~lang:(resolved_lang ()) ~overlays:effective_overlays)) - | [ "readings"; ys ] -> + | "readings" :: rest when List.length rest <= 1 -> reject_emit "readings"; - reject_table "readings"; + reject_template_flavour "readings"; reject_publish "readings"; reject_lang_sub "readings"; + let ys = + resolve_single_year "readings" + ~positional:(match rest with [ ys ] -> Some ys | _ -> None) + ~flag:year + in let lang_t = resolved_lang () in let sigla = load_sigla ~raw ~lang_t ~sigla_style_flag:sigla_style ~sigla_book_flag:sigla_book @@ -2684,25 +3086,45 @@ let () = (match resolve_rite rite with | `Ef -> with_year ys (readings_report ~lang:lang_t ~sigla ~overlays:effective_overlays) | `Of -> with_year ys (readings_report_of ~lang:lang_t ~sigla ~overlays:effective_overlays)) - | [ "rubrics"; ys ] -> + | "rubrics" :: rest when List.length rest <= 1 -> (* --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. *) + is said. Fix 2 (cli-flags-report, 2026-08-27): --lang/--raw are + now ALSO accepted -- [said] (rubrics_line's own formulary-slug + column) is a machine key exactly like [day_line]'s own [slug_s], + and there turned out to be no principled reason for this row + alone to refuse translating it (see [rubrics_name]'s own + citation). --sigla-* stay refused: this row still prints no + citation for them to act on. Naming resolves through + [rubrics_lang], NOT the shared [resolved_lang] every other + naming-bearing command uses -- see that function's own + citation for why (the EF byte-identity constraint on this + exact no-flag invocation). *) reject_emit "rubrics"; - reject_table "rubrics"; + reject_template_flavour "rubrics"; reject_publish "rubrics"; + (* --lang/--raw refused, and PRINCIPLED rather than an oversight. + This row is a date, a slug, a source keyword, two booleans and a + preface key -- not one field is display text, so there is nothing + to translate and nothing to strip. An earlier pass added a name + column so --lang WOULD have something to act on; that changed the + default output from six tab-separated fields to seven, breaking + every existing consumer and the byte-identical-EF rule with it. + Documenting the asymmetry is the cheaper correct answer. + Contrast `day`/`readings`, which carry a name and take both. *) reject_lang "rubrics"; reject_lang_sub "rubrics"; reject_sigla "rubrics"; - reject_rite "rubrics"; - with_year ys (rubrics_report ~overlays:effective_overlays) + let ys = + resolve_single_year "rubrics" + ~positional:(match rest with [ ys ] -> Some ys | _ -> None) + ~flag:year + in + with_year ys (rubrics_report ~rite:(resolve_rite rite) ~lang:(rubrics_lang ()) ~overlays:effective_overlays) | [ "emit" ] -> ( reject_table "emit"; reject_publish "emit"; reject_lang_sub "emit"; - reject_rite "emit"; match (match format with Some f -> Some f | None -> Colitur_naming.Config.format config) with | None -> Printf.eprintf "colitur: emit requires --format csv|json|sexp|xml|ics\n"; @@ -2718,47 +3140,46 @@ let () = load_sigla ~raw ~lang_t ~sigla_style_flag:sigla_style ~sigla_book_flag:sigla_book ~sigla_tradition_flag:sigla_tradition ~config in + let rite = resolve_rite rite in with_year from_ys (fun from_y -> with_year to_ys (fun to_y -> - emit_report ~lang:lang_t ~sigla ~format ~overlays:effective_overlays ~dtstamp - ~from_y ~to_y)))) - | [ ("table" | "render") as cmd ] -> ( + emit_report ~rite ~lang:lang_t ~sigla ~format ~overlays:effective_overlays + ~dtstamp ~from_y ~to_y)))) + (* Fix 3, same shape as `day`/`readings`/`rubrics` above: a bare + [cmd] (year from [--year], as before `table`/`render` accepted + any year at all) or [cmd; ys] (year positional, NEW). *) + | (("table" | "render") as cmd) :: rest when List.length rest <= 1 -> ( reject_emit cmd; reject_publish cmd; reject_lang_sub cmd; - reject_rite cmd; + let positional_year = match rest with [ ys ] -> Some ys | _ -> None in match (match template with Some t -> Some t | None -> Colitur_naming.Config.template config) with | None -> - Printf.eprintf "colitur: %s requires --year YEAR and --template FILE\n" cmd; + Printf.eprintf "colitur: %s requires a year (positional or --year) and --template FILE\n" cmd; exit 2 | Some template -> ( - match year with - | None -> - Printf.eprintf "colitur: %s requires --year YEAR and --template FILE\n" cmd; - exit 2 - | Some ys -> - let lang_t = resolved_lang () in - let sigla = - load_sigla ~raw ~lang_t ~sigla_style_flag:sigla_style ~sigla_book_flag:sigla_book - ~sigla_tradition_flag:sigla_tradition ~config - in - with_year ys (fun y -> - table_report ~lang:lang_t ~sigla ~template - (* flag > config > infer from the template's own - extension. None here still means "infer", which is - the usual case, so this is an option-or rather than - a Config.resolve with a default. *) - ~flavour_opt: - (match flavour with - | Some _ -> flavour - | None -> Colitur_naming.Config.flavour config) - ~overlays:effective_overlays y) - )) + let ys = resolve_single_year cmd ~positional:positional_year ~flag:year in + let lang_t = resolved_lang () in + let sigla = + load_sigla ~raw ~lang_t ~sigla_style_flag:sigla_style ~sigla_book_flag:sigla_book + ~sigla_tradition_flag:sigla_tradition ~config + in + let rite = resolve_rite rite in + with_year ys (fun y -> + table_report ~rite ~lang:lang_t ~sigla ~template + (* flag > config > infer from the template's own + extension. None here still means "infer", which is + the usual case, so this is an option-or rather than + a Config.resolve with a default. *) + ~flavour_opt: + (match flavour with + | Some _ -> flavour + | None -> Colitur_naming.Config.flavour config) + ~overlays:effective_overlays y))) | [ "publish" ] -> ( reject_table "publish"; reject_format_for "publish" format; reject_lang_sub "publish"; - reject_rite "publish"; match out with | None -> Printf.eprintf "colitur: publish requires --out DIR\n"; @@ -2774,6 +3195,7 @@ let () = load_sigla ~raw ~lang_t ~sigla_style_flag:sigla_style ~sigla_book_flag:sigla_book ~sigla_tradition_flag:sigla_tradition ~config in + let rite = resolve_rite rite in with_year from_ys (fun from_y -> with_year to_ys (fun to_y -> (* [publish_report] writes many files across a whole @@ -2792,7 +3214,7 @@ let () = underlying errno in [Sys_error] instead -- both are real on this path, so both are caught. *) try - publish_report ~lang:lang_t ~sigla ~from_y ~to_y ~out + publish_report ~rite ~lang:lang_t ~sigla ~from_y ~to_y ~out ~overlays:effective_overlays ~dtstamp ~prune with | Unix.Unix_error (e, fn, arg) -> -- cgit v1.3