diff options
Diffstat (limited to 'bin/main.ml')
| -rw-r--r-- | bin/main.ml | 710 |
1 files changed, 566 insertions, 144 deletions
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. + 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. - [d.formulary] is documented as [Some] on every day of every year for EF, + 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 - "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">\n\ - <title>colitur</title>\n\ - <style>body{font-family:sans-serif;max-width:40em;margin:2em auto;line-height:1.5}\n\ - code{background:#f4f4f4;padding:.1em .3em}</style></head><body>\n\ - <h1>colitur</h1>\n\ - <p>Liturgical calendar of the 1962 Missale Romanum. Citations only \xe2\x80\x94 never scripture text.</p>\n"; + (Printf.sprintf + "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">\n\ + <title>colitur</title>\n\ + <style>body{font-family:sans-serif;max-width:40em;margin:2em auto;line-height:1.5}\n\ + code{background:#f4f4f4;padding:.1em .3em}</style></head><body>\n\ + <h1>colitur</h1>\n\ + <p>%s Citations only \xe2\x80\x94 never scripture text.</p>\n" + missal_sentence); Buffer.add_string b "<h2>Subscribe</h2>\n<ul>\n"; for y = from_y to to_y do - Buffer.add_string b (Printf.sprintf "<li><a href=\"ef/%d.ics\">ef/%d.ics</a></li>\n" y y) + Buffer.add_string b + (Printf.sprintf "<li><a href=\"%s/%d.ics\">%s/%d.ics</a></li>\n" rite_id y rite_id y) done; Buffer.add_string b "</ul>\n<h2>Data</h2>\n<ul>\n"; for y = from_y to to_y do Buffer.add_string b (Printf.sprintf - "<li>%d: <a href=\"ef/%d.json\">json</a> <a href=\"ef/%d.csv\">csv</a> \ - <a href=\"ef/%d.xml\">xml</a> \xe2\x80\x94 per-day at <code>ef/%d/MM/DD.json</code></li>\n" - y y y y y) + "<li>%d: <a href=\"%s/%d.json\">json</a> <a href=\"%s/%d.csv\">csv</a> \ + <a href=\"%s/%d.xml\">xml</a> \xe2\x80\x94 per-day at <code>%s/%d/MM/DD.json</code></li>\n" + y rite_id y rite_id y rite_id y rite_id y) done; Buffer.add_string b "</ul>\n<p>Contract: <a href=\"schema/day-v1.json\">schema/day-v1.json</a></p>\n\ </body></html>\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 <year> Easter, and the movable feasts anchored to it - colitur temporal <year> the temporal cycle, one line per day + colitur temporal <year> [--rite ef|of] + 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> [--rite ef|of] [--overlay FILE ...] [--lang CODE|FILE] [--raw] - colitur rubrics <year> [--overlay FILE ...] - colitur emit --format csv|json|sexp|xml|ics --from Y --to Y + colitur day|readings|rubrics [<year>] [--year Y] [--rite ef|of] + [--overlay FILE ...] [--lang CODE|FILE] [--raw] + <year> 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>] [--year Y] --template FILE [--rite ef|of] + [--flavour X] [--overlay FILE ...] [--lang CODE|FILE] [--raw] + colitur render --template FILE [<year>] [--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 <year> 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 <lang-dir>/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/<year>.{json,csv,xml,ics} one civil year, all days - ef/<year>/<mm>/<dd>.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 + <rite>/<year>.{json,csv,xml,ics} one civil year, all days + <rite>/<year>/<mm>/<dd>.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 + + <rite> 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 <year>`'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) -> |
